refactor(shared): derive WellKnownAgentType from TuiAgent

The hand-written 22-member WellKnownAgentType union was a stale copy of the
launchable-agent list, 15 members behind TuiAgent: aug, autohand,
claude-agent-teams, cline, codebuff, continue, crush, goose, kilo, kimi, kiro,
mistral-vibe, openclaw, qwen-code, rovo. All 21 non-'unknown' members it did
carry were already TuiAgent members, so the union is now derived
(`TuiAgent | 'unknown'`) and cannot drift again.

'unknown' stays outside TuiAgent: it is the "no agent identified yet" sentinel,
not a launchable agent.

AgentType is structurally unchanged. `(string & {})` absorbs the union, so
AgentType was and remains `string` — this is a documentation/staleness fix with
no behaviour change and zero consumers affected (WellKnownAgentType had none
repo-wide beyond the AgentType alias itself).

tui-agent.ts is a pure type union with no imports, so this adds no cycle.

Adds a type-level coverage test that fails to compile if anyone reverts to a
hand-written list.
This commit is contained in:
Jinjing
2026-09-08 01:28:45 -07:00
parent d346d6f447
commit 701063bad6
2 changed files with 35 additions and 24 deletions
+31
View File
@@ -15,6 +15,8 @@ import {
AGENT_STATUS_STATES,
AGENT_TYPE_MAX_LENGTH
} from './agent-status-types'
import type { AgentType, WellKnownAgentType } from './agent-status-types'
import type { TuiAgent } from './tui-agent'
afterEach(() => {
vi.restoreAllMocks()
@@ -674,3 +676,32 @@ describe('normalizeAgentStatusPayload matches the JSON round trip', () => {
}
})
})
describe('WellKnownAgentType', () => {
// Compile-time proof the union is derived from TuiAgent rather than hand-copied:
// a literal list that misses any launchable agent id fails to typecheck here.
const widenTuiAgent = (agent: TuiAgent): WellKnownAgentType => agent
it('covers every TuiAgent id plus the unknown sentinel', () => {
// ids the previous 22-member hand-written union had drifted past
const formerlyMissing: WellKnownAgentType[] = [
'qwen-code',
'mistral-vibe',
'claude-agent-teams'
]
const sentinel: WellKnownAgentType = 'unknown'
expect([...formerlyMissing, sentinel, widenTuiAgent('rovo')]).toEqual([
'qwen-code',
'mistral-vibe',
'claude-agent-teams',
'unknown',
'rovo'
])
})
it('keeps AgentType open to custom agent names', () => {
const custom: AgentType = 'some-in-house-agent'
expect(custom).toBe('some-in-house-agent')
})
})
+4 -24
View File
@@ -5,6 +5,7 @@
import type { AgentProviderSessionMetadata } from './agent-session-resume'
import type { OrchestrationFleetAttention } from './orchestration-fleet-attention'
import type { AgentStatusRowFacets } from './agent-status-observation'
import type { TuiAgent } from './tui-agent'
import {
normalizeInteractivePromptField,
normalizeOptionalField,
@@ -26,30 +27,9 @@ export const AGENT_STATUS_STATES = ['working', 'blocked', 'waiting', 'done'] as
export type AgentStatusState = (typeof AGENT_STATUS_STATES)[number]
export type AgentWorkingMode = 'monitoring'
// Why: agent types aren't a fixed set (custom agents exist); any non-empty string is
// accepted — these well-known names are just a convenience union for pattern-matching.
export type WellKnownAgentType =
| 'claude'
| 'openclaude'
| 'codex'
| 'gemini'
| 'antigravity'
| 'amp'
| 'opencode'
| 'mimo-code'
| 'cursor'
| 'copilot'
| 'aider'
| 'pi'
| 'omp'
| 'prime-agent'
| 'droid'
| 'command-code'
| 'grok'
| 'hermes'
| 'devin'
| 'ante'
| 'trae'
| 'unknown'
// accepted — the well-known names are the launchable TuiAgent ids plus the 'unknown'
// sentinel (no agent identified yet), a convenience union for pattern-matching.
export type WellKnownAgentType = TuiAgent | 'unknown'
export type AgentType = WellKnownAgentType | (string & {})
/** A snapshot of a previous agent state, used to render activity blocks.