mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(notifications): preserve recovered attention until interaction
This commit is contained in:
@@ -4845,7 +4845,7 @@
|
||||
"pnpm exec playwright test tests/e2e/paired-remote-agent-visibility-notifications.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/terminal-orphan-owner.test.ts src/main/runtime/terminal-orphan-topology.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts --maxWorkers=1",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler-spawn-admission.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/runtime/orca-runtime.test.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-session-tabs-visibility-resume-notifications.test.tsx src/renderer/src/runtime/web-session-tabs-notification-receipt-order.test.tsx src/renderer/src/runtime/web-session-tabs-notification-reconciler.test.ts src/renderer/src/hooks/agent-hook-completion-notifications-inventory.test.ts src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts src/main/ipc/notifications-delivery-gating.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-session-tabs-visibility-resume-notifications.test.tsx src/renderer/src/runtime/web-session-tabs-notification-receipt-order.test.tsx src/renderer/src/runtime/web-session-tabs-notification-reconciler.test.ts src/renderer/src/hooks/agent-hook-completion-notifications-inventory.test.ts src/renderer/src/hooks/useAutoAckViewedAgent.attention.test.tsx src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts src/main/ipc/notifications-delivery-gating.test.ts",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler-spawn-admission.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts src/renderer/src/lib/codex-session-restart.test.ts"
|
||||
],
|
||||
"testFiles": [
|
||||
@@ -4861,6 +4861,7 @@
|
||||
"src/renderer/src/runtime/web-session-tabs-notification-receipt-order.test.tsx",
|
||||
"src/renderer/src/runtime/web-session-tabs-notification-reconciler.test.ts",
|
||||
"src/renderer/src/hooks/agent-hook-completion-notifications-inventory.test.ts",
|
||||
"src/renderer/src/hooks/useAutoAckViewedAgent.attention.test.tsx",
|
||||
"src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts",
|
||||
"src/main/ipc/notifications-delivery-gating.test.ts",
|
||||
"src/renderer/src/runtime/runtime-terminal-inspection.test.ts",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { cleanup, renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { makePaneKey } from '../../../shared/stable-pane-id'
|
||||
import { useAppStore } from '../store'
|
||||
import { makeTab } from '../store/slices/store-test-helpers'
|
||||
import { useAutoAckViewedAgent } from './useAutoAckViewedAgent'
|
||||
|
||||
const WORKTREE_ID = 'wt-attention'
|
||||
const TAB_ID = 'tab-attention'
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
const PANE_KEY = makePaneKey(TAB_ID, LEAF_ID)
|
||||
|
||||
describe('useAutoAckViewedAgent recovered attention', () => {
|
||||
const acknowledgeAgents = vi.fn()
|
||||
const clearWorktreeUnread = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
|
||||
useAppStore.setState({
|
||||
activeView: 'terminal',
|
||||
activeTabId: TAB_ID,
|
||||
activeWorktreeId: WORKTREE_ID,
|
||||
tabsByWorktree: { [WORKTREE_ID]: [makeTab({ id: TAB_ID, worktreeId: WORKTREE_ID })] },
|
||||
terminalLayoutsByTabId: {
|
||||
[TAB_ID]: { root: null, activeLeafId: LEAF_ID, expandedLeafId: null }
|
||||
},
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {},
|
||||
acknowledgedAgentsByPaneKey: {},
|
||||
unreadAgentCompletionPanes: {},
|
||||
unreadTerminalTabs: {},
|
||||
acknowledgeAgents,
|
||||
clearWorktreeUnread
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.restoreAllMocks()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('acks the visible row without clearing unread that has no completion-source marker', () => {
|
||||
renderHook(() => useAutoAckViewedAgent(false))
|
||||
|
||||
useAppStore.getState().setAgentStatus(PANE_KEY, {
|
||||
state: 'waiting',
|
||||
prompt: 'Approve?',
|
||||
agentType: 'claude'
|
||||
})
|
||||
|
||||
expect(acknowledgeAgents).toHaveBeenCalledWith([PANE_KEY])
|
||||
expect(clearWorktreeUnread).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -258,13 +258,16 @@ export function useAutoAckViewedAgent(floatingPanelVisible: boolean): void {
|
||||
}
|
||||
const worktreeId = target.worktreeId
|
||||
acknowledgeViewedAgentAttention(current, {
|
||||
activeWorktreeId: shouldClearViewedAgentWorktreeUnread(current, {
|
||||
activeWorktreeId: worktreeId,
|
||||
activeTabId: tabId,
|
||||
paneKeysToClear
|
||||
})
|
||||
? worktreeId
|
||||
: null,
|
||||
// Why: row acknowledgement does not own coarse unread; only the completion-source marker may auto-clear it.
|
||||
activeWorktreeId:
|
||||
activePaneKey &&
|
||||
shouldClearViewedAgentWorktreeUnread(current, {
|
||||
activeWorktreeId: worktreeId,
|
||||
activeTabId: tabId,
|
||||
paneKeysToClear
|
||||
})
|
||||
? worktreeId
|
||||
: null,
|
||||
activeTabId: tabId,
|
||||
paneKeys: toAck,
|
||||
activePaneKey
|
||||
|
||||
@@ -23,7 +23,7 @@ export type WebSessionTabsNotificationReconciler = {
|
||||
observeSnapshot: (
|
||||
snapshot: RuntimeMobileSessionTabsResult,
|
||||
options?: { attentionRequired?: boolean }
|
||||
) => void
|
||||
) => boolean
|
||||
observeInventory: (
|
||||
snapshots: readonly RuntimeMobileSessionTabsResult[],
|
||||
options: { armPublished: boolean; attentionRequired?: boolean }
|
||||
@@ -62,14 +62,14 @@ export function createWebSessionTabsNotificationReconciler(args: {
|
||||
const observeAcceptedSnapshot = (
|
||||
snapshot: RuntimeMobileSessionTabsResult,
|
||||
attentionRequired: boolean
|
||||
): void => {
|
||||
): boolean => {
|
||||
const current = worktrees.get(snapshot.worktree)
|
||||
if (isRemoval(snapshot)) {
|
||||
worktrees.delete(snapshot.worktree)
|
||||
return
|
||||
return current !== undefined
|
||||
}
|
||||
if (current && !advancesFreshness(snapshot, current)) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
const eligible = current?.eligible === true
|
||||
worktrees.set(snapshot.worktree, {
|
||||
@@ -81,15 +81,17 @@ export function createWebSessionTabsNotificationReconciler(args: {
|
||||
seedOnly: !eligible,
|
||||
attentionRequired: eligible && attentionRequired
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
observeSnapshot: (snapshot, options) => {
|
||||
observeAcceptedSnapshot(snapshot, options?.attentionRequired === true)
|
||||
const accepted = observeAcceptedSnapshot(snapshot, options?.attentionRequired === true)
|
||||
const state = worktrees.get(snapshot.worktree)
|
||||
if (state) {
|
||||
state.eligible = true
|
||||
}
|
||||
return accepted
|
||||
},
|
||||
observeInventory: (snapshots, options) => {
|
||||
const publishedWorktrees = new Set<string>()
|
||||
|
||||
@@ -361,13 +361,21 @@ function untrackWebSessionTabsWorktree(environmentId: string, worktreeId: string
|
||||
export function recordReceivedWebSessionTabsSnapshot(
|
||||
environmentId: string,
|
||||
snapshot: RuntimeMobileSessionTabsResult,
|
||||
options: { observeNotification?: boolean; attentionRequired?: boolean } = {}
|
||||
options: {
|
||||
observeNotification?: boolean
|
||||
attentionRequired?: boolean
|
||||
onNotificationAccepted?: () => void
|
||||
} = {}
|
||||
): number {
|
||||
const receivedFrame = (receivedSessionTabsFrameSequence += 1)
|
||||
if (options.observeNotification !== false) {
|
||||
getWebSessionTabsNotificationLifecycle(environmentId).observeSnapshot(snapshot, {
|
||||
attentionRequired: options.attentionRequired
|
||||
})
|
||||
const accepted = getWebSessionTabsNotificationLifecycle(environmentId).observeSnapshot(
|
||||
snapshot,
|
||||
{ attentionRequired: options.attentionRequired }
|
||||
)
|
||||
if (accepted) {
|
||||
options.onNotificationAccepted?.()
|
||||
}
|
||||
}
|
||||
const key = sessionTabsFreshnessKey(environmentId, snapshot.worktree)
|
||||
latestReceivedSessionTabsSnapshotByWorktree.set(key, {
|
||||
@@ -5046,9 +5054,11 @@ export function useWebSessionTabsSync(): void {
|
||||
return
|
||||
}
|
||||
const attentionRequired = awaitingVisibilityResumeSnapshot
|
||||
awaitingVisibilityResumeSnapshot = false
|
||||
const receivedFrame = recordReceivedWebSessionTabsSnapshot(environmentId, event, {
|
||||
attentionRequired
|
||||
attentionRequired,
|
||||
onNotificationAccepted: () => {
|
||||
awaitingVisibilityResumeSnapshot = false
|
||||
}
|
||||
})
|
||||
recordVisibilityResumeSnapshotReceiptRef.current(
|
||||
environmentId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { RuntimeMobileSessionTabsResult } from '../../src/shared/runtime-ty
|
||||
import { makePaneKey } from '../../src/shared/stable-pane-id'
|
||||
import { toWebTerminalSurfaceTabId } from '../../src/shared/terminal-surface-id'
|
||||
import { WINDOW_VISIBILITY_SUBSCRIPTION_PARK_DELAY_MS } from '../../src/renderer/src/runtime/window-visibility-subscription-parking'
|
||||
import { WINDOW_VISIBILITY_SUBSCRIPTION_PARK_DELAY_BACKOFF_LIMIT } from '../../src/renderer/src/runtime/window-visibility-subscription-parking'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { readHookEndpoint } from './helpers/agent-hook-endpoint'
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ import { revealPairedClientWindow } from './helpers/paired-client-window-reveal'
|
||||
|
||||
const REQUIRED_CLAUDE_EVENTS = ['UserPromptSubmit', 'Stop', 'PermissionRequest'] as const
|
||||
const NOTIFICATION_BARRIER_SOURCE = 'sta-5244-barrier'
|
||||
let completedHideCycles = 0
|
||||
type NotificationDispatch = {
|
||||
source?: string
|
||||
agentState?: string
|
||||
@@ -200,7 +202,11 @@ async function hideUntilSubscriptionsPark(client: PairedElectronClient): Promise
|
||||
})
|
||||
document.dispatchEvent(new Event('visibilitychange'))
|
||||
})
|
||||
await client.page.waitForTimeout(WINDOW_VISIBILITY_SUBSCRIPTION_PARK_DELAY_MS + 100)
|
||||
const parkDelay =
|
||||
WINDOW_VISIBILITY_SUBSCRIPTION_PARK_DELAY_MS *
|
||||
Math.min(2 ** completedHideCycles, WINDOW_VISIBILITY_SUBSCRIPTION_PARK_DELAY_BACKOFF_LIMIT)
|
||||
await client.page.waitForTimeout(parkDelay + 100)
|
||||
completedHideCycles += 1
|
||||
}
|
||||
|
||||
async function revealAfterSubscriptionsPark(client: PairedElectronClient): Promise<void> {
|
||||
@@ -320,6 +326,7 @@ test('recovers hidden remote completion and permission alerts exactly once', asy
|
||||
let coldClient: PairedElectronClient | null = null
|
||||
let terminal: string | null = null
|
||||
let testError: unknown
|
||||
completedHideCycles = 0
|
||||
|
||||
try {
|
||||
await host.client.call('repo.add', { path: testRepoPath, kind: 'git' })
|
||||
|
||||
Reference in New Issue
Block a user