mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
perf: group Space decision inputs by workspace (#26297)
This commit is contained in:
+304
@@ -0,0 +1,304 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, cleanup, renderHook } from '@testing-library/react'
|
||||
import { useStore } from 'zustand'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '../../store/types'
|
||||
import {
|
||||
createTestStore,
|
||||
makeOpenFile,
|
||||
makeTab,
|
||||
makeWorktree
|
||||
} from '../../store/slices/store-test-helpers'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
import type { WorkspaceSpaceWorktree } from '../../../../shared/workspace-space-types'
|
||||
import { resetAgentStatusEpochClockForTests } from '@/lib/agent-status-epoch-clock'
|
||||
import { useWorkspaceSpaceManagerBindings } from './use-workspace-space-manager-bindings'
|
||||
import { useWorkspaceSpaceDecisionProjection } from './use-workspace-space-decision-projection'
|
||||
|
||||
let store: ReturnType<typeof createTestStore>
|
||||
vi.mock('../../store', () => ({
|
||||
useAppStore: Object.assign(
|
||||
<Result,>(selector: (state: AppState) => Result) => useStore(store, selector),
|
||||
{ getState: () => store.getState() }
|
||||
)
|
||||
}))
|
||||
|
||||
function row(
|
||||
worktreeId: string,
|
||||
executionHostId: 'local' | 'ssh:host' = 'local'
|
||||
): WorkspaceSpaceWorktree {
|
||||
return {
|
||||
worktreeId,
|
||||
executionHostId,
|
||||
repoId: 'repo',
|
||||
repoDisplayName: 'Repo',
|
||||
repoPath: '/repo',
|
||||
displayName: worktreeId,
|
||||
path: `/repo/${worktreeId}`,
|
||||
branch: 'main',
|
||||
isMainWorktree: false,
|
||||
isRemote: executionHostId !== 'local',
|
||||
isSparse: false,
|
||||
canDelete: true,
|
||||
lastActivityAt: 0,
|
||||
status: 'ok',
|
||||
error: null,
|
||||
scannedAt: 0,
|
||||
sizeBytes: 0,
|
||||
reclaimableBytes: 0,
|
||||
skippedEntryCount: 0,
|
||||
topLevelItems: [],
|
||||
omittedTopLevelItemCount: 0,
|
||||
omittedTopLevelSizeBytes: 0
|
||||
}
|
||||
}
|
||||
|
||||
function entry(paneKey: string, overrides: Partial<AgentStatusEntry> = {}): AgentStatusEntry {
|
||||
return {
|
||||
paneKey,
|
||||
state: 'working',
|
||||
prompt: '',
|
||||
updatedAt: Date.now(),
|
||||
stateStartedAt: Date.now(),
|
||||
stateHistory: [],
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function seed(rows: WorkspaceSpaceWorktree[]) {
|
||||
store.setState({
|
||||
worktreesByRepo: { repo: rows.map((r) => makeWorktree({ id: r.worktreeId, repoId: 'repo' })) },
|
||||
tabsByWorktree: Object.fromEntries(
|
||||
rows.map((r) => [
|
||||
r.worktreeId,
|
||||
[makeTab({ id: `${r.worktreeId}-tab`, worktreeId: r.worktreeId })]
|
||||
])
|
||||
),
|
||||
workspaceSpaceAnalysis: {
|
||||
scannedAt: 1,
|
||||
worktrees: rows,
|
||||
repos: [],
|
||||
totalSizeBytes: 0,
|
||||
reclaimableBytes: 0,
|
||||
worktreeCount: rows.length,
|
||||
scannedWorktreeCount: rows.length,
|
||||
unavailableWorktreeCount: 0
|
||||
},
|
||||
refreshGitHubForWorktreeIfStale: vi.fn(async () => {})
|
||||
})
|
||||
}
|
||||
|
||||
function mountProjection() {
|
||||
return renderHook(() => useWorkspaceSpaceDecisionProjection(useWorkspaceSpaceManagerBindings()))
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(1_000_000)
|
||||
store = createTestStore()
|
||||
resetAgentStatusEpochClockForTests()
|
||||
})
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.clearAllTimers()
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
resetAgentStatusEpochClockForTests()
|
||||
})
|
||||
|
||||
describe('mounted Space decision projection', () => {
|
||||
it('reads each owned agent instead of every global record on each title publication', () => {
|
||||
seed(Array.from({ length: 20 }, (_, i) => row(`w${i}`)))
|
||||
let liveProbes = 0,
|
||||
retainedProbes = 0
|
||||
const live: AppState['agentStatusByPaneKey'] = {}
|
||||
const retained: AppState['retainedAgentsByPaneKey'] = {}
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
const paneKey = `w${i}-tab:legacy`
|
||||
live[paneKey] = {
|
||||
...entry(paneKey),
|
||||
get state(): AgentStatusEntry['state'] {
|
||||
liveProbes += 1
|
||||
return 'working'
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < 40; i += 1) {
|
||||
const worktreeId = `w${i % 20}`
|
||||
retained[`completed-${i}`] = {
|
||||
entry: entry(`completed-${i}:legacy`, { state: 'done' }),
|
||||
get worktreeId() {
|
||||
retainedProbes += 1
|
||||
return worktreeId
|
||||
},
|
||||
tab: makeTab({ id: `completed-${i}`, worktreeId }),
|
||||
agentType: 'claude',
|
||||
startedAt: 0
|
||||
}
|
||||
}
|
||||
store.setState({ agentStatusByPaneKey: live, retainedAgentsByPaneKey: retained })
|
||||
const view = mountProjection()
|
||||
liveProbes = 0
|
||||
retainedProbes = 0
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
act(() => store.getState().setRuntimePaneTitle('w0-tab', 0, `Ordinary command ${i}`))
|
||||
}
|
||||
expect(view.result.current.decisionDetailsByWorktreeId.size).toBe(20)
|
||||
expect({ liveProbes, retainedProbes }).toEqual({ liveProbes: 50, retainedProbes: 200 })
|
||||
})
|
||||
|
||||
it('updates from real status, retention, draft, tab and source-row changes', () => {
|
||||
seed([row('w0'), row('w1')])
|
||||
const view = mountProjection()
|
||||
const details = () => view.result.current.decisionDetailsByWorktreeId.get('local|w0')
|
||||
act(() =>
|
||||
store.getState().setAgentStatus('w0-tab:legacy', {
|
||||
agentType: 'custom-non-icon-agent',
|
||||
state: 'working',
|
||||
prompt: 'Generated task'
|
||||
})
|
||||
)
|
||||
expect(details()?.activeAgentCount).toBe(1)
|
||||
act(() =>
|
||||
store.getState().setAgentStatus('w0-tab:legacy', {
|
||||
agentType: 'custom-non-icon-agent',
|
||||
state: 'done',
|
||||
prompt: 'Generated task'
|
||||
})
|
||||
)
|
||||
expect(details()?.activeAgentCount).toBe(0)
|
||||
act(() =>
|
||||
store.getState().retainAgents([
|
||||
{
|
||||
entry: entry('completed:legacy', { state: 'done' }),
|
||||
worktreeId: 'w0',
|
||||
tab: makeTab({ id: 'completed', worktreeId: 'w0' }),
|
||||
agentType: 'claude',
|
||||
startedAt: 0
|
||||
}
|
||||
])
|
||||
)
|
||||
expect(details()?.completedAgentCount).toBe(1)
|
||||
act(() => store.setState({ openFiles: [makeOpenFile({ id: 'file', worktreeId: 'w0' })] }))
|
||||
act(() => store.getState().setEditorDraft('file', 'Changed content'))
|
||||
expect(details()?.dirtyEditorBufferCount).toBe(1)
|
||||
act(() => store.setState({ tabsByWorktree: { w0: [] } }))
|
||||
expect(details()?.terminalTabCount).toBe(0)
|
||||
act(() => store.setState({ workspaceSpaceAnalysis: null }))
|
||||
expect(view.result.current.decisionDetailsByWorktreeId.size).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps special own keys and migration workspace-or-tab ownership across host collisions', () => {
|
||||
seed([row('w0'), row('w0', 'ssh:host'), row('w1')])
|
||||
store.setState({
|
||||
tabsByWorktree: {
|
||||
w0: [makeTab({ id: 'shared', worktreeId: 'w0' })],
|
||||
w1: [makeTab({ id: 'shared', worktreeId: 'w0' })]
|
||||
},
|
||||
agentStatusByPaneKey: Object.fromEntries([
|
||||
['__proto__', entry('shared:legacy', { agentType: 'custom-non-icon-agent' })],
|
||||
['constructor', entry('shared:legacy')],
|
||||
['2', entry('wrong:legacy')],
|
||||
['shared:legacy', entry('other:legacy')]
|
||||
]),
|
||||
migrationUnsupportedByPtyId: Object.fromEntries<
|
||||
AppState['migrationUnsupportedByPtyId'][string]
|
||||
>([
|
||||
[
|
||||
'__proto__',
|
||||
{
|
||||
ptyId: 'm1',
|
||||
worktreeId: 'w0',
|
||||
tabId: 'shared',
|
||||
source: 'local',
|
||||
reason: 'legacy-numeric-pane-key',
|
||||
updatedAt: 1
|
||||
}
|
||||
],
|
||||
[
|
||||
'missing-owner',
|
||||
{
|
||||
ptyId: 'm3',
|
||||
tabId: 'shared',
|
||||
source: 'local',
|
||||
reason: 'legacy-numeric-pane-key',
|
||||
updatedAt: 1
|
||||
}
|
||||
],
|
||||
[
|
||||
'constructor',
|
||||
{
|
||||
ptyId: 'm2',
|
||||
worktreeId: 'w0',
|
||||
tabId: '',
|
||||
paneKey: 'shared:legacy',
|
||||
source: 'local',
|
||||
reason: 'legacy-numeric-pane-key',
|
||||
updatedAt: 1
|
||||
}
|
||||
]
|
||||
]),
|
||||
retainedAgentsByPaneKey: Object.fromEntries([
|
||||
[
|
||||
'__proto__',
|
||||
{
|
||||
entry: entry('done:legacy', { state: 'done' }),
|
||||
worktreeId: 'w0',
|
||||
tab: makeTab({ id: 'done', worktreeId: 'w0' }),
|
||||
agentType: 'claude',
|
||||
startedAt: 0
|
||||
}
|
||||
],
|
||||
[
|
||||
'constructor',
|
||||
{
|
||||
entry: entry('working:legacy'),
|
||||
worktreeId: 'w0',
|
||||
tab: makeTab({ id: 'working', worktreeId: 'w0' }),
|
||||
agentType: 'claude',
|
||||
startedAt: 0
|
||||
}
|
||||
]
|
||||
])
|
||||
})
|
||||
const view = mountProjection()
|
||||
const results = view.result.current.decisionDetailsByWorktreeId
|
||||
expect(results.get('local|w0')).toMatchObject({ activeAgentCount: 5, completedAgentCount: 1 })
|
||||
expect(results.get('ssh:host|w0')).toMatchObject({
|
||||
activeAgentCount: 5,
|
||||
completedAgentCount: 1
|
||||
})
|
||||
expect(results.get('local|w1')).toMatchObject({ activeAgentCount: 4, completedAgentCount: 0 })
|
||||
})
|
||||
|
||||
it('does not enumerate global records when there are no scan rows', () => {
|
||||
seed([])
|
||||
const trap = {
|
||||
ownKeys() {
|
||||
throw new Error('empty analysis enumerated global agent records')
|
||||
}
|
||||
}
|
||||
store.setState({
|
||||
agentStatusByPaneKey: new Proxy<AppState['agentStatusByPaneKey']>({}, trap),
|
||||
migrationUnsupportedByPtyId: new Proxy<AppState['migrationUnsupportedByPtyId']>({}, trap),
|
||||
retainedAgentsByPaneKey: new Proxy<AppState['retainedAgentsByPaneKey']>({}, trap)
|
||||
})
|
||||
expect(mountProjection().result.current.decisionDetailsByWorktreeId.size).toBe(0)
|
||||
})
|
||||
|
||||
it('samples freshness on epoch changes without reusing a stale verdict', () => {
|
||||
seed([row('w0')])
|
||||
store.setState({ agentStatusByPaneKey: { 'w0-tab:legacy': entry('w0-tab:legacy') } })
|
||||
const view = mountProjection()
|
||||
expect(view.result.current.decisionDetailsByWorktreeId.get('local|w0')?.activeAgentCount).toBe(
|
||||
1
|
||||
)
|
||||
act(() => {
|
||||
vi.setSystemTime(3_000_000)
|
||||
store.setState({ agentStatusEpoch: store.getState().agentStatusEpoch + 1 })
|
||||
})
|
||||
expect(view.result.current.decisionDetailsByWorktreeId.get('local|w0')?.activeAgentCount).toBe(
|
||||
0
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { groupWorkspaceSpaceDecisionInputs } from './workspace-space-decision-inputs'
|
||||
import { getAgentStatusEpochNow } from '@/lib/agent-status-epoch-clock'
|
||||
import {
|
||||
getWorkspaceDecisionDetails,
|
||||
@@ -47,6 +48,23 @@ export function useWorkspaceSpaceDecisionProjection(bindings: WorkspaceSpaceMana
|
||||
}
|
||||
return counts
|
||||
}, [sourceRows])
|
||||
const groupedInputs = useMemo(
|
||||
() =>
|
||||
groupWorkspaceSpaceDecisionInputs(sourceRows, tabsByWorktree, {
|
||||
agentStatusByPaneKey,
|
||||
migrationUnsupportedByPtyId,
|
||||
retainedAgentsByPaneKey,
|
||||
openFiles
|
||||
}),
|
||||
[
|
||||
sourceRows,
|
||||
tabsByWorktree,
|
||||
agentStatusByPaneKey,
|
||||
migrationUnsupportedByPtyId,
|
||||
retainedAgentsByPaneKey,
|
||||
openFiles
|
||||
]
|
||||
)
|
||||
const decisionDetailsByWorktreeId = useMemo(() => {
|
||||
// Why: the epoch bumps when fresh hook entries cross the stale boundary so
|
||||
// delete readiness recomputes with the same wall-clock sample as the store.
|
||||
@@ -77,12 +95,14 @@ export function useWorkspaceSpaceDecisionProjection(bindings: WorkspaceSpaceMana
|
||||
settings,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
...groupedInputs.get(worktree.worktreeId),
|
||||
now: agentStatusNow
|
||||
})
|
||||
)
|
||||
}
|
||||
return details
|
||||
}, [
|
||||
groupedInputs,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
agentStatusEpoch,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
|
||||
import type { WorkspaceSpaceWorktree } from '../../../../shared/workspace-space-types'
|
||||
import type { WorkspaceDecisionInputs } from './workspace-space-decision-details'
|
||||
import { getPaneKeyTabId } from './workspace-space-presentation'
|
||||
|
||||
type GroupedInputs = Pick<
|
||||
WorkspaceDecisionInputs,
|
||||
'agentStatusByPaneKey' | 'migrationUnsupportedByPtyId' | 'retainedAgentsByPaneKey' | 'openFiles'
|
||||
>
|
||||
|
||||
type Bucket = {
|
||||
agentStatuses: [string, WorkspaceDecisionInputs['agentStatusByPaneKey'][string]][]
|
||||
migrations: [string, WorkspaceDecisionInputs['migrationUnsupportedByPtyId'][string]][]
|
||||
retainedAgents: [string, WorkspaceDecisionInputs['retainedAgentsByPaneKey'][string]][]
|
||||
openFiles: WorkspaceDecisionInputs['openFiles'][number][]
|
||||
}
|
||||
|
||||
export function groupWorkspaceSpaceDecisionInputs(
|
||||
rows: readonly Pick<WorkspaceSpaceWorktree, 'worktreeId'>[],
|
||||
tabsByWorktree: Record<string, readonly TerminalTab[]>,
|
||||
inputs: GroupedInputs
|
||||
): Map<string, GroupedInputs> {
|
||||
const result = new Map<string, GroupedInputs>()
|
||||
if (rows.length === 0) {
|
||||
return result
|
||||
}
|
||||
const buckets = new Map<string, Bucket>()
|
||||
const tabOwners = new Map<string, Set<string>>()
|
||||
for (const row of rows) {
|
||||
if (buckets.has(row.worktreeId)) {
|
||||
continue
|
||||
}
|
||||
buckets.set(row.worktreeId, {
|
||||
agentStatuses: [],
|
||||
migrations: [],
|
||||
retainedAgents: [],
|
||||
openFiles: []
|
||||
})
|
||||
for (const tab of tabsByWorktree[row.worktreeId] ?? []) {
|
||||
let owners = tabOwners.get(tab.id)
|
||||
if (!owners) {
|
||||
owners = new Set()
|
||||
tabOwners.set(tab.id, owners)
|
||||
}
|
||||
owners.add(row.worktreeId)
|
||||
}
|
||||
}
|
||||
for (const [key, entry] of Object.entries(inputs.agentStatusByPaneKey)) {
|
||||
const tabId = getPaneKeyTabId(entry.paneKey || key)
|
||||
if (!tabId) {
|
||||
continue
|
||||
}
|
||||
for (const owner of tabOwners.get(tabId) ?? []) {
|
||||
buckets.get(owner)?.agentStatuses.push([key, entry])
|
||||
}
|
||||
}
|
||||
for (const [key, entry] of Object.entries(inputs.migrationUnsupportedByPtyId)) {
|
||||
const tabId = entry.tabId ?? (entry.paneKey ? getPaneKeyTabId(entry.paneKey) : null)
|
||||
const owners = new Set(tabId ? tabOwners.get(tabId) : undefined)
|
||||
if (entry.worktreeId !== undefined && buckets.has(entry.worktreeId)) {
|
||||
owners.add(entry.worktreeId)
|
||||
}
|
||||
for (const owner of owners) {
|
||||
buckets.get(owner)?.migrations.push([key, entry])
|
||||
}
|
||||
}
|
||||
for (const [key, entry] of Object.entries(inputs.retainedAgentsByPaneKey)) {
|
||||
buckets.get(entry.worktreeId)?.retainedAgents.push([key, entry])
|
||||
}
|
||||
for (const file of inputs.openFiles) {
|
||||
buckets.get(file.worktreeId)?.openFiles.push(file)
|
||||
}
|
||||
for (const [id, bucket] of buckets) {
|
||||
result.set(id, {
|
||||
agentStatusByPaneKey: Object.fromEntries(bucket.agentStatuses),
|
||||
migrationUnsupportedByPtyId: Object.fromEntries(bucket.migrations),
|
||||
retainedAgentsByPaneKey: Object.fromEntries(bucket.retainedAgents),
|
||||
openFiles: bucket.openFiles
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export type WorkspaceSpaceAgentActivityInputs = {
|
||||
now: number
|
||||
}
|
||||
|
||||
function getPaneKeyTabId(paneKey: string): string | null {
|
||||
export function getPaneKeyTabId(paneKey: string): string | null {
|
||||
const parsed = parsePaneKey(paneKey)
|
||||
if (parsed) {
|
||||
return parsed.tabId
|
||||
|
||||
Reference in New Issue
Block a user