feat(wormhole): Refined Industrial UI redesign#41
Conversation
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>
|
Reviewed commit: Critical Issues1. Race Condition in Click-to-Focus (use-wormhole-client.ts:191-195)The 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 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, Fix: Use a counter or timestamp: onClick={() => createSession(`session-${Date.now()}`)}Security4. XSS Risk in Dynamic Session/Tab NamesSession and tab names from server state are rendered without sanitization:
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 Performance5. Inefficient Re-renders in ChromeBar (ChromeBar.tsx:10-17)Every state change triggers:
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]);Correctness7. Unsafe Tab Indexing (ChromeBar.tsx:38-45)const firstTab = state.tabs.find((t) => t.sessionId === session.id);
// ...
onClick={() => {
if (firstTab) switchTab(firstTab.id);
}}Problem: 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, 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);
}Minor9. Missing Null Check in PaneLayout (PaneLayout.tsx:95)const channel = channels[window.key];
if (channel === undefined) return <div>Loading...</div>;Issue: 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"SummaryHigh priority:
Medium priority:
Low priority:
|
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>
Summary
[data-wormhole]attributeCommits
Source of truth
docs/mockups/terminal-refined-industrial.html🤖 Generated with Claude Code