mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
fix(sidebar): rank interrupted below every live state
Corrects the precedence from the previous commit. Interrupted means the user pressed Esc or Ctrl+C — a deliberate act, so they already know. It must be DISTINGUISHABLE from done (that is the bug) without being LOUDER than anything live. Moved below done in all three ladders — resolveWorktreeStatus, the tab attention badge, and SUMMARY_STATE_ORDER, which had also ranked it above working. That matches smart-attention, which already classes an interrupted done as 4 (idle), below both done and working; the card, the tab, the summary text and the sort now agree instead of two of them disagreeing. Tests re-pinned to the corrected order, including one of my own from the previous commit that asserted a finished sibling must not mask an interrupted pane — it should, and now does.
This commit is contained in:
@@ -8,13 +8,15 @@ export type SummaryAgentGroup = {
|
||||
agents: DashboardAgentRowData[]
|
||||
}
|
||||
|
||||
// Why interrupted sits below done (STA-5357): Esc / Ctrl+C is a deliberate act, so the user already
|
||||
// knows — it demands the least attention of any live-ish state, matching smart-attention's Class 4.
|
||||
const SUMMARY_STATE_ORDER: AgentDotState[] = [
|
||||
'waiting',
|
||||
'blocked',
|
||||
'interrupted',
|
||||
'working',
|
||||
'monitoring',
|
||||
'done',
|
||||
'interrupted',
|
||||
'idle'
|
||||
]
|
||||
|
||||
|
||||
@@ -125,7 +125,9 @@ describe('resolveTerminalTabActivityStatus', () => {
|
||||
).toBe('interrupted')
|
||||
})
|
||||
|
||||
it('does not let a finished sibling mask an interrupted pane', () => {
|
||||
it('lets a finished sibling win — interrupted is the quietest state', () => {
|
||||
// Why: Esc / Ctrl+C is deliberate, so the user already knows. Interrupted must be
|
||||
// DISTINGUISHABLE from done (that is the bug) without being LOUDER than anything live.
|
||||
const interrupted = entry(FIRST_LEAF_ID, 'done', { interrupted: true })
|
||||
const finished = entry(SECOND_LEAF_ID, 'done')
|
||||
expect(
|
||||
@@ -137,7 +139,7 @@ describe('resolveTerminalTabActivityStatus', () => {
|
||||
},
|
||||
ptyIdsByTabId: LIVE_PTY
|
||||
})
|
||||
).toBe('interrupted')
|
||||
).toBe('done')
|
||||
})
|
||||
|
||||
it('falls back to a live working title when hook status is stale', () => {
|
||||
|
||||
@@ -213,16 +213,17 @@ export function resolveTerminalTabAttentionBadge({
|
||||
if (status === 'monitoring') {
|
||||
return 'monitoring'
|
||||
}
|
||||
// Why above unread and done: a cancelled turn needs a human, and an unread bell would bury it.
|
||||
if (status === 'interrupted') {
|
||||
return 'interrupted'
|
||||
}
|
||||
if (hasUnread) {
|
||||
return 'unread'
|
||||
}
|
||||
if (status === 'done') {
|
||||
return 'done'
|
||||
}
|
||||
// Why last (STA-5357): the user pressed Esc / Ctrl+C, so this is the least attention-demanding
|
||||
// signal — it must be distinguishable from `done`, but it must not outrank anything live.
|
||||
if (status === 'interrupted') {
|
||||
return 'interrupted'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -16,23 +16,21 @@ const base = {
|
||||
|
||||
describe('resolveWorktreeStatus — interrupted (STA-5357)', () => {
|
||||
it('reports interrupted rather than done for an interrupted agent', () => {
|
||||
// Why `hasLiveDone` is also set: `interrupted` only ever coexists with `done`
|
||||
// (agent-status-types.ts clamps it), so the done flag is always present too.
|
||||
// Before the fix that made the card indistinguishable from a clean finish.
|
||||
expect(resolveWorktreeStatus({ ...base, hasInterrupted: true, hasLiveDone: true })).toBe(
|
||||
'interrupted'
|
||||
)
|
||||
// The bug: `interrupted` is clamped onto `done` at parse time, so the summary swallowed it into
|
||||
// hasLiveDone and the card rendered a cancelled turn as a clean finish.
|
||||
expect(resolveWorktreeStatus({ ...base, hasInterrupted: true })).toBe('interrupted')
|
||||
})
|
||||
|
||||
it('keeps interrupted visible when a sibling agent merely finished', () => {
|
||||
expect(resolveWorktreeStatus({ ...base, hasInterrupted: true, hasLiveDone: true })).not.toBe(
|
||||
'done'
|
||||
)
|
||||
it('is never the emerald done state on its own', () => {
|
||||
expect(resolveWorktreeStatus({ ...base, hasInterrupted: true })).not.toBe('done')
|
||||
})
|
||||
|
||||
it('outranks working, so a cancelled turn is not hidden by a busy sibling', () => {
|
||||
// Why interrupted YIELDS to everything else: Esc / Ctrl+C is a deliberate act, so the user already
|
||||
// knows. It is the least attention-demanding state — smart-attention already classes it 4 (idle),
|
||||
// below both done and working. Distinguishable from done, never louder than a live agent.
|
||||
it('yields to a working sibling — the live agent is the louder signal', () => {
|
||||
expect(resolveWorktreeStatus({ ...base, hasInterrupted: true, hasLiveWorking: true })).toBe(
|
||||
'interrupted'
|
||||
'working'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -42,12 +40,16 @@ describe('resolveWorktreeStatus — interrupted (STA-5357)', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('outranks monitoring', () => {
|
||||
it('yields to monitoring — background work is still live', () => {
|
||||
expect(resolveWorktreeStatus({ ...base, hasInterrupted: true, hasLiveMonitoring: true })).toBe(
|
||||
'interrupted'
|
||||
'monitoring'
|
||||
)
|
||||
})
|
||||
|
||||
it('yields to a sibling that genuinely finished', () => {
|
||||
expect(resolveWorktreeStatus({ ...base, hasInterrupted: true, hasLiveDone: true })).toBe('done')
|
||||
})
|
||||
|
||||
it('leaves every other combination alone', () => {
|
||||
expect(resolveWorktreeStatus({ ...base, hasLiveDone: true })).toBe('done')
|
||||
expect(resolveWorktreeStatus({ ...base, hasLiveWorking: true })).toBe('working')
|
||||
|
||||
@@ -167,13 +167,6 @@ export function resolveWorktreeStatus(args: {
|
||||
if (heuristic === 'permission') {
|
||||
return 'permission'
|
||||
}
|
||||
// Why above working (STA-5357): `interrupted` only ever rides on `done`, so before this it set
|
||||
// `hasLiveDone` and the card rendered a cancelled turn as a clean finish — and a busy sibling could
|
||||
// hide it entirely. It sits below permission because a prompt waiting on the user is the more urgent
|
||||
// ask, and matches SUMMARY_STATE_ORDER so the card and the expanded agent list agree.
|
||||
if (args.hasInterrupted) {
|
||||
return 'interrupted'
|
||||
}
|
||||
// Why: restored cards get the hook snapshot before panes mount; trust the explicit working row so they stay yellow on restart.
|
||||
if (args.hasLiveWorking || heuristic === 'working') {
|
||||
return 'working'
|
||||
@@ -184,5 +177,13 @@ export function resolveWorktreeStatus(args: {
|
||||
if (args.hasLiveDone || args.hasRetainedDone) {
|
||||
return 'done'
|
||||
}
|
||||
// Why LAST (STA-5357): `interrupted` means the user pressed Esc or Ctrl+C, so they already know —
|
||||
// it is the least attention-demanding state, exactly as smart-attention classes it (Class 4, idle,
|
||||
// below both done and working). It still needs its own branch rather than folding into `done`,
|
||||
// because the flag rides on `done` and rendering it as a completion is the bug this fixes: a
|
||||
// cancelled turn is not a finished one.
|
||||
if (args.hasInterrupted) {
|
||||
return 'interrupted'
|
||||
}
|
||||
return heuristic
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user