mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 08:03:33 +00:00
fix(sidebar): spin the worktree dot for Claude's braille-spinner titles (#9040)
The worktree activity dot required a provider token in the terminal title
before it would attribute activity to an agent. Claude's thinking title is a
braille spinner plus free-form task text ("⠋ implementing the feature") with
no provider token, so the gate rejected it and the worktree resolved to
'active' the whole time Claude was working. StatusIndicator renders 'active'
and 'done' with the same emerald dot and only 'working' as the yellow spinner
ring, so the symptom reads as a missing thinking indicator.
The sidebar row builder already solved this in #9647 by falling back to the
tab's launchAgent for spinner-only titles. Thread the same launch identity
into the dot's attribution gate so the dot and the row agree.
The #9647 invariant is preserved: a spinner with no launch identity still
cannot spin the dot, so a frozen spinner frame left behind by an exited agent
does not produce a permanent "working, 0 agents" state.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getWorktreeStatus } from './worktree-status'
|
||||
|
||||
function livePtyMap(...tabIds: string[]): Record<string, string[]> {
|
||||
return Object.fromEntries(tabIds.map((id, i) => [id, [`pty-${i}`]]))
|
||||
}
|
||||
|
||||
// Why: #9040 — Claude's thinking title is a braille spinner plus task text with no
|
||||
// provider token, so the dot's attribution gate rejected it and the worktree stayed
|
||||
// grey while Claude was clearly working. The sidebar row builder already falls back
|
||||
// to the tab's launch identity for spinner-only titles (#9647); the dot must agree.
|
||||
describe('#9040 worktree dot attributes spinner titles to the launched agent', () => {
|
||||
it('spins for a Claude spinner title when the tab was launched as claude', () => {
|
||||
const status = getWorktreeStatus(
|
||||
[{ id: 'tab-1', title: '⠋ implementing the feature', launchAgent: 'claude' }],
|
||||
[],
|
||||
livePtyMap('tab-1')
|
||||
)
|
||||
|
||||
expect(status).toBe('working')
|
||||
})
|
||||
|
||||
it('spins for a spinner-only pane title when the tab was launched as claude', () => {
|
||||
const status = getWorktreeStatus(
|
||||
[{ id: 'tab-1', title: 'bash', launchAgent: 'claude' }],
|
||||
[],
|
||||
livePtyMap('tab-1'),
|
||||
{ 'tab-1': { 0: '⠙ refactoring the parser' } }
|
||||
)
|
||||
|
||||
expect(status).toBe('working')
|
||||
})
|
||||
|
||||
// Why: pins the #9647 gate — spinner attribution needs a launch identity, so a
|
||||
// frozen spinner frame left by an exited agent still cannot spin the dot forever.
|
||||
it('stays active for a spinner title with no launch identity', () => {
|
||||
const status = getWorktreeStatus(
|
||||
[{ id: 'tab-1', title: '⠐ Review branch for regressions' }],
|
||||
[],
|
||||
livePtyMap('tab-1')
|
||||
)
|
||||
|
||||
expect(status).toBe('active')
|
||||
})
|
||||
|
||||
it('does not manufacture activity from a non-spinner title with a launch identity', () => {
|
||||
const status = getWorktreeStatus(
|
||||
[{ id: 'tab-1', title: 'bash', launchAgent: 'claude' }],
|
||||
[],
|
||||
livePtyMap('tab-1')
|
||||
)
|
||||
|
||||
expect(status).toBe('active')
|
||||
})
|
||||
})
|
||||
@@ -2,10 +2,12 @@ import { resolveAgentTypeFromTerminalTitle } from '@/components/sidebar/worktree
|
||||
import { classifyTitleActivity } from '@/lib/pane-agent-evidence'
|
||||
import { tabHasLivePty } from '@/lib/tab-has-live-pty'
|
||||
import { resolveRuntimePaneTitleLeafIdFromRoot } from '@/lib/runtime-pane-title-leaf-id'
|
||||
import { containsBrailleSpinner } from '../../../shared/agent-title-core'
|
||||
import type {
|
||||
TerminalLayoutSnapshot,
|
||||
TerminalPaneLayoutNode,
|
||||
TerminalTab
|
||||
TerminalTab,
|
||||
TuiAgent
|
||||
} from '../../../shared/types'
|
||||
import type { LiveAgentWorktreeStatus } from './worktree-activity-state'
|
||||
|
||||
@@ -27,7 +29,7 @@ const STATUS_LABELS: Record<WorktreeStatus, string> = {
|
||||
}
|
||||
|
||||
export function getWorktreeStatus(
|
||||
tabs: readonly Pick<TerminalTab, 'id' | 'title'>[],
|
||||
tabs: readonly Pick<TerminalTab, 'id' | 'title' | 'launchAgent'>[],
|
||||
browserTabs: readonly { id: string }[],
|
||||
ptyIdsByTabId: Record<string, string[]>,
|
||||
runtimePaneTitlesByTabId: Record<string, Record<number, string>> = {},
|
||||
@@ -54,7 +56,7 @@ export function getWorktreeStatus(
|
||||
}
|
||||
|
||||
function tabHasStatus(
|
||||
tab: Pick<TerminalTab, 'id' | 'title'>,
|
||||
tab: Pick<TerminalTab, 'id' | 'title' | 'launchAgent'>,
|
||||
runtimePaneTitlesByTabId: Record<string, Record<number, string>>,
|
||||
status: 'permission' | 'working',
|
||||
options: WorktreeStatusHeuristicOptions
|
||||
@@ -77,7 +79,10 @@ function tabHasStatus(
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (classifyTitleActivity(title) === status && titleStatusIsAgentAttributable(title)) {
|
||||
if (
|
||||
classifyTitleActivity(title) === status &&
|
||||
titleStatusIsAgentAttributable(title, tab.launchAgent)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -87,12 +92,21 @@ function tabHasStatus(
|
||||
if (agentStatusPaneIds && agentStatusPaneIds.size > 0) {
|
||||
return false
|
||||
}
|
||||
return classifyTitleActivity(tab.title) === status && titleStatusIsAgentAttributable(tab.title)
|
||||
return (
|
||||
classifyTitleActivity(tab.title) === status &&
|
||||
titleStatusIsAgentAttributable(tab.title, tab.launchAgent)
|
||||
)
|
||||
}
|
||||
|
||||
// Why: require agent attribution so a bare never-cleared spinner title can't spin the dot "0 agents" forever with no matching sidebar row.
|
||||
function titleStatusIsAgentAttributable(title: string): boolean {
|
||||
return resolveAgentTypeFromTerminalTitle(title) !== null
|
||||
function titleStatusIsAgentAttributable(title: string, launchAgent?: TuiAgent | null): boolean {
|
||||
if (resolveAgentTypeFromTerminalTitle(title) !== null) {
|
||||
return true
|
||||
}
|
||||
// Why: a spinner proves activity but not identity (Claude's thinking title has no provider
|
||||
// token, #9040); the tab's launch identity supplies it, mirroring the row builder's spinner
|
||||
// fallback (#9647) so the dot and the sidebar row agree.
|
||||
return containsBrailleSpinner(title) && Boolean(launchAgent)
|
||||
}
|
||||
|
||||
export function getWorktreeStatusLabel(status: WorktreeStatus): string {
|
||||
@@ -109,7 +123,7 @@ export function getWorktreeStatusLabel(status: WorktreeStatus): string {
|
||||
* `hasRetainedDone` is a retained-agent snapshot scoped to this worktreeId.
|
||||
*/
|
||||
export function resolveWorktreeStatus(args: {
|
||||
tabs: readonly Pick<TerminalTab, 'id' | 'title'>[]
|
||||
tabs: readonly Pick<TerminalTab, 'id' | 'title' | 'launchAgent'>[]
|
||||
browserTabs: readonly { id: string }[]
|
||||
ptyIdsByTabId: Record<string, string[]>
|
||||
runtimePaneTitlesByTabId?: Record<string, Record<number, string>>
|
||||
|
||||
Reference in New Issue
Block a user