Skip to content

fix: instant terminal switching + flicker-free rendering - #24

Merged
aterrylu merged 4 commits into
mainfrom
terry/instant-terminal-switching
Mar 12, 2026
Merged

fix: instant terminal switching + flicker-free rendering#24
aterrylu merged 4 commits into
mainfrom
terry/instant-terminal-switching

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

Summary

  • VSCode-style session keep-alive: Terminal instances stay mounted across session switches — only CSS visibility toggles. Eliminates the violent flashing caused by destroying and recreating terminals on every switch.
  • xterm.js 5 → 6 upgrade: Native DEC 2026 synchronized output support. Claude Code wraps redraws in BSU/ESU sequences — xterm.js 6 buffers these and renders atomically, eliminating the remaining per-update flicker.
  • Review fixes: Early-return bug fix (terminals no longer unmount when no session selected), dead code removal, React.memo on SessionPane, lazy WebGL loading, zero-dimension fit guard.

Architecture

graph LR
  A[SessionViewManager] -->|renders all| B[SessionPane A]
  A -->|renders all| C[SessionPane B]
  A -->|renders all| D[SessionPane C]
  B -->|visible| E[useTerminal + WebGL]
  C -->|hidden| F[useTerminal + canvas]
  D -->|hidden| G[useTerminal + canvas]
  style B fill:#4ade80,color:#000
  style C fill:#94a3b8,color:#000
  style D fill:#94a3b8,color:#000
Loading
  • All live sessions render simultaneously; only the active one is display: flex
  • WebGL addon loads lazily on first visibility (prevents browser context exhaustion)
  • FitAddon skips hidden containers (0×0 dimensions) to avoid bogus resize messages
  • React.memo on SessionPane prevents re-renders from 5s session poll

Test plan

  • Switch between 2+ active sessions — should be instant, no flash
  • Create new session while others are running — existing terminals preserved
  • Session ends (PTY exit) — removed from list, other sessions unaffected
  • Claude Code streaming output — reduced/eliminated flicker vs before
  • No session selected — placeholder shown, terminals still alive in background

🤖 Generated with Claude Code

aterrylu and others added 2 commits March 11, 2026 15:35
Replace destroy-and-recreate pattern with persistent session views.
All terminal instances stay alive in memory; switching sessions just
toggles container visibility. Eliminates violent redraw/flashing.

- SessionViewManager: renders all live sessions, toggles display
- SessionPane: extensible view container per session (terminal now,
  VNC/graphics views in the future)
- useTerminal: takes explicit sessionId param for multi-instance use
- Remove TerminalView (replaced by SessionPane)
- Remove key={sessionId} from App.tsx (was forcing remounts)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix early-return bug that unmounted all terminals when no session selected
- Remove dead mountedRef code
- Wrap SessionPane in React.memo to prevent re-renders on 5s poll
- Guard FitAddon.fit() against zero dimensions in hidden containers
- Lazy-load WebGL addon on first visibility (prevents context exhaustion)
- Upgrade xterm.js 5→6 for native DEC 2026 synchronized output support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@aterrylu
aterrylu enabled auto-merge (squash) March 12, 2026 05:44
// Lazily load WebGL only when the terminal becomes visible
if (!webglLoaded) {
try {
terminal.loadAddon(new WebglAddon());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Suggestion

Problem: webglLoaded is a closure-local flag, so once a hidden terminal becomes visible and loads WebGL, that context lives for the lifetime of the terminal instance. With many sessions open and the user having visited each at least once, you can accumulate N WebGL contexts.

Why it matters: Browsers cap WebGL contexts at ~16 (Chrome) or ~8 (Safari). Exceeding this silently falls back to software rendering — which is benign but invisible to the user, and the console.warn here only fires during the initial load attempt, not on the silent eviction.

Suggested fix: Track active WebGL addons and dispose() on hidden terminals, keeping only the visible one GPU-accelerated:

// In useTerminal, after loading WebGL:
const webglAddonRef = useRef<WebglAddon | null>(null);

// When becoming visible (ResizeObserver, offsetWidth > 0):
if (!webglAddonRef.current) {
  try {
    const addon = new WebglAddon();
    terminal.loadAddon(addon);
    webglAddonRef.current = addon;
  } catch (err) { /* fall through to canvas */ }
}

// When becoming hidden (visibility toggle on SessionPane):
// webglAddonRef.current?.dispose(); webglAddonRef.current = null;

Not blocking for the typical 2–5 session use case, but worth addressing before supporting double-digit session counts.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — implemented. WebGL addon is now disposed when a terminal becomes hidden (ResizeObserver detects 0×0) and reloaded when it becomes visible again. Only the active terminal holds a GPU context at any time.

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean implementation of the VSCode-style keep-alive pattern — the architecture (SessionViewManager → SessionPane → useTerminal) is well-structured and the guards (zero-dim fit skip, lazy WebGL, isActiveRef for multi-instance status) are all correct. Left one suggestion about WebGL context accumulation across many sessions (not blocking). xterm v6 synchronized output support is a nice bonus that directly addresses the per-update flicker.

Only the visible terminal holds a GPU context — WebGL addon is disposed
when a terminal becomes hidden and reloaded when it becomes visible.
Resolves merge conflicts with main.

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

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous suggestion (WebGL context accumulation) is fully addressed — disposing on hidden + reloading on visible is the right pattern. One minor nit: sessionId !== null is stricter than the old !sessionId guard; if the store ever initialises sessionId as undefined the placeholder would not render, but in practice this is fine if the type is string | null. Architecture is clean, the ResizeObserver-driven WebGL lifecycle is correct, and the xterm v6 synchronized output support is a solid bonus. Ship it. ✅

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@aterrylu
aterrylu merged commit 2c29878 into main Mar 12, 2026
1 check passed
@aterrylu
aterrylu deleted the terry/instant-terminal-switching branch March 12, 2026 05:54
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.

2 participants