mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
Fix late-paired completion notification arming
This commit is contained in:
@@ -853,7 +853,7 @@ export async function refreshWebRuntimeSessionTabsSnapshot(
|
||||
// Why: eager refreshes can resolve after the user switched worktrees; update tabs without stealing focus.
|
||||
const patch = applyFreshWebSessionTabsSnapshot(state, snapshot, environmentId)
|
||||
return patch === state ? state : patch
|
||||
})
|
||||
}, snapshot)
|
||||
} catch (error) {
|
||||
if (options.errorMode === 'throw') {
|
||||
throw error
|
||||
|
||||
@@ -12,6 +12,7 @@ vi.mock('@/hooks/agent-hook-completion-notifications', () => ({
|
||||
}))
|
||||
|
||||
import { useAppStore } from '@/store'
|
||||
import { createAgentCompletionCoordinator } from '@/components/terminal-pane/agent-completion-coordinator'
|
||||
import {
|
||||
applyWebSessionTabsSnapshot,
|
||||
applyWebSessionTabsStorePatch,
|
||||
@@ -67,7 +68,8 @@ function makeAgentSnapshot(
|
||||
function applySnapshot(snapshot: RuntimeMobileSessionTabsResult, live: boolean): void {
|
||||
applyWebSessionTabsStorePatch(
|
||||
(state) => applyWebSessionTabsSnapshot(state, snapshot, ENVIRONMENT_ID, NOW),
|
||||
live ? snapshot : undefined
|
||||
snapshot,
|
||||
live
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,12 +87,12 @@ describe('paired session-tab agent completion notifications', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('forwards accepted live status and excludes snapshot or reconnect replays', () => {
|
||||
it('seeds accepted working snapshots and excludes completed replays', () => {
|
||||
applySnapshot(makeAgentSnapshot(1, NOW), false)
|
||||
expect(mocks.observeAgentHookCompletionForNotification).not.toHaveBeenCalled()
|
||||
expect(mocks.observeAgentHookCompletionForNotification).toHaveBeenCalledTimes(1)
|
||||
|
||||
applySnapshot(makeAgentSnapshot(2, NOW + 1_000), true)
|
||||
expect(mocks.observeAgentHookCompletionForNotification).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.observeAgentHookCompletionForNotification).toHaveBeenCalledTimes(2)
|
||||
|
||||
const turnCompletedAt = NOW + 2_000
|
||||
applySnapshot(makeAgentSnapshot(3, NOW + 2_000, turnCompletedAt), true)
|
||||
@@ -105,6 +107,32 @@ describe('paired session-tab agent completion notifications', () => {
|
||||
})
|
||||
|
||||
applySnapshot(makeAgentSnapshot(4, NOW + 3_000, turnCompletedAt + 1_000), false)
|
||||
expect(mocks.observeAgentHookCompletionForNotification).toHaveBeenCalledTimes(2)
|
||||
expect(mocks.observeAgentHookCompletionForNotification).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('announces one live stamped completion after a late-pair working seed', () => {
|
||||
const dispatchCompletion = vi.fn()
|
||||
const coordinator = createAgentCompletionCoordinator({
|
||||
paneKey: makePaneKey(toWebTerminalSurfaceTabId(HOST_TAB_ID), LEAF_ID),
|
||||
getPtyId: () => 'pty-1',
|
||||
getSettings: () => null,
|
||||
inspectProcess: vi.fn(),
|
||||
dispatchCompletion,
|
||||
isLive: () => true
|
||||
})
|
||||
mocks.observeAgentHookCompletionForNotification.mockImplementation(({ payload }) =>
|
||||
coordinator.observeHookStatus(payload)
|
||||
)
|
||||
|
||||
applySnapshot(makeAgentSnapshot(1, NOW), false)
|
||||
applySnapshot(makeAgentSnapshot(2, NOW + 1_000, NOW + 1_000), true)
|
||||
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not seed a stamped reconnect replay', () => {
|
||||
applySnapshot(makeAgentSnapshot(1, NOW, NOW), false)
|
||||
|
||||
expect(mocks.observeAgentHookCompletionForNotification).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -57,6 +57,10 @@ vi.mock('../store', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/agent-hook-completion-notifications', () => ({
|
||||
observeAgentHookCompletionForNotification: vi.fn()
|
||||
}))
|
||||
|
||||
const WT = 'repo::/worktree'
|
||||
const ENV = 'web-env-1'
|
||||
const NOW = 1_700_000_000_000
|
||||
|
||||
@@ -3419,10 +3419,11 @@ function applyWebSessionTabsSnapshotOperations(
|
||||
|
||||
export function applyWebSessionTabsStorePatch(
|
||||
buildPatch: (state: AppState) => WebSessionTabsSyncState | Partial<WebSessionTabsSyncState>,
|
||||
liveSnapshot?: RuntimeMobileSessionTabsResult
|
||||
agentStatusSnapshots?: RuntimeMobileSessionTabsResult | readonly RuntimeMobileSessionTabsResult[],
|
||||
allowCompletionNotification = false
|
||||
): void {
|
||||
let mirroredAgentStatusChanged = false
|
||||
const acceptedLiveStatuses: {
|
||||
const acceptedNotificationStatuses: {
|
||||
paneKey: string
|
||||
worktreeId: string
|
||||
payload: ReturnType<typeof pickParsedAgentStatusPayload> & { stateStartedAt: number }
|
||||
@@ -3430,37 +3431,48 @@ export function applyWebSessionTabsStorePatch(
|
||||
useAppStore.setState((state) => {
|
||||
const patch = buildPatch(state)
|
||||
mirroredAgentStatusChanged = patch !== state && Object.hasOwn(patch, 'agentStatusByPaneKey')
|
||||
if (mirroredAgentStatusChanged && liveSnapshot) {
|
||||
if (mirroredAgentStatusChanged && agentStatusSnapshots) {
|
||||
const nextAgentStatuses = patch.agentStatusByPaneKey ?? state.agentStatusByPaneKey
|
||||
for (const surface of liveSnapshot.tabs) {
|
||||
if (surface.type !== 'terminal') {
|
||||
continue
|
||||
}
|
||||
const remapped = remapHostAgentStatus(surface)
|
||||
const accepted = remapped ? nextAgentStatuses[remapped.paneKey] : undefined
|
||||
if (
|
||||
!remapped ||
|
||||
!accepted ||
|
||||
accepted === state.agentStatusByPaneKey[remapped.paneKey] ||
|
||||
!agentStatusEntryEqual(accepted, remapped)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const turnCompletedAt = normalizeTurnCompletedAtField(
|
||||
surface.turnCompletedAt,
|
||||
accepted.state
|
||||
)
|
||||
acceptedLiveStatuses.push({
|
||||
paneKey: accepted.paneKey,
|
||||
worktreeId: accepted.worktreeId ?? liveSnapshot.worktree,
|
||||
payload: {
|
||||
...pickParsedAgentStatusPayload({
|
||||
...accepted,
|
||||
...(turnCompletedAt !== undefined ? { turnCompletedAt } : {})
|
||||
}),
|
||||
stateStartedAt: accepted.stateStartedAt
|
||||
const snapshots = Array.isArray(agentStatusSnapshots)
|
||||
? agentStatusSnapshots
|
||||
: [agentStatusSnapshots]
|
||||
for (const snapshot of snapshots) {
|
||||
for (const surface of snapshot.tabs) {
|
||||
if (surface.type !== 'terminal') {
|
||||
continue
|
||||
}
|
||||
})
|
||||
const remapped = remapHostAgentStatus(surface)
|
||||
const accepted = remapped ? nextAgentStatuses[remapped.paneKey] : undefined
|
||||
if (
|
||||
!remapped ||
|
||||
!accepted ||
|
||||
accepted === state.agentStatusByPaneKey[remapped.paneKey] ||
|
||||
!agentStatusEntryEqual(accepted, remapped)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const turnCompletedAt = normalizeTurnCompletedAtField(
|
||||
surface.turnCompletedAt,
|
||||
accepted.state
|
||||
)
|
||||
if (
|
||||
!allowCompletionNotification &&
|
||||
(accepted.state !== 'working' || turnCompletedAt !== undefined)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
acceptedNotificationStatuses.push({
|
||||
paneKey: accepted.paneKey,
|
||||
worktreeId: accepted.worktreeId ?? snapshot.worktree,
|
||||
payload: {
|
||||
...pickParsedAgentStatusPayload({
|
||||
...accepted,
|
||||
...(turnCompletedAt !== undefined ? { turnCompletedAt } : {})
|
||||
}),
|
||||
stateStartedAt: accepted.stateStartedAt
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return patch
|
||||
@@ -3469,7 +3481,7 @@ export function applyWebSessionTabsStorePatch(
|
||||
if (mirroredAgentStatusChanged) {
|
||||
useAppStore.getState().scheduleAgentStatusFreshness()
|
||||
}
|
||||
for (const status of acceptedLiveStatuses) {
|
||||
for (const status of acceptedNotificationStatuses) {
|
||||
observeAgentHookCompletionForNotification(status)
|
||||
}
|
||||
}
|
||||
@@ -3538,8 +3550,9 @@ function loadInitialWebSessionTabs(
|
||||
receivedFrames[index]!
|
||||
)
|
||||
)
|
||||
applyWebSessionTabsStorePatch((state) =>
|
||||
applyFreshWebSessionTabsSnapshots(state, applicable, environmentId)
|
||||
applyWebSessionTabsStorePatch(
|
||||
(state) => applyFreshWebSessionTabsSnapshots(state, applicable, environmentId),
|
||||
applicable
|
||||
)
|
||||
} finally {
|
||||
for (const finishRecovery of finishRecoveries) {
|
||||
@@ -3846,8 +3859,9 @@ export function useWebSessionTabsSync(): void {
|
||||
batch.deferredRepairWorktrees.delete(worktreeId)
|
||||
}
|
||||
if (operations.length > 0) {
|
||||
applyWebSessionTabsStorePatch((state) =>
|
||||
applyWebSessionTabsSnapshotOperations(state, operations)
|
||||
applyWebSessionTabsStorePatch(
|
||||
(state) => applyWebSessionTabsSnapshotOperations(state, operations),
|
||||
operations.map(({ snapshot }) => snapshot)
|
||||
)
|
||||
}
|
||||
finishVisibilityResumeBatchIfIdle(batch)
|
||||
@@ -4156,8 +4170,10 @@ export function useWebSessionTabsSync(): void {
|
||||
: []
|
||||
)
|
||||
if (freshSnapshots.length > 0) {
|
||||
applyWebSessionTabsStorePatch((state) =>
|
||||
applyWebSessionTabsSnapshots(state, freshSnapshots, environmentId)
|
||||
applyWebSessionTabsStorePatch(
|
||||
(state) =>
|
||||
applyWebSessionTabsSnapshots(state, freshSnapshots, environmentId),
|
||||
freshSnapshots
|
||||
)
|
||||
}
|
||||
const freshSnapshotSet = new Set(freshSnapshots)
|
||||
@@ -4228,7 +4244,8 @@ export function useWebSessionTabsSync(): void {
|
||||
if (shouldApplyWebSessionTabsSnapshot(recovered, environmentId)) {
|
||||
applyWebSessionTabsStorePatch(
|
||||
(state) => applyWebSessionTabsSnapshot(state, recovered, environmentId),
|
||||
event.type === 'updated' && !replayed ? recovered : undefined
|
||||
recovered,
|
||||
event.type === 'updated' && !replayed
|
||||
)
|
||||
recordVisibilityResumeSnapshot(environmentId, recovered, receivedFrame)
|
||||
}
|
||||
@@ -4353,7 +4370,8 @@ export function useWebSessionTabsSync(): void {
|
||||
const replayed = isRuntimeSubscriptionReplayResponse(response)
|
||||
applyWebSessionTabsStorePatch(
|
||||
(state) => applyWebSessionTabsSnapshot(state, recovered, environmentId),
|
||||
event.type === 'updated' && !replayed ? recovered : undefined
|
||||
recovered,
|
||||
event.type === 'updated' && !replayed
|
||||
)
|
||||
recordVisibilityResumeSnapshotRef.current(environmentId, recovered, receivedFrame)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user