Users want to swap what's in a pane without removing the pane from the layout. "Replace pane" detaches the current content (terminal keeps running in background), then shows the pane picker so the user can choose a new agent/browser/editor/shell.
- Right-click context menu option "Replace pane" on: pane header, terminal content, browser content, editor content
- Detach terminal (keep PTY running) — same cleanup as close
- Always available, even when it's the only pane in a tab
- Replaces pane content with
{ kind: 'picker' }, resetting auto-derived title
Add a replacePane reducer alongside updatePaneContent. It:
- Sets pane content to
{ kind: 'picker' } - Clears
paneTitleSetByUser[tabId][paneId]so the title resets - Updates
paneTitles[tabId][paneId]to the picker-derived title ("New Tab")
This is necessary because updatePaneContent alone won't reset user-set titles — if someone renamed a pane then replaced it, the old name would stick.
Export the action from the slice.
- Add
replacePane: (tabId: string, paneId: string) => voidtoMenuActionstype - Add a "Replace pane" menu item to:
target.kind === 'pane'— after "Rename pane", with a separator before ittarget.kind === 'terminal'— at the end, after a separatortarget.kind === 'browser'— at the end, after a separatortarget.kind === 'editor'— at the end, after a separator
- New
replacePanecallback:- Guard:
if (!panes[tabId]) return(handles stale menu / closed tab) - Look up pane content via
findPaneContent(panes[tabId], paneId) - If content is terminal with
terminalId, sendws.send({ type: 'terminal.detach', terminalId }) - Dispatch
replacePane({ tabId, paneId })
- Guard:
- Wire into the
actionsobject passed tobuildMenuItems
Unit test (test/unit/client/store/panesSlice.test.ts):
replacePanesets content to{ kind: 'picker' }replacePaneclearspaneTitleSetByUserand resets derived titlereplacePaneon non-existent pane is a no-op
Unit test (test/unit/client/context-menu/menu-defs.test.ts — new file):
buildMenuItemsfortarget.kind === 'pane'includes "Replace pane" itembuildMenuItemsfortarget.kind === 'terminal'includes "Replace pane" itembuildMenuItemsfortarget.kind === 'browser'includes "Replace pane" itembuildMenuItemsfortarget.kind === 'editor'includes "Replace pane" item
E2e test (test/e2e/replace-pane.test.tsx — new file):
- Replace a terminal pane: right-click → Replace pane → verify picker is shown, terminal detached
- Replace in single-pane tab: verify works and picker renders
- Replace a renamed pane: verify title resets to "New Tab"
tab.terminalId was not cleared when panes were closed or replaced, causing openTerminal dedup to focus stale tabs. Fixed in this branch: clearStaleTabTerminalId helper clears tab.terminalId when the detached terminal matches, applied to replacePaneAction, closePane action (ContextMenuProvider), and handleClose (PaneContainer).
src/store/panesSlice.ts— newreplacePaneactionsrc/components/context-menu/menu-defs.ts— new menu items +MenuActionstypesrc/components/context-menu/ContextMenuProvider.tsx— new handler + wiringtest/unit/client/store/panesSlice.test.ts— Redux teststest/unit/client/context-menu/menu-defs.test.ts— menu definition tests (new)test/e2e/replace-pane.test.tsx— e2e tests (new)
npm test— all existing + new tests pass- Manual: right-click pane header → "Replace pane" → pane shows picker
- Manual: right-click terminal content → "Replace pane" → terminal detaches, picker shown
- Manual: right-click browser/editor content → "Replace pane" → picker shown
- Manual: single-pane tab → "Replace pane" still available and works
- Manual: renamed pane → "Replace pane" → title resets to "New Tab"