mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
fix: exclude orphaned worktrees from active agent count
tabsByWorktree can retain entries for worktrees deleted outside Orca or whose cleanup didn't complete. This caused phantom agents to appear in the titlebar count. Now both countWorkingAgents and getWorkingAgentsPerWorktree filter against worktreesByRepo as the source of truth.
This commit is contained in:
parent
a68bab60a6
commit
d88f5d8a88
3 changed files with 103 additions and 21 deletions
|
|
@ -95,23 +95,19 @@ function App(): React.JSX.Element {
|
|||
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
|
||||
const tabsByWorktree = useAppStore((s) => s.tabsByWorktree)
|
||||
const activeTabId = useAppStore((s) => s.activeTabId)
|
||||
const activeAgentCount = useAppStore((s) =>
|
||||
countWorkingAgents({
|
||||
tabsByWorktree: s.tabsByWorktree,
|
||||
runtimePaneTitlesByTabId: s.runtimePaneTitlesByTabId
|
||||
})
|
||||
)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const agentInputs = useAppStore(
|
||||
useShallow((s) => ({
|
||||
tabsByWorktree: s.tabsByWorktree,
|
||||
runtimePaneTitlesByTabId: s.runtimePaneTitlesByTabId
|
||||
runtimePaneTitlesByTabId: s.runtimePaneTitlesByTabId,
|
||||
worktreesByRepo: s.worktreesByRepo
|
||||
}))
|
||||
)
|
||||
const activeAgentCount = useMemo(() => countWorkingAgents(agentInputs), [agentInputs])
|
||||
const workingAgentsPerWorktree = useMemo(
|
||||
() => getWorkingAgentsPerWorktree(agentInputs),
|
||||
[agentInputs]
|
||||
)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const expandedPaneByTabId = useAppStore((s) => s.expandedPaneByTabId)
|
||||
const canExpandPaneByTabId = useAppStore((s) => s.canExpandPaneByTabId)
|
||||
const workspaceSessionReady = useAppStore((s) => s.workspaceSessionReady)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import type { TerminalTab } from '../../../shared/types'
|
||||
import type { TerminalTab, Worktree } from '../../../shared/types'
|
||||
import { countWorkingAgents, getWorkingAgentsPerWorktree } from './agent-status'
|
||||
|
||||
function makeTab(overrides: Partial<TerminalTab> = {}): TerminalTab {
|
||||
|
|
@ -16,6 +16,32 @@ function makeTab(overrides: Partial<TerminalTab> = {}): TerminalTab {
|
|||
}
|
||||
}
|
||||
|
||||
function worktrees(...ids: string[]): Record<string, Worktree[]> {
|
||||
return {
|
||||
repo: ids.map(
|
||||
(id) =>
|
||||
({
|
||||
id,
|
||||
repoId: 'repo',
|
||||
path: `/path/${id}`,
|
||||
head: '',
|
||||
branch: '',
|
||||
isBare: false,
|
||||
isMainWorktree: false,
|
||||
displayName: id,
|
||||
comment: '',
|
||||
linkedIssue: null,
|
||||
linkedPR: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0
|
||||
}) satisfies Worktree
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe('countWorkingAgents', () => {
|
||||
it('counts each live working tab when pane-level titles are unavailable', () => {
|
||||
expect(
|
||||
|
|
@ -27,7 +53,8 @@ describe('countWorkingAgents', () => {
|
|||
],
|
||||
'wt-2': [makeTab({ id: 'tab-3', worktreeId: 'wt-2', title: '⠋ Codex is thinking' })]
|
||||
},
|
||||
runtimePaneTitlesByTabId: {}
|
||||
runtimePaneTitlesByTabId: {},
|
||||
worktreesByRepo: worktrees('wt-1', 'wt-2')
|
||||
})
|
||||
).toBe(3)
|
||||
})
|
||||
|
|
@ -44,7 +71,8 @@ describe('countWorkingAgents', () => {
|
|||
2: '✦ Gemini CLI',
|
||||
3: '✳ Claude Code'
|
||||
}
|
||||
}
|
||||
},
|
||||
worktreesByRepo: worktrees('wt-1')
|
||||
})
|
||||
).toBe(2)
|
||||
})
|
||||
|
|
@ -60,7 +88,8 @@ describe('countWorkingAgents', () => {
|
|||
makeTab({ id: 'tab-4', title: '⠂ Claude Code', ptyId: null })
|
||||
]
|
||||
},
|
||||
runtimePaneTitlesByTabId: {}
|
||||
runtimePaneTitlesByTabId: {},
|
||||
worktreesByRepo: worktrees('wt-1')
|
||||
})
|
||||
).toBe(0)
|
||||
})
|
||||
|
|
@ -76,10 +105,24 @@ describe('countWorkingAgents', () => {
|
|||
1: '✳ Claude Code',
|
||||
2: 'bash'
|
||||
}
|
||||
}
|
||||
},
|
||||
worktreesByRepo: worktrees('wt-1')
|
||||
})
|
||||
).toBe(0)
|
||||
})
|
||||
|
||||
it('excludes orphaned worktrees not in worktreesByRepo', () => {
|
||||
expect(
|
||||
countWorkingAgents({
|
||||
tabsByWorktree: {
|
||||
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })],
|
||||
'wt-deleted': [makeTab({ id: 'tab-2', worktreeId: 'wt-deleted', title: '✦ Gemini CLI' })]
|
||||
},
|
||||
runtimePaneTitlesByTabId: {},
|
||||
worktreesByRepo: worktrees('wt-1')
|
||||
})
|
||||
).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorkingAgentsPerWorktree', () => {
|
||||
|
|
@ -95,7 +138,8 @@ describe('getWorkingAgentsPerWorktree', () => {
|
|||
2: '✦ Gemini CLI',
|
||||
3: '✳ Claude Code'
|
||||
}
|
||||
}
|
||||
},
|
||||
worktreesByRepo: worktrees('wt-1')
|
||||
})
|
||||
).toEqual({
|
||||
'wt-1': {
|
||||
|
|
@ -106,4 +150,21 @@ describe('getWorkingAgentsPerWorktree', () => {
|
|||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('excludes orphaned worktrees not in worktreesByRepo', () => {
|
||||
expect(
|
||||
getWorkingAgentsPerWorktree({
|
||||
tabsByWorktree: {
|
||||
'wt-1': [makeTab({ id: 'tab-1', title: '⠂ Claude Code' })],
|
||||
'wt-deleted': [makeTab({ id: 'tab-2', worktreeId: 'wt-deleted', title: '✦ Gemini CLI' })]
|
||||
},
|
||||
runtimePaneTitlesByTabId: {},
|
||||
worktreesByRepo: worktrees('wt-1')
|
||||
})
|
||||
).toEqual({
|
||||
'wt-1': {
|
||||
agents: [{ label: 'Claude Code', status: 'working', tabId: 'tab-1', paneId: null }]
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { TerminalTab } from '../../../shared/types'
|
||||
import type { TerminalTab, Worktree } from '../../../shared/types'
|
||||
|
||||
// Re-export from shared module so existing renderer imports continue to work.
|
||||
// Why: the main process now needs the same agent detection logic for stat
|
||||
|
|
@ -19,9 +19,10 @@ import {
|
|||
getAgentLabel
|
||||
} from '../../../shared/agent-detection'
|
||||
|
||||
type CountWorkingAgentsArgs = {
|
||||
type AgentQueryArgs = {
|
||||
tabsByWorktree: Record<string, TerminalTab[]>
|
||||
runtimePaneTitlesByTabId: Record<string, Record<number, string>>
|
||||
worktreesByRepo: Record<string, Worktree[]>
|
||||
}
|
||||
|
||||
export type WorkingAgentEntry = {
|
||||
|
|
@ -37,11 +38,20 @@ export type WorktreeAgents = {
|
|||
|
||||
export function getWorkingAgentsPerWorktree({
|
||||
tabsByWorktree,
|
||||
runtimePaneTitlesByTabId
|
||||
}: CountWorkingAgentsArgs): Record<string, WorktreeAgents> {
|
||||
runtimePaneTitlesByTabId,
|
||||
worktreesByRepo
|
||||
}: AgentQueryArgs): Record<string, WorktreeAgents> {
|
||||
const validIds = collectWorktreeIds(worktreesByRepo)
|
||||
const result: Record<string, WorktreeAgents> = {}
|
||||
|
||||
for (const [worktreeId, tabs] of Object.entries(tabsByWorktree)) {
|
||||
// Why: tabsByWorktree can retain orphaned entries for worktrees that no
|
||||
// longer exist in git (e.g. deleted worktrees whose tab cleanup didn't
|
||||
// complete, or worktrees removed outside Orca). worktreesByRepo is the
|
||||
// source of truth — only include worktrees that still exist.
|
||||
if (!validIds.has(worktreeId)) {
|
||||
continue
|
||||
}
|
||||
const agents: WorkingAgentEntry[] = []
|
||||
|
||||
for (const tab of tabs) {
|
||||
|
|
@ -78,11 +88,16 @@ export function getWorkingAgentsPerWorktree({
|
|||
|
||||
export function countWorkingAgents({
|
||||
tabsByWorktree,
|
||||
runtimePaneTitlesByTabId
|
||||
}: CountWorkingAgentsArgs): number {
|
||||
runtimePaneTitlesByTabId,
|
||||
worktreesByRepo
|
||||
}: AgentQueryArgs): number {
|
||||
const validIds = collectWorktreeIds(worktreesByRepo)
|
||||
let count = 0
|
||||
|
||||
for (const tabs of Object.values(tabsByWorktree)) {
|
||||
for (const [worktreeId, tabs] of Object.entries(tabsByWorktree)) {
|
||||
if (!validIds.has(worktreeId)) {
|
||||
continue
|
||||
}
|
||||
for (const tab of tabs) {
|
||||
count += countWorkingAgentsForTab(tab, runtimePaneTitlesByTabId)
|
||||
}
|
||||
|
|
@ -91,6 +106,16 @@ export function countWorkingAgents({
|
|||
return count
|
||||
}
|
||||
|
||||
function collectWorktreeIds(worktreesByRepo: Record<string, Worktree[]>): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
for (const worktrees of Object.values(worktreesByRepo)) {
|
||||
for (const wt of worktrees) {
|
||||
ids.add(wt.id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
function countWorkingAgentsForTab(
|
||||
tab: TerminalTab,
|
||||
runtimePaneTitlesByTabId: Record<string, Record<number, string>>
|
||||
|
|
|
|||
Loading…
Reference in a new issue