mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 00:03:20 +00:00
Sweep pane foreground entries on tab close and worktree teardown
Wholesale teardowns (tab close, worktree sleep/remove) retire pane keys without per-pane close events, stranding unreachable foreground entries for the renderer's lifetime. Mirror the agent-status prefix sweeps at both chokepoints; no-op sweeps bail without a state change. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createTestStore } from './store-test-helpers'
|
||||
import type { TerminalTab } from '../../../../shared/types'
|
||||
|
||||
function terminalTab(id: string, worktreeId: string): TerminalTab {
|
||||
return {
|
||||
id,
|
||||
ptyId: null,
|
||||
worktreeId,
|
||||
title: 'Terminal 1',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1
|
||||
}
|
||||
}
|
||||
|
||||
describe('pane foreground agent slice', () => {
|
||||
it('sets, value-bails, and clears entries per pane key', () => {
|
||||
const store = createTestStore()
|
||||
store
|
||||
.getState()
|
||||
.setPaneForegroundAgent('tab-1:leaf-1', { agent: 'aider', shellForeground: false })
|
||||
const first = store.getState().paneForegroundAgentByPaneKey
|
||||
|
||||
store
|
||||
.getState()
|
||||
.setPaneForegroundAgent('tab-1:leaf-1', { agent: 'aider', shellForeground: false })
|
||||
expect(store.getState().paneForegroundAgentByPaneKey).toBe(first)
|
||||
|
||||
store.getState().clearPaneForegroundAgent('tab-1:leaf-1')
|
||||
expect(store.getState().paneForegroundAgentByPaneKey).toEqual({})
|
||||
})
|
||||
|
||||
it('sweeps only the closed tab prefix, not sibling tabs or prefix-share ids', () => {
|
||||
const store = createTestStore()
|
||||
store
|
||||
.getState()
|
||||
.setPaneForegroundAgent('tab-1:leaf-1', { agent: 'aider', shellForeground: false })
|
||||
store
|
||||
.getState()
|
||||
.setPaneForegroundAgent('tab-10:leaf-1', { agent: 'codex', shellForeground: false })
|
||||
|
||||
store.getState().clearPaneForegroundAgentByTabPrefix('tab-1')
|
||||
|
||||
expect(Object.keys(store.getState().paneForegroundAgentByPaneKey)).toEqual(['tab-10:leaf-1'])
|
||||
})
|
||||
|
||||
it('sweeps every tab of a worktree on wholesale teardown', () => {
|
||||
const store = createTestStore()
|
||||
store.setState({
|
||||
tabsByWorktree: {
|
||||
'wt-1': [terminalTab('tab-1', 'wt-1'), terminalTab('tab-2', 'wt-1')],
|
||||
'wt-2': [terminalTab('tab-3', 'wt-2')]
|
||||
}
|
||||
})
|
||||
store
|
||||
.getState()
|
||||
.setPaneForegroundAgent('tab-1:leaf-1', { agent: 'aider', shellForeground: false })
|
||||
store.getState().setPaneForegroundAgent('tab-2:leaf-1', { agent: null, shellForeground: true })
|
||||
store
|
||||
.getState()
|
||||
.setPaneForegroundAgent('tab-3:leaf-1', { agent: 'codex', shellForeground: false })
|
||||
|
||||
const before = store.getState().paneForegroundAgentByPaneKey
|
||||
store.getState().clearPaneForegroundAgentByWorktree('wt-missing')
|
||||
expect(store.getState().paneForegroundAgentByPaneKey).toBe(before)
|
||||
|
||||
store.getState().clearPaneForegroundAgentByWorktree('wt-1')
|
||||
|
||||
expect(Object.keys(store.getState().paneForegroundAgentByPaneKey)).toEqual(['tab-3:leaf-1'])
|
||||
})
|
||||
})
|
||||
@@ -19,6 +19,10 @@ export type PaneForegroundAgentSlice = {
|
||||
paneForegroundAgentByPaneKey: Record<string, PaneForegroundAgentEntry>
|
||||
setPaneForegroundAgent: (paneKey: string, entry: PaneForegroundAgentEntry) => void
|
||||
clearPaneForegroundAgent: (paneKey: string) => void
|
||||
/** Wholesale teardown sweeps (tab close, worktree sleep/remove) retire pane
|
||||
* keys without per-pane close events — clear their entries too. */
|
||||
clearPaneForegroundAgentByTabPrefix: (tabIdPrefix: string) => void
|
||||
clearPaneForegroundAgentByWorktree: (worktreeId: string) => void
|
||||
}
|
||||
|
||||
export const createPaneForegroundAgentSlice: StateCreator<
|
||||
@@ -52,5 +56,34 @@ export const createPaneForegroundAgentSlice: StateCreator<
|
||||
delete next[paneKey]
|
||||
return { paneForegroundAgentByPaneKey: next }
|
||||
})
|
||||
},
|
||||
clearPaneForegroundAgentByTabPrefix: (tabIdPrefix) => {
|
||||
set((s) => clearEntriesByTabPrefixes(s.paneForegroundAgentByPaneKey, [`${tabIdPrefix}:`]) ?? s)
|
||||
},
|
||||
clearPaneForegroundAgentByWorktree: (worktreeId) => {
|
||||
set((s) => {
|
||||
const prefixes = (s.tabsByWorktree[worktreeId] ?? []).map((tab) => `${tab.id}:`)
|
||||
return clearEntriesByTabPrefixes(s.paneForegroundAgentByPaneKey, prefixes) ?? s
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function clearEntriesByTabPrefixes(
|
||||
entries: Record<string, PaneForegroundAgentEntry>,
|
||||
tabPrefixes: string[]
|
||||
): Pick<PaneForegroundAgentSlice, 'paneForegroundAgentByPaneKey'> | null {
|
||||
if (tabPrefixes.length === 0) {
|
||||
return null
|
||||
}
|
||||
const staleKeys = Object.keys(entries).filter((paneKey) =>
|
||||
tabPrefixes.some((prefix) => paneKey.startsWith(prefix))
|
||||
)
|
||||
if (staleKeys.length === 0) {
|
||||
return null
|
||||
}
|
||||
const next = { ...entries }
|
||||
for (const paneKey of staleKeys) {
|
||||
delete next[paneKey]
|
||||
}
|
||||
return { paneForegroundAgentByPaneKey: next }
|
||||
}
|
||||
|
||||
@@ -1184,6 +1184,9 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
|
||||
// so retention suppressors are planted: a live→gone transition inside the
|
||||
// same frame as the tab close cannot re-snapshot a row we just dropped.
|
||||
get().dropAgentStatusByTabPrefix(tabId)
|
||||
// Why: retired pane keys never recur, so stranded foreground entries would
|
||||
// accumulate for the renderer's whole lifetime.
|
||||
get().clearPaneForegroundAgentByTabPrefix(tabId)
|
||||
// Why: closing a tab permanently retires every pane under it (a reopen mints
|
||||
// a fresh leafId at epoch 0), so drop the panes' hibernation output epochs to
|
||||
// keep that module-level map from growing for the renderer's whole lifetime.
|
||||
@@ -2349,6 +2352,7 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
|
||||
sleepingPaneKeys: opts?.sleepingPaneKeys,
|
||||
retainedCompletionEvidence
|
||||
})
|
||||
get().clearPaneForegroundAgentByWorktree(worktreeId)
|
||||
|
||||
if (ptyIds.length === 0 && expectedRuntimePtyIds.length === 0) {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user