Skip to content

feat(wormhole): Refined Industrial UI redesign#41

Merged
usirin merged 12 commits intomainfrom
umut/wormhole-ui-redesign
Feb 16, 2026
Merged

feat(wormhole): Refined Industrial UI redesign#41
usirin merged 12 commits intomainfrom
umut/wormhole-ui-redesign

Conversation

@usirin
Copy link
Member

@usirin usirin commented Feb 16, 2026

Summary

  • Complete UI redesign of the Wormhole terminal multiplexer
  • Unified chrome bar with base-ui Menu (session dropdown) and Tabs
  • Design token system scoped via [data-wormhole] attribute
  • JetBrains Mono font, amber/gold accent on near-black palette

Commits

  • feat: JetBrains Mono font import, design tokens, CSS module rewrite
  • feat: ChromeBar with session dropdown and tabs (base-ui)
  • feat: SVG pane controls, styled disconnected overlay
  • feat: Wire ChromeBar, delete old SessionBar/TabBar
  • fix: Terminal sizing, tab layout, click-to-focus, remove animations
  • chore: Add external source references to CLAUDE.md
  • docs: Mockup, implementation plans, blog entry

Source of truth

docs/mockups/terminal-refined-industrial.html

🤖 Generated with Claude Code

usirin and others added 9 commits February 15, 2026 21:33
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mations

- Separate opacity wrapper from ghostty-web canvas ref to fix garbled
  text after split operations
- Use width/height: 100% on .pane instead of flex: 1 (Panel inner div
  is not a flex container)
- Add .tabsRoot class to Tabs.Root so it participates in chrome bar flex
- Add client-side focusPane for click-to-focus (optimistic local update)
- Add missing design tokens (accent-hover, bg-active, border-subtle)
- Remove page-load and focus-state animations for snappier UX
- Fix biome formatting (spaces to tabs) and SVG a11y lint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add CF Sandbox SDK and base-ui docs links to external sources table.
Also fix import sort in ChannelMap test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Refined Industrial mockup (source of truth for wormhole UI)
- Implementation plan and fix plan for the redesign
- Blog index entry for standalone essay

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 16, 2026

Reviewed commit: ac7fa21

Critical Issues

1. Race Condition in Click-to-Focus (use-wormhole-client.ts:191-195)

The focusPane implementation uses optimistic local state updates:

focusPane: (path) =>
  setState((s) => ({
    ...s,
    tabs: s.tabs.map((t) => (t.id === s.activeTab ? {...t, focus: path} : t)),
  }))

Problem: This local state can be overwritten by server layout_update messages, causing focus to snap back unexpectedly. The decision doc mentions "server layout_update can override", but there's no debouncing or conflict resolution.

Impact: User clicks pane → focus updates locally → server sends layout_update → focus reverts to server state.

Fix: Either add timestamp-based conflict resolution, debounce server updates after local focus changes, or make server aware of click-to-focus via protocol extension.


2. Memory Leak in Terminal Buffers (use-wormhole-client.ts:110-114)

if (!terminalBuffers.current.has(channel)) {
  terminalBuffers.current.set(channel, []);
}
terminalBuffers.current.get(channel)!.push(payload);

Problem: Buffered terminal data accumulates indefinitely if a terminal component never mounts (e.g., tab closed before render, navigation away, error in component).

Impact: Memory grows unbounded for orphaned channels.

Fix: Add buffer size limit or TTL-based cleanup:

const BUFFER_LIMIT = 100; // messages
if (buffer.length > BUFFER_LIMIT) buffer.shift();

3. Session Naming Collision (ChromeBar.tsx:66)

onClick={() => createSession(`session-${state.sessions.length + 1}`)}

Problem: If a session is destroyed, sessions.length decreases, causing duplicate names (session-1, session-1).

Fix: Use a counter or timestamp:

onClick={() => createSession(`session-${Date.now()}`)}

Security

4. XSS Risk in Dynamic Session/Tab Names

