fix(agent-status): invalidate background presentation mirrors

This commit is contained in:
Brennan Benson
2026-08-14 16:13:05 -07:00
parent ac9542b1c9
commit 4f87ff8a47
8 changed files with 99 additions and 7 deletions
@@ -35,7 +35,8 @@ describe('createHookStatusSessionTabsInvalidator', () => {
['agentType', { agentType: 'codex' }],
['toolName', { toolName: 'Bash' }],
['interactivePrompt', { interactivePrompt: '{"questions":[]}' }],
['interrupted', { interrupted: true }]
['interrupted', { interrupted: true }],
['backgroundOnly', { backgroundOnly: true }]
])('invalidates when %s changes', (_field, payload) => {
const changed = createHookStatusSessionTabsInvalidator()
changed(working())
@@ -28,7 +28,8 @@ export function createHookStatusSessionTabsInvalidator(): {
(previous.agentType ?? null) !== (next.agentType ?? null) ||
(previous.toolName ?? null) !== (next.toolName ?? null) ||
(previous.interactivePrompt ?? null) !== (next.interactivePrompt ?? null) ||
(previous.interrupted ?? false) !== (next.interrupted ?? false)
(previous.interrupted ?? false) !== (next.interrupted ?? false) ||
(previous.backgroundOnly ?? false) !== (next.backgroundOnly ?? false)
)
}
// Why: a cleared pane must re-arm, else the memo swallows the first event of the
+16 -2
View File
@@ -19258,13 +19258,25 @@ describe('OrcaRuntimeService', () => {
runtime.onPtyData(
'hook-only-pty',
'\x1b]9999;{"state":"waiting","prompt":"fix the tests","agentType":"opencode"}\x07',
'\x1b]9999;{"state":"working","prompt":"fix the tests","agentType":"opencode","backgroundOnly":true}\x07',
101
)
await waitForMobileSessionTabsEvents(events, 2)
expect(events).toHaveLength(2)
expect(events[1]?.tabs[0]?.type === 'terminal' && events[1].tabs[0].agentStatus).toEqual(
expect.objectContaining({ state: 'working', backgroundOnly: true })
)
runtime.onPtyData(
'hook-only-pty',
'\x1b]9999;{"state":"waiting","prompt":"fix the tests","agentType":"opencode"}\x07',
102
)
await waitForMobileSessionTabsEvents(events, 3)
expect(events).toHaveLength(3)
expect(events[2]?.tabs[0]?.type === 'terminal' && events[2].tabs[0].agentStatus).toEqual(
expect.objectContaining({ state: 'waiting' })
)
@@ -36714,7 +36726,8 @@ describe('OrcaRuntimeService', () => {
lastAssistantMessage: 'on it',
connectionId: null,
receivedAt: now,
stateStartedAt: now - 100
stateStartedAt: now - 100,
backgroundOnly: true
}
]
})
@@ -36772,6 +36785,7 @@ describe('OrcaRuntimeService', () => {
taskTitle: 'Dispatch prompt work',
displayName: 'Review dispatch prompts and make worker labels distinct',
lastAssistantMessage: 'on it',
backgroundOnly: true,
stateStartedAt: now - 100,
updatedAt: now
})
+2 -1
View File
@@ -10928,7 +10928,8 @@ export class OrcaRuntimeService {
(previous.payload.agentType ?? null) !== (payload.agentType ?? null) ||
(previous.payload.toolName ?? null) !== (payload.toolName ?? null) ||
(previous.payload.interactivePrompt ?? null) !== (payload.interactivePrompt ?? null) ||
(previous.payload.interrupted ?? false) !== (payload.interrupted ?? false)
(previous.payload.interrupted ?? false) !== (payload.interrupted ?? false) ||
(previous.payload.backgroundOnly ?? false) !== (payload.backgroundOnly ?? false)
)
}
@@ -118,6 +118,19 @@ describe('AgentTerminalDialog', () => {
expect(screen.getByText(/Claude · Done/)).toBeInTheDocument()
})
it('labels background-only work without claiming the foreground turn is working', () => {
render(
<AgentTerminalDialog
card={card({ backgroundOnly: true })}
onOpenChange={() => {}}
onReveal={() => {}}
/>
)
expect(screen.getByText(/Claude · Background work/)).toBeInTheDocument()
expect(screen.queryByText(/Claude · Working/)).not.toBeInTheDocument()
})
it('preserves the execution host when revealing a colliding worktree ID', () => {
const onReveal = vi.fn()
render(
@@ -39,6 +39,9 @@ function AgentTerminalFrame({
onOpenChange,
onReveal
}: AgentTerminalFrameProps): React.JSX.Element {
const displayState = dashboardCardDisplayState(card)
const foregroundState =
card.backgroundOnly === true && displayState === 'working' ? 'background' : displayState
const reveal = (): void => {
onReveal({
repoId: card.repoId,
@@ -58,8 +61,7 @@ function AgentTerminalFrame({
</span>
{title}
<span className="text-[11px] text-muted-foreground">
{formatAgentTypeLabel(card.agentType)} ·{' '}
{agentStateLabel(dashboardCardDisplayState(card))}
{formatAgentTypeLabel(card.agentType)} · {agentStateLabel(foregroundState)}
</span>
<Button
type="button"
@@ -1987,6 +1987,64 @@ describe('applyWebSessionTabsSnapshot', () => {
expect(patch.sortEpoch).toBe(1)
})
it('invalidates mirrored presentation caches when only background work remains', () => {
const hostPaneKey = makePaneKey('host-tab-1', LEAF_ID)
const snapshot = makeSnapshot([
{
type: 'terminal',
id: HOST_SURFACE_ID,
title: 'claude [working]',
parentTabId: 'host-tab-1',
leafId: LEAF_ID,
isActive: true,
status: 'ready',
terminal: 'terminal-1',
agentStatus: {
state: 'working',
prompt: 'run the server',
updatedAt: NOW - 100,
stateStartedAt: NOW - 1_000,
agentType: 'claude',
paneKey: hostPaneKey,
tabId: 'host-tab-1',
worktreeId: WT,
stateHistory: []
}
}
])
const initial = applyWebSessionTabsSnapshot(
makeState(),
snapshot,
ENV,
NOW
) as Partial<WebSessionTabsSyncState>
const mirroredPaneKey = Object.keys(initial.agentStatusByPaneKey ?? {})[0]!
const background = applyWebSessionTabsSnapshot(
makeState({ ...initial }),
{
...snapshot,
snapshotVersion: 2,
tabs: snapshot.tabs.map((tab) =>
tab.type === 'terminal' && tab.agentStatus
? {
...tab,
agentStatus: { ...tab.agentStatus, backgroundOnly: true }
}
: tab
)
},
ENV,
NOW
) as Partial<WebSessionTabsSyncState>
expect(background.agentStatusByPaneKey?.[mirroredPaneKey]).toMatchObject({
state: 'working',
backgroundOnly: true
})
expect(background.agentStatusEpoch).toBe(2)
expect(background.sortEpoch).toBe(2)
})
it('applies a marker-only host restart degradation to mirrored agent status', () => {
const hostPaneKey = makePaneKey('host-tab-1', LEAF_ID)
const snapshot = makeSnapshot([
@@ -1293,9 +1293,11 @@ function buildMirroredAgentStatusPatch(
existing?.state === 'done' &&
entry.state === 'done' &&
agentEntryCompletionAt(existing) !== agentEntryCompletionAt(entry)
const backgroundOnlyChanged = existing?.backgroundOnly !== entry.backgroundOnly
const entrySortRelevantChange =
!existing ||
existing.state !== entry.state ||
backgroundOnlyChanged ||
!isAgentStatusFresh(existing, now) ||
entryFreshnessChanged ||
entryAttributionChanged ||