fix(tui): handle timeout/error subagent statuses in /agents (#26687)

Accept delegation timeout/error statuses in the TUI subagent model, normalize unknown status strings defensively, and harden /agents overlay rendering/sorting so unknown statuses cannot crash glyph/color lookup. Add regression tests for live event normalization and disk snapshot replay.
This commit is contained in:
brooklyn! 2026-05-15 20:19:02 -05:00 committed by GitHub
parent 566d8f0d75
commit 006937f7d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 173 additions and 14 deletions

View file

@ -57,25 +57,33 @@ const FILTER_LABEL: Record<FilterMode, string> = {
}
const STATUS_RANK: Record<Status, number> = {
error: 0,
failed: 0,
interrupted: 1,
timeout: 1,
running: 2,
queued: 3,
completed: 4
}
const statusRank = (status: string): number => STATUS_RANK[status as Status] ?? STATUS_RANK.error
const SORT_COMPARATORS: Record<SortMode, (a: SubagentNode, b: SubagentNode) => number> = {
'depth-first': (a, b) => a.item.depth - b.item.depth || a.item.index - b.item.index,
'tools-desc': (a, b) => b.aggregate.totalTools - a.aggregate.totalTools,
'duration-desc': (a, b) => b.aggregate.totalDuration - a.aggregate.totalDuration,
status: (a, b) => STATUS_RANK[a.item.status] - STATUS_RANK[b.item.status]
status: (a, b) => statusRank(a.item.status) - statusRank(b.item.status)
}
const FILTER_PREDICATES: Record<FilterMode, (n: SubagentNode) => boolean> = {
all: () => true,
leaf: n => n.children.length === 0,
running: n => n.item.status === 'running' || n.item.status === 'queued',
failed: n => n.item.status === 'failed' || n.item.status === 'interrupted'
failed: n =>
n.item.status === 'error' ||
n.item.status === 'failed' ||
n.item.status === 'interrupted' ||
n.item.status === 'timeout'
}
const STATUS_GLYPH: Record<Status, { color: (t: Theme) => string; glyph: string }> = {
@ -83,7 +91,9 @@ const STATUS_GLYPH: Record<Status, { color: (t: Theme) => string; glyph: string
queued: { color: t => t.color.muted, glyph: '○' },
completed: { color: t => t.color.statusGood, glyph: '✓' },
interrupted: { color: t => t.color.warn, glyph: '■' },
failed: { color: t => t.color.error, glyph: '✗' }
failed: { color: t => t.color.error, glyph: '✗' },
timeout: { color: t => t.color.warn, glyph: '⌛' },
error: { color: t => t.color.error, glyph: '⚠' }
}
// Heatmap palette — cold → hot, resolved against the active theme.
@ -111,7 +121,8 @@ const formatRowId = (n: number): string => String(n + 1).padStart(2, ' ')
const cycle = <T,>(order: readonly T[], current: T): T => order[(order.indexOf(current) + 1) % order.length]!
const statusGlyph = (item: SubagentProgress, t: Theme) => {
const g = STATUS_GLYPH[item.status]
// Defensive fallback for cross-version snapshots with unknown statuses.
const g = STATUS_GLYPH[item.status] ?? STATUS_GLYPH.error
return { color: g.color(t), glyph: g.glyph }
}

View file

@ -327,7 +327,11 @@ function SubagentAccordion({
const aggregate = node.aggregate
const statusTone: 'dim' | 'error' | 'warn' =
item.status === 'failed' ? 'error' : item.status === 'interrupted' ? 'warn' : 'dim'
item.status === 'error' || item.status === 'failed'
? 'error'
: item.status === 'interrupted' || item.status === 'timeout'
? 'warn'
: 'dim'
const prefix = item.taskCount > 1 ? `[${item.index + 1}/${item.taskCount}] ` : ''
const goalLabel = item.goal || `Subagent ${item.index + 1}`