mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-21 15:55:50 +00:00
之前几次渲染层修复都失败,因为没动 tab 列表的数据源:打开 /workflows 会 自动 hydrate 最多 20 个历史 done/killed run,全部塞进一行 TabsBar,超出 终端宽度后 Ink 把字符画到屏外造成重影乱码。 - selectors.ts 加 filterActiveRuns(只留 status === 'running')和 capTabsForDisplay(超额 fold 成 +N)两个 pure function - WorkflowsPanel 接线 activeRuns:focus clamp、focused、nextTab/prevTab、 TabsBar 全部基于过滤后的 activeRuns - TabsBar 复用 truncateLabel 限制每个 tab 名 18 字符 + 最多 6 个 tab, 多余显示 +N,从结构上钉死单行总宽度 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
158 lines
4.9 KiB
TypeScript
158 lines
4.9 KiB
TypeScript
import { expect, test } from 'bun:test'
|
|
import type { AgentProgress, RunProgress } from '../progress/store.js'
|
|
import {
|
|
ALL_PHASE,
|
|
capTabsForDisplay,
|
|
filterActiveRuns,
|
|
mergePhases,
|
|
filterAgentsByPhase,
|
|
tabLabel,
|
|
} from '../panel/selectors.js'
|
|
|
|
function run(partial: Partial<RunProgress>): RunProgress {
|
|
return {
|
|
runId: 'r1',
|
|
workflowName: 'w',
|
|
status: 'running',
|
|
phases: [],
|
|
declaredPhases: [],
|
|
currentPhase: null,
|
|
agents: [],
|
|
agentCount: 0,
|
|
startedAt: 1,
|
|
updatedAt: 1,
|
|
...partial,
|
|
}
|
|
}
|
|
|
|
test('mergePhases: declared order first, actual phases append undeclared ones, counts done/total', () => {
|
|
const r = run({
|
|
declaredPhases: ['Find', 'Review', 'Verify'],
|
|
phases: [
|
|
{ title: 'Find', status: 'done' },
|
|
{ title: 'Review', status: 'running' },
|
|
],
|
|
agents: [
|
|
{
|
|
id: 1,
|
|
phase: 'Find',
|
|
status: 'done',
|
|
resultKind: 'ok',
|
|
outputShape: 'text',
|
|
},
|
|
{ id: 2, phase: 'Find', status: 'done', resultKind: 'dead' },
|
|
{ id: 3, phase: 'Review', status: 'running' },
|
|
],
|
|
})
|
|
expect(mergePhases(r)).toEqual([
|
|
{ title: 'Find', status: 'done', done: 2, total: 2 },
|
|
{ title: 'Review', status: 'running', done: 0, total: 1 },
|
|
{ title: 'Verify', status: 'pending', done: 0, total: 0 },
|
|
])
|
|
})
|
|
|
|
test('mergePhases: actual but undeclared phase appended to the end', () => {
|
|
const r = run({
|
|
declaredPhases: ['Find'],
|
|
phases: [
|
|
{ title: 'Find', status: 'done' },
|
|
{ title: 'Adhoc', status: 'running' },
|
|
],
|
|
agents: [],
|
|
})
|
|
expect(mergePhases(r).map(p => p.title)).toEqual(['Find', 'Adhoc'])
|
|
})
|
|
|
|
test('filterAgentsByPhase: All / undefined → all; specified → only that phase', () => {
|
|
const agents: AgentProgress[] = [
|
|
{ id: 1, phase: 'A', status: 'running' },
|
|
{
|
|
id: 2,
|
|
phase: 'B',
|
|
status: 'done',
|
|
resultKind: 'ok',
|
|
outputShape: 'text',
|
|
},
|
|
]
|
|
expect(filterAgentsByPhase(agents, undefined)).toHaveLength(2)
|
|
expect(filterAgentsByPhase(agents, ALL_PHASE)).toHaveLength(2)
|
|
expect(filterAgentsByPhase(agents, 'A')).toEqual([agents[0]])
|
|
})
|
|
|
|
test('tabLabel: workflow name + last 4 chars short code of runId', () => {
|
|
expect(tabLabel('review-changes', 'wf_abc123def')).toBe('review-changes#3def')
|
|
})
|
|
|
|
// filterActiveRuns: only running runs reach the panel's tab row. Done/killed/completed are hidden
|
|
// so opening /workflows no longer floods the tab row with months of historical runs (caused
|
|
// tab overflow → garbled render when total width exceeded the terminal).
|
|
test('filterActiveRuns: only status === "running" survives; completed/failed/killed dropped', () => {
|
|
const r1 = run({ runId: 'r1', status: 'running' })
|
|
const r2 = run({ runId: 'r2', status: 'running' })
|
|
const r3 = run({ runId: 'r3', status: 'completed' })
|
|
const r4 = run({ runId: 'r4', status: 'failed' })
|
|
const r5 = run({ runId: 'r5', status: 'killed' })
|
|
expect(filterActiveRuns([r1, r2, r3, r4, r5])).toEqual([r1, r2])
|
|
})
|
|
|
|
test('filterActiveRuns: empty input -> empty output', () => {
|
|
expect(filterActiveRuns([])).toEqual([])
|
|
})
|
|
|
|
test('filterActiveRuns: all terminal -> empty (panel falls back to "(no active runs)")', () => {
|
|
expect(
|
|
filterActiveRuns([run({ status: 'completed' }), run({ status: 'killed' })]),
|
|
).toEqual([])
|
|
})
|
|
|
|
test('filterActiveRuns: preserves input order (no re-sort)', () => {
|
|
const a = run({ runId: 'a', status: 'running', startedAt: 5 })
|
|
const b = run({ runId: 'b', status: 'running', startedAt: 1 })
|
|
expect(filterActiveRuns([a, b]).map(r => r.runId)).toEqual(['a', 'b'])
|
|
})
|
|
|
|
// capTabsForDisplay: even if active runs somehow accumulate (long-lived sessions, runaway launcher),
|
|
// the tab row must never overflow the terminal — cap at maxTabs, fold the remainder into a +N marker.
|
|
test('capTabsForDisplay: under cap -> as-is', () => {
|
|
const runs = [
|
|
run({ runId: 'r1', status: 'running' }),
|
|
run({ runId: 'r2', status: 'running' }),
|
|
]
|
|
expect(capTabsForDisplay(runs, 8)).toEqual({ runs, overflow: 0 })
|
|
})
|
|
|
|
test('capTabsForDisplay: over cap -> first maxTabs runs + overflow count', () => {
|
|
const runs = Array.from({ length: 10 }, (_, i) =>
|
|
run({ runId: `r${i}`, status: 'running' }),
|
|
)
|
|
const capped = capTabsForDisplay(runs, 8)
|
|
expect(capped.runs).toHaveLength(8)
|
|
expect(capped.runs.map(r => r.runId)).toEqual([
|
|
'r0',
|
|
'r1',
|
|
'r2',
|
|
'r3',
|
|
'r4',
|
|
'r5',
|
|
'r6',
|
|
'r7',
|
|
])
|
|
expect(capped.overflow).toBe(2)
|
|
})
|
|
|
|
test('capTabsForDisplay: exactly at cap -> no overflow', () => {
|
|
const runs = Array.from({ length: 8 }, (_, i) =>
|
|
run({ runId: `r${i}`, status: 'running' }),
|
|
)
|
|
const capped = capTabsForDisplay(runs, 8)
|
|
expect(capped.runs).toHaveLength(8)
|
|
expect(capped.overflow).toBe(0)
|
|
})
|
|
|
|
test('capTabsForDisplay: maxTabs=0 -> all folded into overflow (degenerate but defined)', () => {
|
|
const runs = [run({ runId: 'r1', status: 'running' })]
|
|
const capped = capTabsForDisplay(runs, 0)
|
|
expect(capped.runs).toEqual([])
|
|
expect(capped.overflow).toBe(1)
|
|
})
|