fix(sidebar): open retained agent rows whose exact pane still has a live PTY (#20804)

Retained sidebar rows were unconditionally inert, so an idle agent whose
live status was dropped could not be navigated to even though its terminal
was still running. Gate activation on the runtime PTY map for the exact
stable leaf (stablePaneHasLivePty) so sleeping panes, whose layout bindings
survive sleep, stay inert and are never resumed implicitly.

Fixes #20803

Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
LANG LIU
2026-10-27 18:08:37 -07:00
committed by GitHub
co-authored by Neil
parent 130781678d
commit 5904b204b4
3 changed files with 75 additions and 18 deletions
@@ -51,6 +51,8 @@ function mockAgent({
let mockAgents: DashboardAgentRowData[] = []
let mockAgentActivityDisplayMode: 'compact' | 'full' | undefined
let mockTabsByWorktree: Record<string, { id: string }[]> = {}
let mockTerminalLayoutsByTabId: Record<string, unknown> = {}
let mockPtyIdsByTabId: Record<string, string[]> = {}
let mockStructuredTabIds = new Set<string>()
let mockAgentStatusByPaneKey: Record<string, { worktreeId?: string }> = {}
let mockActiveTabId: string | null = null
@@ -83,8 +85,8 @@ function buildMockStoreState(): Record<string, unknown> {
setActiveTab: mockSetActiveTab,
setActiveTabType: mockSetActiveTabType,
tabsByWorktree: mockTabsByWorktree,
terminalLayoutsByTabId: {},
ptyIdsByTabId: {},
terminalLayoutsByTabId: mockTerminalLayoutsByTabId,
ptyIdsByTabId: mockPtyIdsByTabId,
runtimePaneTitlesByTabId: {},
sendPromptToSidebarAgentTarget: vi.fn(),
settings: {
@@ -173,6 +175,8 @@ describe('WorktreeCardAgents activation', () => {
mockAgents = []
mockAgentActivityDisplayMode = undefined
mockTabsByWorktree = {}
mockTerminalLayoutsByTabId = {}
mockPtyIdsByTabId = {}
mockStructuredTabIds = new Set()
mockAgentStatusByPaneKey = {}
mockActiveTabId = null
@@ -183,6 +187,54 @@ describe('WorktreeCardAgents activation', () => {
)
})
it.each([
['opens a retained row whose exact pane still has a live PTY', ['pty-a'], true],
['keeps a retained row inert when its pane PTY is no longer live', [], false]
])('%s (#20803)', async (_name, livePtyIds, shouldActivate) => {
mockAgentActivityDisplayMode = 'full'
const tabId = 'retained-tab'
const paneKey = makePaneKey(tabId, LEAF_A)
mockAgents = [
{
...mockAgent({ paneKey, tabId, agentType: 'codex', prompt: 'Idle', worktreeId: 'wt-1' }),
rowSource: 'retained',
state: 'done'
}
]
mockTabsByWorktree = { 'wt-1': [{ id: tabId }] }
mockTerminalLayoutsByTabId = {
[tabId]: {
root: {
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: LEAF_A },
second: { type: 'leaf', leafId: LEAF_B }
},
activeLeafId: LEAF_B,
expandedLeafId: null,
ptyIdsByLeafId: { [LEAF_A]: 'pty-a', [LEAF_B]: 'pty-b' }
}
}
mockPtyIdsByTabId = { [tabId]: [...livePtyIds, 'pty-b'] }
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
capturedRowActivations[0].onActivate(tabId, paneKey)
if (shouldActivate) {
expect(activationMocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-1')
expect(activationMocks.activateTabAndFocusPane).toHaveBeenCalledWith(
tabId,
LEAF_A,
expect.anything()
)
} else {
expect(activationMocks.activateAndRevealWorktree).not.toHaveBeenCalled()
expect(activationMocks.activateTabAndFocusPane).not.toHaveBeenCalled()
}
expect(staleAgentRowMocks.dismissStaleAgentRowByKey).not.toHaveBeenCalled()
})
it('activates a projected structured session row through the unified tab path', async () => {
mockAgentActivityDisplayMode = 'full'
const tabId = 'structured-tab'
@@ -396,19 +396,6 @@ describe('WorktreeCardAgents', () => {
expect(markup).toContain('data-pane-key="tab-1:2"')
})
it('keeps retained completion rows passive when activated', async () => {
mockAgentActivityDisplayMode = 'full'
mockAgents = [mockAgent({ rowSource: 'retained', state: 'done' })]
const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents')
renderToStaticMarkup(<WorktreeCardAgents worktreeId="wt-1" />)
expect(capturedRowActivations).toHaveLength(1)
capturedRowActivations[0].onActivate('tab-1', 'tab-1:1')
expect(activationMocks.activateAndRevealWorktree).not.toHaveBeenCalled()
expect(activationMocks.activateTabAndFocusPane).not.toHaveBeenCalled()
})
it('shows orchestration child agent rows under their parent by default', async () => {
mockAgentActivityDisplayMode = 'full'
mockAgents = [
@@ -29,6 +29,7 @@ import { useWorktreeAgentExpansionState } from './worktree-card-agents-expansion
import { translate } from '@/i18n/i18n'
import { activateStructuredAgentSessionTab } from '@/lib/agent-session/structured-agent-session-tab-activation'
import { selectAgentRowsUnread } from './worktree-card-agent-ack-inputs'
import { stablePaneHasLivePty } from '@/lib/sleeping-agent-pane-ownership'
export const SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT =
'orca-suppress-worktree-list-scroll-adjustment'
@@ -185,9 +186,26 @@ const WorktreeCardAgentsBody = React.memo(function WorktreeCardAgentsBody({
},
[worktreeId]
)
const handleActivateRetainedAgent = useCallback(() => {
// Retained rows are completion evidence; activation would resume sleeping sessions, so keep them inert.
}, [])
const handleActivateRetainedAgent = useCallback(
(tabId: string, paneKey: string) => {
const parsed = parsePaneKey(paneKey)
const state = useAppStore.getState()
// Retained rows are completion evidence; only open a still-live pane so a sleeping target pane is never resumed.
if (
parsed?.tabId === tabId &&
state.tabsByWorktree[worktreeId]?.some((tab) => tab.id === tabId) &&
stablePaneHasLivePty(
tabId,
parsed.leafId,
state.ptyIdsByTabId,
state.terminalLayoutsByTabId[tabId]
)
) {
handleActivateAgentTab(tabId, paneKey)
}
},
[handleActivateAgentTab, worktreeId]
)
const now = useNow(30_000)
const { rootRows: rootAgents, childrenByParentPaneKey } = useMemo(