mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(attention): rescan when focusing a split group
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, cleanup, renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as AgentAutoAckPresence from './agent-auto-ack-presence'
|
||||
import { useAutoAckViewedAgent } from './useAutoAckViewedAgent'
|
||||
import { useAppStore } from '../store'
|
||||
import { makeTab, makeTabGroup, makeUnifiedTab } from '../store/slices/store-test-helpers'
|
||||
import { structuredAgentSessionPaneKey } from '../../../shared/structured-agent-session-projection'
|
||||
|
||||
vi.mock('./agent-auto-ack-presence', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof AgentAutoAckPresence>()),
|
||||
createAutoAckPresenceCheck: (_read: unknown, onPresent: () => void) => ({
|
||||
request: onPresent,
|
||||
dispose() {}
|
||||
})
|
||||
}))
|
||||
|
||||
const WORKSPACE = 'focus-group-workspace'
|
||||
const TERMINAL_GROUP = 'terminal-group'
|
||||
const CHAT_GROUP = 'chat-group'
|
||||
const TERMINAL_TAB = 'terminal-tab'
|
||||
const CHAT_TAB = 'chat-tab'
|
||||
const CHAT_SESSION = 'chat-session'
|
||||
const CHAT_SUBJECT = structuredAgentSessionPaneKey(CHAT_TAB, CHAT_SESSION)
|
||||
|
||||
describe('useAutoAckViewedAgent focus-group transitions', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
|
||||
useAppStore.setState({
|
||||
activeView: 'terminal',
|
||||
activeWorktreeId: WORKSPACE,
|
||||
activeTabId: TERMINAL_TAB,
|
||||
activeTabIdByWorktree: { [WORKSPACE]: TERMINAL_TAB },
|
||||
activeGroupIdByWorktree: { [WORKSPACE]: TERMINAL_GROUP },
|
||||
activeTabType: 'terminal',
|
||||
tabsByWorktree: { [WORKSPACE]: [makeTab({ id: TERMINAL_TAB, worktreeId: WORKSPACE })] },
|
||||
unifiedTabsByWorktree: {
|
||||
[WORKSPACE]: [
|
||||
makeUnifiedTab({
|
||||
id: TERMINAL_TAB,
|
||||
worktreeId: WORKSPACE,
|
||||
groupId: TERMINAL_GROUP
|
||||
}),
|
||||
makeUnifiedTab({
|
||||
id: CHAT_TAB,
|
||||
worktreeId: WORKSPACE,
|
||||
groupId: CHAT_GROUP,
|
||||
contentType: 'agent-session',
|
||||
entityId: CHAT_SESSION,
|
||||
agentSessionAgent: 'claude'
|
||||
})
|
||||
]
|
||||
},
|
||||
groupsByWorktree: {
|
||||
[WORKSPACE]: [
|
||||
makeTabGroup({
|
||||
id: TERMINAL_GROUP,
|
||||
worktreeId: WORKSPACE,
|
||||
activeTabId: TERMINAL_TAB,
|
||||
tabOrder: [TERMINAL_TAB]
|
||||
}),
|
||||
makeTabGroup({
|
||||
id: CHAT_GROUP,
|
||||
worktreeId: WORKSPACE,
|
||||
activeTabId: CHAT_TAB,
|
||||
tabOrder: [CHAT_TAB]
|
||||
})
|
||||
]
|
||||
},
|
||||
unreadAgentCompletionPanes: { [CHAT_SUBJECT]: 'agent-completion' },
|
||||
unreadTerminalTabs: {},
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {},
|
||||
acknowledgedAgentsByPaneKey: {}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('rescans when focusing a group whose structured tab becomes visible', () => {
|
||||
renderHook(() => useAutoAckViewedAgent(false))
|
||||
expect(useAppStore.getState().unreadAgentCompletionPanes[CHAT_SUBJECT]).toBe('agent-completion')
|
||||
|
||||
act(() => {
|
||||
useAppStore.getState().focusGroup(WORKSPACE, CHAT_GROUP)
|
||||
})
|
||||
|
||||
expect(useAppStore.getState().activeGroupIdByWorktree[WORKSPACE]).toBe(CHAT_GROUP)
|
||||
expect(useAppStore.getState().unreadAgentCompletionPanes[CHAT_SUBJECT]).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -32,14 +32,16 @@ export function useAutoAckViewedAgent(floatingPanelVisible: boolean): void {
|
||||
// Init to undefined so the first maybeAck() (on mount) always passes the ref guard and scans.
|
||||
let lastActiveView: unknown = undefined
|
||||
let lastActiveTabId: unknown = undefined
|
||||
let lastActiveWorktreeId: unknown = undefined
|
||||
let lastActiveWorkspaceGroupId: unknown = undefined
|
||||
let lastActiveWorkspaceGroups: unknown = undefined
|
||||
let lastFloatingWorkspaceActiveTabId: unknown = undefined
|
||||
let lastFloatingWorkspaceGroupId: unknown = undefined
|
||||
let lastFloatingWorkspaceGroups: unknown = undefined
|
||||
let lastAgentStatus: unknown = undefined
|
||||
let lastRetained: unknown = undefined
|
||||
let lastAcknowledged: unknown = undefined
|
||||
let lastLayouts: unknown = undefined
|
||||
// Why: activating a structured chat only moves its layout group's activeTabId — no terminal
|
||||
// slice changes, so without this the scan that clears its attention would be skipped.
|
||||
let lastGroups: unknown = undefined
|
||||
let lastUnreadAgentCompletionPanes: unknown = undefined
|
||||
|
||||
// `force` re-scans after a signal the store never sees: panel open/closed is React-local state.
|
||||
@@ -49,18 +51,32 @@ export function useAutoAckViewedAgent(floatingPanelVisible: boolean): void {
|
||||
)
|
||||
const maybeAck = (options?: { force?: boolean; presenceConfirmed?: boolean }): void => {
|
||||
const s = useAppStore.getState()
|
||||
const activeWorktreeId = s.activeWorktreeId
|
||||
const activeWorkspaceGroupId = activeWorktreeId
|
||||
? (s.activeGroupIdByWorktree[activeWorktreeId] ?? null)
|
||||
: null
|
||||
const activeWorkspaceGroups = activeWorktreeId
|
||||
? s.groupsByWorktree[activeWorktreeId]
|
||||
: undefined
|
||||
const floatingWorkspaceActiveTabId =
|
||||
s.activeTabIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? null
|
||||
const floatingWorkspaceGroupId =
|
||||
s.activeGroupIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? null
|
||||
const floatingWorkspaceGroups = s.groupsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]
|
||||
if (
|
||||
!options?.force &&
|
||||
s.activeView === lastActiveView &&
|
||||
s.activeTabId === lastActiveTabId &&
|
||||
activeWorktreeId === lastActiveWorktreeId &&
|
||||
activeWorkspaceGroupId === lastActiveWorkspaceGroupId &&
|
||||
activeWorkspaceGroups === lastActiveWorkspaceGroups &&
|
||||
floatingWorkspaceActiveTabId === lastFloatingWorkspaceActiveTabId &&
|
||||
floatingWorkspaceGroupId === lastFloatingWorkspaceGroupId &&
|
||||
floatingWorkspaceGroups === lastFloatingWorkspaceGroups &&
|
||||
s.agentStatusByPaneKey === lastAgentStatus &&
|
||||
s.retainedAgentsByPaneKey === lastRetained &&
|
||||
s.acknowledgedAgentsByPaneKey === lastAcknowledged &&
|
||||
s.terminalLayoutsByTabId === lastLayouts &&
|
||||
s.groupsByWorktree === lastGroups &&
|
||||
s.unreadAgentCompletionPanes === lastUnreadAgentCompletionPanes
|
||||
) {
|
||||
return
|
||||
@@ -69,12 +85,16 @@ export function useAutoAckViewedAgent(floatingPanelVisible: boolean): void {
|
||||
// Presence signals force a rescan; unrelated writes must not retry an away result.
|
||||
lastActiveView = s.activeView
|
||||
lastActiveTabId = s.activeTabId
|
||||
lastActiveWorktreeId = activeWorktreeId
|
||||
lastActiveWorkspaceGroupId = activeWorkspaceGroupId
|
||||
lastActiveWorkspaceGroups = activeWorkspaceGroups
|
||||
lastFloatingWorkspaceActiveTabId = floatingWorkspaceActiveTabId
|
||||
lastFloatingWorkspaceGroupId = floatingWorkspaceGroupId
|
||||
lastFloatingWorkspaceGroups = floatingWorkspaceGroups
|
||||
lastAgentStatus = s.agentStatusByPaneKey
|
||||
lastRetained = s.retainedAgentsByPaneKey
|
||||
lastAcknowledged = s.acknowledgedAgentsByPaneKey
|
||||
lastLayouts = s.terminalLayoutsByTabId
|
||||
lastGroups = s.groupsByWorktree
|
||||
lastUnreadAgentCompletionPanes = s.unreadAgentCompletionPanes
|
||||
|
||||
// Why: tab-active only proxies "seen"; gate on window visible+focused so away-time transitions don't silently clear the bold signal.
|
||||
|
||||
Reference in New Issue
Block a user