Skip to content

Stabilize terminal mount across pane moves and splits#667

Merged
mehmetozguldev merged 3 commits into
athasdev:masterfrom
RA1NCS:fix/terminal-split-stable-portal
May 7, 2026
Merged

Stabilize terminal mount across pane moves and splits#667
mehmetozguldev merged 3 commits into
athasdev:masterfrom
RA1NCS:fix/terminal-split-stable-portal

Conversation

@RA1NCS
Copy link
Copy Markdown
Contributor

@RA1NCS RA1NCS commented May 7, 2026

Summary

Fixes #648.

The previous fix (#896dcbf8) added snapshot save/restore to mask the symptom, but the actual root cause is that <XtermTerminal> was being unmounted and remounted whenever a buffer moved between panes (editor pane split, tab move, etc.). Snapshot replay then corrupted alt-buffer state for any TUI in flight (Claude Code, vim, htop, lazygit), produced duplicate banners, narrow reflow, and other rendering artifacts depending on timing.

This PR replaces the masking layer with a stable mount.

Architecture

  • New <TerminalHost /> mounted at app root holds every live xterm instance for the lifetime of its session.
  • Each pane renders a <TerminalSlot sessionId> placeholder div instead of <XtermTerminal> directly. Slots register themselves in terminal-slots-store.
  • TerminalHost keeps a stable wrapper <div> per session and re-parents it (raw DOM appendChild) into whichever slot is currently registered. When no slot exists for a brief window during a pane move, the wrapper is parked in an offscreen container so xterm stays mounted.
  • Buffer moves now only swap a wrapper's DOM parent — no React unmount, no PTY churn, no snapshot dance.

Other fixes folded in

  • Native mousedown listener on the slot routes activation to the host pane. Portal rendering breaks React event bubbling (events bubble through React tree, not DOM tree), so the existing handlePaneMouseDownCapture in PaneContainer was never seeing clicks on the portaled xterm — pane stayed inactive, isActive stayed false, terminal looked muted (opacity-60) and the tab stayed gray.
  • After in-tab split (handleSplitView), explicitly restore the initiating terminal as active. createTerminal swaps activeTerminalId to the new companion, but the companion is rendered only inside the initiator's iter in terminal-container.tsx's map — so the split layout was hidden behind the new fresh pane until the user switched tabs.

Removed

  • saveSessionSnapshot / getSessionSnapshot / clearSessionSnapshot in terminal-store.
  • TerminalViewSnapshot + viewSnapshot + serializedContent on Terminal.
  • terminal-store-snapshot.test.ts — the behavior it covered no longer exists.
  • All snapshot wiring in terminal.tsx and use-terminal-connection.ts.

Test plan

  • Open a terminal, run seq 1 100, split editor pane horizontally with a markdown file → terminal pane retains scrollback, no duplicate output, no narrow reflow.
  • Same with Claude Code TUI running → CC content survives split, alt-buffer not corrupted.
  • Close the split → terminal reflows back to full width, no stale narrow content.
  • Click into terminal pane → pane becomes active, terminal opacity normalizes, tab styling reflects active state.
  • In-tab split (split icon in terminal tab bar) → both panes visible side-by-side; original session preserved.
  • bun run typecheck clean.
  • bun run lint clean (no new warnings).

Fixes athasdev#648

Mount each xterm exactly once per session at app root via TerminalHost
and re-parent its DOM (raw appendChild) into whichever TerminalSlot is
currently displaying it. Editor-pane splits, terminal-tab splits, tab
moves, and other layout changes only swap a portal target — no React
unmount, no PTY churn.

Removes the snapshot save/restore dance entirely. Snapshot replay was
masking the underlying remount bug and corrupting alt-buffer state when
TUIs (Claude Code, vim, htop, etc.) were active during a layout change.

Slots route mousedown to onActivate via a native listener, since portal
rendering breaks React event bubbling and the host pane would otherwise
never see the click.

Also restores the initiating terminal as active after the in-tab split,
so the split layout (companion rendered inside the initiator's iter)
stays visible instead of swapping to the new companion.
@RA1NCS
Copy link
Copy Markdown
Contributor Author

RA1NCS commented May 7, 2026

@mehmetozguldev Have a look please → tested, took a LONG time to fix

@mehmetozguldev mehmetozguldev self-requested a review May 7, 2026 10:54
Copy link
Copy Markdown
Member

@mehmetozguldev mehmetozguldev left a comment

Choose a reason for hiding this comment

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

LGTM

@mehmetozguldev mehmetozguldev merged commit e410dda into athasdev:master May 7, 2026
2 checks passed
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.

Split tab clears terminal state and scrollback

2 participants