diff --git a/docs/ai/design/feature-agent-list-type.md b/docs/ai/design/feature-agent-list-type.md new file mode 100644 index 0000000..8c3b38e --- /dev/null +++ b/docs/ai/design/feature-agent-list-type.md @@ -0,0 +1,55 @@ +--- +phase: design +title: "Agent List Type Column - Design" +description: Technical design for adding agent type display to the list command +--- + +# Design: Agent List Type Column + +## Architecture Overview + +This is a presentation-layer change only. No data model or adapter changes needed. + +```mermaid +graph LR + A[AgentAdapter] -->|AgentInfo with type| B[AgentManager] + B -->|agents array| C[agent list command] + C -->|formatType helper| D[Table Output] + style C fill:#ff9,stroke:#333 + style D fill:#ff9,stroke:#333 +``` + +Yellow highlights indicate changed components. + +## Data Models + +No changes. `AgentInfo.type` (`AgentType = 'claude' | 'gemini_cli' | 'codex' | 'other'`) is already available. + +## Component Changes + +### `packages/cli/src/commands/agent.ts` + +1. **Add `formatType()` helper** — maps `AgentType` to human-friendly label: + | AgentType | Display Label | + |-----------|--------------| + | `claude` | Claude Code | + | `codex` | Codex | + | `gemini_cli` | Gemini CLI | + | `other` | Other | + +2. **Update table headers** — insert "Type" as the 2nd column: + `['Agent', 'Type', 'Status', 'Working On', 'Active']` + +3. **Update row mapping** — insert `formatType(agent.type)` as the 2nd value. + +4. **Update column styles** — insert a style function for the Type column (dim or standard color). + +## Design Decisions + +- **Human-friendly labels**: Users shouldn't need to know internal enum values. +- **2nd column placement**: Type is a primary identifier, logically grouped with agent name. +- **No data layer changes**: The type field already exists and is populated correctly. + +## Non-Functional Requirements + +- No performance impact — simple string mapping on already-loaded data. diff --git a/docs/ai/implementation/feature-agent-list-type.md b/docs/ai/implementation/feature-agent-list-type.md new file mode 100644 index 0000000..31e7710 --- /dev/null +++ b/docs/ai/implementation/feature-agent-list-type.md @@ -0,0 +1,35 @@ +--- +phase: implementation +title: "Agent List Type Column - Implementation" +description: Implementation notes for adding type column +--- + +# Implementation: Agent List Type Column + +## Code Structure + +**Files to modify:** +- `packages/cli/src/commands/agent.ts` — main change (formatType helper + table update) +- `packages/cli/src/__tests__/commands/agent.test.ts` — test updates + +## Implementation Notes + +### `formatType()` helper + +```typescript +function formatType(type: AgentType): string { + const labels: Record = { + claude: 'Claude Code', + codex: 'Codex', + gemini_cli: 'Gemini CLI', + other: 'Other', + }; + return labels[type] ?? type; +} +``` + +### Table changes + +- Insert "Type" header at index 1 +- Insert `formatType(agent.type)` in row at index 1 +- Insert column style at index 1 (standard text, no special coloring) diff --git a/docs/ai/planning/feature-agent-list-type.md b/docs/ai/planning/feature-agent-list-type.md new file mode 100644 index 0000000..924bb75 --- /dev/null +++ b/docs/ai/planning/feature-agent-list-type.md @@ -0,0 +1,35 @@ +--- +phase: planning +title: "Agent List Type Column - Planning" +description: Task breakdown for adding agent type to list output +--- + +# Planning: Agent List Type Column + +## Milestones + +- [ ] Milestone 1: Type column displayed in agent list table + +## Task Breakdown + +### Phase 1: Implementation + +- [x] Task 1.1: Add `formatType()` helper function in `packages/cli/src/commands/agent.ts` +- [x] Task 1.2: Update table headers to include "Type" as 2nd column +- [x] Task 1.3: Update row mapping to include formatted type as 2nd value +- [x] Task 1.4: Update columnStyles array to include Type column style + +### Phase 2: Testing + +- [x] Task 2.1: Update existing agent list tests to expect the Type column +- [x] Task 2.2: Add unit tests for `formatType()` covering all AgentType values + +## Dependencies + +- Task 1.2–1.4 depend on Task 1.1 +- Task 2.1–2.2 can run after Phase 1 is complete + +## Risks & Mitigation + +- **Low risk**: Purely additive UI change. No data or adapter modifications. +- **Table width**: Adding a column could affect formatting on narrow terminals — mitigated by short labels. diff --git a/docs/ai/requirements/feature-agent-list-type.md b/docs/ai/requirements/feature-agent-list-type.md new file mode 100644 index 0000000..3c8a4fa --- /dev/null +++ b/docs/ai/requirements/feature-agent-list-type.md @@ -0,0 +1,44 @@ +--- +phase: requirements +title: "Agent List Type Column" +description: Display agent type (Claude Code, Codex, etc.) in the agent list table output +--- + +# Requirements: Agent List Type Column + +## Problem Statement + +When running `ai-devkit agent list`, users see agents listed with Name, Status, Working On, and Active columns. However, the **agent type** (Claude Code, Codex, etc.) is not displayed in the table output, even though the data is already available in the `AgentInfo` model. Users managing multiple agent types cannot quickly distinguish which tool each agent belongs to without using `--json`. + +## Goals & Objectives + +**Primary goals:** +- Display a human-friendly "Type" column in the `agent list` table output +- Map internal type values to readable labels: `claude` → "Claude Code", `codex` → "Codex", `gemini_cli` → "Gemini CLI", `other` → "Other" + +**Non-goals:** +- Changing the AgentType enum or adding new agent types +- Filtering agents by type (future feature) +- Modifying `--json` output (type is already included) + +## User Stories & Use Cases + +- As a developer running multiple agent types, I want to see each agent's type in the list so I can quickly identify which tool is handling each task. +- As a user with both Claude Code and Codex agents, I want to distinguish them at a glance without resorting to JSON output. + +## Success Criteria + +- [ ] `ai-devkit agent list` shows a "Type" column as the 2nd column (after Agent) +- [ ] Type labels are human-friendly (not raw enum values) +- [ ] Existing table layout and functionality is preserved +- [ ] All existing tests pass; new tests cover the type column + +## Constraints & Assumptions + +- The `AgentInfo.type` field is already populated by all adapters +- Table column order: Agent | Type | Status | Working On | Active +- No changes to the data model or adapter layer required + +## Questions & Open Items + +- None — all information is available from the existing codebase. diff --git a/docs/ai/testing/feature-agent-list-type.md b/docs/ai/testing/feature-agent-list-type.md new file mode 100644 index 0000000..899b7af --- /dev/null +++ b/docs/ai/testing/feature-agent-list-type.md @@ -0,0 +1,29 @@ +--- +phase: testing +title: "Agent List Type Column - Testing" +description: Test strategy for the agent type display feature +--- + +# Testing: Agent List Type Column + +## Test Coverage Goals + +- 100% coverage of `formatType()` helper +- All existing agent list tests updated to validate the Type column + +## Unit Tests + +### `formatType()` +- [ ] Returns "Claude Code" for `claude` type +- [ ] Returns "Codex" for `codex` type +- [ ] Returns "Gemini CLI" for `gemini_cli` type +- [ ] Returns "Other" for `other` type + +### Agent list table output +- [ ] Table headers include "Type" as 2nd column +- [ ] Each row includes the formatted type value +- [ ] Existing status, name, summary, and active columns still render correctly + +## Test Data + +- Use existing mock agent fixtures with explicit `type` values diff --git a/packages/cli/src/__tests__/commands/agent.test.ts b/packages/cli/src/__tests__/commands/agent.test.ts index a3e83cc..b0829d8 100644 --- a/packages/cli/src/__tests__/commands/agent.test.ts +++ b/packages/cli/src/__tests__/commands/agent.test.ts @@ -72,6 +72,7 @@ describe('agent command', () => { const agents = [ { name: 'repo-a', + type: 'claude', status: AgentStatus.RUNNING, summary: 'Working', lastActive: now, @@ -104,6 +105,7 @@ describe('agent command', () => { mockManager.listAgents.mockResolvedValue([ { name: 'repo-a', + type: 'claude', status: AgentStatus.WAITING, summary: 'Need input', lastActive: new Date('2026-02-26T10:00:00.000Z'), @@ -111,6 +113,7 @@ describe('agent command', () => { }, { name: 'repo-b', + type: 'codex', status: AgentStatus.IDLE, summary: '', lastActive: new Date('2026-02-26T09:55:00.000Z'), @@ -124,16 +127,40 @@ describe('agent command', () => { expect(ui.table).toHaveBeenCalled(); const tableArg: any = (ui.table as any).mock.calls[0][0]; - expect(tableArg.rows[0][1]).toContain('wait'); - expect(tableArg.rows[0][3]).toBe('just now'); + expect(tableArg.headers).toEqual(['Agent', 'Type', 'Status', 'Working On', 'Active']); + expect(tableArg.rows[0][1]).toBe('Claude Code'); + expect(tableArg.rows[1][1]).toBe('Codex'); + expect(tableArg.rows[0][2]).toContain('wait'); + expect(tableArg.rows[0][4]).toBe('just now'); expect(ui.warning).toHaveBeenCalledWith('1 agent(s) waiting for input.'); }); + it('formats all agent types with human-friendly labels', async () => { + jest.spyOn(Date, 'now').mockReturnValue(new Date('2026-02-26T10:00:00.000Z').getTime()); + mockManager.listAgents.mockResolvedValue([ + { name: 'a', type: 'claude', status: AgentStatus.RUNNING, summary: '', lastActive: new Date('2026-02-26T10:00:00.000Z'), pid: 1 }, + { name: 'b', type: 'codex', status: AgentStatus.RUNNING, summary: '', lastActive: new Date('2026-02-26T10:00:00.000Z'), pid: 2 }, + { name: 'c', type: 'gemini_cli', status: AgentStatus.RUNNING, summary: '', lastActive: new Date('2026-02-26T10:00:00.000Z'), pid: 3 }, + { name: 'd', type: 'other', status: AgentStatus.RUNNING, summary: '', lastActive: new Date('2026-02-26T10:00:00.000Z'), pid: 4 }, + ]); + + const program = new Command(); + registerAgentCommand(program); + await program.parseAsync(['node', 'test', 'agent', 'list']); + + const tableArg: any = (ui.table as any).mock.calls[0][0]; + expect(tableArg.rows[0][1]).toBe('Claude Code'); + expect(tableArg.rows[1][1]).toBe('Codex'); + expect(tableArg.rows[2][1]).toBe('Gemini CLI'); + expect(tableArg.rows[3][1]).toBe('Other'); + }); + it('truncates working-on text to first line', async () => { jest.spyOn(Date, 'now').mockReturnValue(new Date('2026-02-26T10:00:00.000Z').getTime()); mockManager.listAgents.mockResolvedValue([ { name: 'repo-a', + type: 'claude', status: AgentStatus.RUNNING, summary: `Investigating parser bug Waiting on user input`, @@ -147,7 +174,7 @@ Waiting on user input`, await program.parseAsync(['node', 'test', 'agent', 'list']); const tableArg: any = (ui.table as any).mock.calls[0][0]; - expect(tableArg.rows[0][2]).toBe('Investigating parser bug'); + expect(tableArg.rows[0][3]).toBe('Investigating parser bug'); }); it('shows available agents when open target is not found', async () => { diff --git a/packages/cli/src/commands/agent.ts b/packages/cli/src/commands/agent.ts index 359ccb1..cd82165 100644 --- a/packages/cli/src/commands/agent.ts +++ b/packages/cli/src/commands/agent.ts @@ -9,6 +9,7 @@ import { TerminalFocusManager, TtyWriter, type AgentInfo, + type AgentType, } from '@ai-devkit/agent-manager'; import { ui } from '../util/terminal-ui'; @@ -38,6 +39,17 @@ function formatRelativeTime(timestamp: Date): string { return `${diffDays}d ago`; } +const TYPE_LABELS: Record = { + claude: 'Claude Code', + codex: 'Codex', + gemini_cli: 'Gemini CLI', + other: 'Other', +}; + +function formatType(type: AgentType): string { + return TYPE_LABELS[type] ?? type; +} + function formatWorkOn(summary?: string): string { const firstLine = (summary ?? '').split(/\r?\n/, 1)[0] || ''; return firstLine || 'No active task'; @@ -77,23 +89,19 @@ export function registerAgentCommand(program: Command): void { const rows = agents.map(agent => [ agent.name, + formatType(agent.type), formatStatus(agent.status), formatWorkOn(agent.summary), formatRelativeTime(agent.lastActive) ]); ui.table({ - headers: ['Agent', 'Status', 'Working On', 'Active'], + headers: ['Agent', 'Type', 'Status', 'Working On', 'Active'], rows: rows, - // Custom column styling - // 0: Name (cyan) - // 1: Status (dynamic based on content) - // 2: Working On (standard) - // 3: Active (dim) columnStyles: [ (text) => chalk.cyan(text), + (text) => chalk.dim(text), (text) => { - // Extract status keyword to determine color if (text.includes(STATUS_DISPLAY[AgentStatus.RUNNING].label)) return chalk.green(text); if (text.includes(STATUS_DISPLAY[AgentStatus.WAITING].label)) return chalk.yellow(text); if (text.includes(STATUS_DISPLAY[AgentStatus.IDLE].label)) return chalk.dim(text);