fix(shutdown): gate legacy terminal host on !anyMountedWorktreeHasLayout

Shutdown-from-focused calls setActiveWorktree(null) before
shutdownWorktreeTerminals, which made effectiveActiveLayout undefined
while layoutByWorktree still held the shut-down worktree's layout.
The legacy render branch only checked !effectiveActiveLayout, so it
mounted fresh TerminalPanes for every mounted worktree, each running
connectPanePty → startFreshSpawn → new PTY ~244ms after shutdown,
re-lighting the sidebar status dot green.

Verified via /electron: with the guard in place, the target tab's
ptyId stays null after shutdown instead of jumping to a new id.
This commit is contained in:
Neil 2026-04-18 01:41:44 -07:00
parent 2105a0a780
commit 896b1f4c86

View file

@ -879,14 +879,25 @@ function Terminal(): React.JSX.Element | null {
</div>
) : null}
{!effectiveActiveLayout && (
{!effectiveActiveLayout && !anyMountedWorktreeHasLayout && (
<>
{/* Why: split-group layouts render their own terminal/browser/editor
surfaces inside TabGroupPanel. Keeping the legacy workspace-level
panes mounted underneath as hidden DOM creates duplicate
TerminalPane/BrowserPane instances for the same tab, which lets
two React trees race over one PTY or webview. Render only one
surface model at a time. */}
surface model at a time.
Also gate on !anyMountedWorktreeHasLayout: when the active
worktree goes null (e.g. during shutdown-from-focused, which
calls setActiveWorktree(null) before shutdownWorktreeTerminals)
effectiveActiveLayout becomes undefined but other mounted
worktrees still have layouts. Without this guard, the legacy
branch mounts fresh TerminalPanes for every worktree in
mountedWorktreeIdsRef, each running connectPanePty
startFreshSpawn new PTY. That respawn is exactly what flips
getWorktreeStatus back to 'active' and re-lights the sidebar
dot green moments after the user clicked Shutdown. */}
{/* Terminal panes container - hidden when editor tab active */}
<div
className={`relative flex-1 min-h-0 overflow-hidden ${