Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/ai/design/feature-agent-list-type.md
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 35 additions & 0 deletions docs/ai/implementation/feature-agent-list-type.md
Original file line number Diff line number Diff line change
@@ -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<AgentType, string> = {
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)
35 changes: 35 additions & 0 deletions docs/ai/planning/feature-agent-list-type.md
Original file line number Diff line number Diff line change
@@ -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.
44 changes: 44 additions & 0 deletions docs/ai/requirements/feature-agent-list-type.md
Original file line number Diff line number Diff line change
@@ -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.
29 changes: 29 additions & 0 deletions docs/ai/testing/feature-agent-list-type.md
Original file line number Diff line number Diff line change
@@ -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
33 changes: 30 additions & 3 deletions packages/cli/src/__tests__/commands/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('agent command', () => {
const agents = [
{
name: 'repo-a',
type: 'claude',
status: AgentStatus.RUNNING,
summary: 'Working',
lastActive: now,
Expand Down Expand Up @@ -104,13 +105,15 @@ 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'),
pid: 100,
},
{
name: 'repo-b',
type: 'codex',
status: AgentStatus.IDLE,
summary: '',
lastActive: new Date('2026-02-26T09:55:00.000Z'),
Expand All @@ -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`,
Expand All @@ -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 () => {
Expand Down
22 changes: 15 additions & 7 deletions packages/cli/src/commands/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TerminalFocusManager,
TtyWriter,
type AgentInfo,
type AgentType,
} from '@ai-devkit/agent-manager';
import { ui } from '../util/terminal-ui';

Expand Down Expand Up @@ -38,6 +39,17 @@ function formatRelativeTime(timestamp: Date): string {
return `${diffDays}d ago`;
}

const TYPE_LABELS: Record<AgentType, string> = {
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';
Expand Down Expand Up @@ -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);
Expand Down