Session and tab names from server state are rendered without sanitization:

  • ChromeBar.tsx:26: {activeSession?.name ?? "—"}
  • ChromeBar.tsx:47: {session.name}
  • ChromeBar.tsx:85: {tab.name}

Concern: If the server or protocol allows user-controlled names, malicious input could inject scripts.

Mitigation: React escapes text nodes by default, so this is LOW risk unless names are used in dangerouslySetInnerHTML elsewhere. Verify server-side validation exists for name inputs.


Performance

5. Inefficient Re-renders in ChromeBar (ChromeBar.tsx:10-17)

Every state change triggers:

  • 2x find() on sessions array
  • 1x filter() on tabs array

Impact: Negligible for <100 sessions/tabs, but could optimize with memoization:

const activeTabRecord = useMemo(() => 
  state.tabs.find((t) => t.id === state.activeTab), 
  [state.tabs, state.activeTab]
);

6. JSON.stringify for Focus Comparison (PaneLayout.tsx:97)

const isFocused = JSON.stringify(path) === JSON.stringify(focus);

Problem: Inefficient for frequent render cycles. Allocates strings on every render.

Fix: Use array equality helper:

const isFocused = path.length === focus.length && 
  path.every((p, i) => p === focus[i]);

Correctness

7. Unsafe Tab Indexing (ChromeBar.tsx:38-45)

const firstTab = state.tabs.find((t) => t.sessionId === session.id);
// ...
onClick={() => {
  if (firstTab) switchTab(firstTab.id);
}}

Problem: firstTab is captured in closure during render. If tabs are modified (e.g., tab closed) before the click handler fires, firstTab may reference a stale/deleted tab.

Impact: Clicking a session in the dropdown could switch to a non-existent tab, causing undefined behavior.

Fix: Look up tab inside the click handler:

onClick={() => {
  const currentFirstTab = state.tabs.find((t) => t.sessionId === session.id);
  if (currentFirstTab) switchTab(currentFirstTab.id);
}}

8. ChannelMap Free-List Not Restored on Deserialization (ChannelMap.ts:75-89)

// does not rebuild the free-list (gaps become permanently lost)

Problem: After deserialization (e.g., DO hibernation recovery), released channels are never reused. If channels 0-100 were allocated, then 0-50 released, fromRecord() sets nextChannel = 101, wasting slots 0-50.

Impact: Accelerates channel exhaustion (max 254 channels).

Fix: Rebuild free-list:

const allocated = new Set(Object.values(record));
for (let i = 0; i < maxSeen + 1; i++) {
  if (!allocated.has(i)) map.freeList.push(i);
}

Minor

9. Missing Null Check in PaneLayout (PaneLayout.tsx:95)

const channel = channels[window.key];
if (channel === undefined) return <div>Loading...</div>;

Issue: channel could be 0 (valid channel), but the check is correct. The variable name channel is slightly misleading since 0 is a valid value.

Suggestion: Rename for clarity:

const channelNum = channels[window.key];
if (channelNum === undefined) return <div>Loading...</div>;

10. Hardcoded Font in TerminalPane (TerminalPane.tsx:23)

fontFamily: "JetBrains Mono"

Issue: No fallback if font fails to load.

Fix: Add fallback:

fontFamily: "JetBrains Mono, monospace"

Summary

High priority:

Medium priority:

Low priority:

usirin and others added 3 commits February 15, 2026 23:00
Module-level docs, class/method/interface annotations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Learnings: react-resizable-panels Panel sizing, ghostty-web canvas
transitions, base-ui Tabs.Root flex layout.
Decision: optimistic click-to-focus via local state.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Complete the documentation pass for packages/sandbox — adds message
direction, frame format descriptions, and layout model docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@usirin usirin marked this pull request as ready for review February 16, 2026 08:36
@usirin usirin merged commit 12bda25 into main Feb 16, 2026
5 checks passed
@usirin usirin deleted the umut/wormhole-ui-redesign branch February 16, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant