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
122 changes: 122 additions & 0 deletions docs/ai/design/2026-05-28-feature-agent-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
phase: design
title: System Design & Architecture
description: Define the technical architecture, components, and data models
---

# System Design & Architecture

## Architecture Overview

```mermaid
graph TD
CLI["agent console (commands/agent.ts)"]
CLI --> ConsoleApp

ConsoleApp --> ConsoleProvider
ConsoleProvider --> useAgentList
useAgentList --> AgentManager

ConsoleApp --> ConsoleAppShell
ConsoleAppShell --> HeaderBar
ConsoleAppShell --> AgentListPane
ConsoleAppShell --> PreviewSection
ConsoleAppShell --> StatusFooter
ConsoleAppShell --> ChatInput

PreviewSection --> useAgentConversation
useAgentConversation --> conversationCache["LRU cache (module-level)"]
useAgentConversation --> AgentAdapter["AgentAdapter.getConversation()"]

ConsoleAppShell --> runAction
runAction -->|subprocess| CLIAgentOpen["agent open <name>"]
runAction -->|subprocess| CLIAgentSend["agent send <msg> --id <name>"]
```

**Key architectural decisions:**
- All keyboard handling (`useInput`) centralised in `ConsoleAppShell` (non-memo) — Ink 7 + React 19 silently drops `useInput` inside `React.memo` components
- Actions dispatch via `spawn()` re-invoking the CLI with `stdio: pipe` so the TUI never yields the terminal
- Context value stabilised with `useMemo` so quiet polls don't re-render all consumers

## Data Models

**AgentInfo** (from `@ai-devkit/agent-manager`)
```typescript
{ name, type, status, projectPath, summary, lastActive, sessionFilePath }
```

**ConversationMessage**
```typescript
{ role: 'user' | 'assistant' | 'system', content: string, timestamp?: string }
```

**ConsoleContextValue**
```typescript
{ agents, error, lastUpdated, isLoading, manager, inputFocused }
```

**CacheEntry** (module-level LRU, max 50)
```typescript
{ mtime: number, messages: ConversationMessage[] }
```

## Component Breakdown

| Component | File | Responsibility |
|-----------|------|----------------|
| `ConsoleApp` | `ConsoleApp.tsx` | Context provider wrapper |
| `ConsoleAppShell` | `ConsoleApp.tsx` | All state, keyboard handling, layout math |
| `HeaderBar` | `HeaderBar.tsx` | Agent count + app label |
| `AgentListPane` | `AgentListPane.tsx` | 2-line agent rows with status/name/type/summary |
| `PreviewSection` | `PreviewSection.tsx` | Runs `useAgentConversation`, wraps `PreviewPane` |
| `PreviewPane` | `PreviewPane.tsx` | Renders last N messages with role/timestamp |
| `StatusFooter` | `StatusFooter.tsx` | Status counts + updated time + keybinding hints |
| `ChatInput` | `ChatInput.tsx` | Controlled text input for sending messages |
| `FormatStatus` | `render/formatStatus.tsx` | Status glyph + label |
| `ConsoleProvider` | `state/ConsoleContext.tsx` | Provides agent list via context |
| `useAgentList` | `hooks/useAgentList.ts` | Polls `manager.listAgents()` every 3s |
| `useAgentConversation` | `hooks/useAgentConversation.ts` | Polls conversation with debounce + LRU cache |
| `useTerminalSize` | `hooks/useTerminalSize.ts` | Debounced terminal resize listener |
| `runAction` | `actions/runAction.ts` | Spawns CLI subprocess for open/send |
| `computeLayout` | `ConsoleApp.tsx` | Pure function: cols/rows → layout dimensions |

## Layout Design

```
┌─ ai-devkit · agent console 3 agents ──────────────────────────────┐
├──────────────────────┬─────────────────────────────────────────────┤
│ AGENTS (3) │ PREVIEW · jarvis · claude · 2m ago · ~/code │
│ ● run jarvis claude│ user: │
│ ~/projects/jarvis │ can you fix the login bug? │
│ ─────────────────────│ assistant: │
│ ◐ wait titan codex │ Sure, looking at auth.ts now… │
│ ~/projects/titan │─────────────────────────────────────────────│
│ ─────────────────────│ ╭─────────────────────────────────────────╮ │
│ ○ idle scout gemini│ │ > press i to type a message │ │
│ ~/projects/scout │ ╰─────────────────────────────────────────╯ │
├──────────────────────┴─────────────────────────────────────────────┤
│ 1 run · 1 wait · 1 idle · updated 2s ago · j/k · o · i · q │
└────────────────────────────────────────────────────────────────────┘
```

- Narrow mode (< 120 cols): only left pane shown, preview hidden
- Left pane fixed at 48 cols; right column fills remaining space

## Design Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Keyboard handler location | Single non-memo `ConsoleAppShell` | Ink 7 + React 19: `useInput` silently fails in `React.memo` |
| Action execution | Subprocess re-invoking CLI | TUI stays alive; no terminal handoff complexity |
| Conversation data | Sync `statSync` + JSONL parse | Session files are local; async adds race complexity without benefit |
| Cache | Module-level LRU Map (max 50) | Survives selection changes; evicts old entries; no library needed |
| Context stabilisation | `useMemo` on context value | Quiet polls (`setState` returns `prev`) don't re-render consumers |
| Poll interval | 3s for both list and conversation | Balances freshness vs CPU; paused during input focus |
| `inFlightRef` reset | At top of each effect run | Prevents blocked fetch after dependency change |

## Non-Functional Requirements

- Layout must not shift when selecting a different agent (all boxes use fixed widths + `flexShrink={0}`)
- Conversation cache must not grow unbounded (LRU eviction at 50 entries)
- Actions must not unmount the TUI (subprocess with `stdio: pipe`)
- Terminal resize must be debounced (80ms) to avoid render storms
71 changes: 71 additions & 0 deletions docs/ai/implementation/2026-05-28-feature-agent-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
phase: implementation
title: Implementation Guide
description: Technical implementation notes, patterns, and code guidelines
---

# Implementation Guide

## Code Structure

```
packages/cli/src/
├── commands/agent.ts # CLI command registration; renders ConsoleApp
└── tui/console/
├── ConsoleApp.tsx # ConsoleProvider + ConsoleAppShell (all state + keyboard)
├── AgentListPane.tsx # 2-line agent rows
├── PreviewPane.tsx # Last-N message renderer
├── PreviewSection.tsx # Runs useAgentConversation, wraps PreviewPane
├── StatusFooter.tsx # Status counts + keybinding hints
├── ChatInput.tsx # Controlled text input
├── HeaderBar.tsx # App label + agent count
├── actions/
│ ├── runAction.ts # Subprocess dispatcher
│ └── types.ts # ConsoleAction discriminated union
├── hooks/
│ ├── useAgentList.ts # 3s poll for agent list
│ ├── useAgentConversation.ts # 3s poll for conversation + LRU cache
│ └── useTerminalSize.ts # Debounced terminal resize
├── render/
│ ├── formatStatus.tsx # FormatStatus component
│ ├── formatRelative.ts # Shared relative-time formatter
│ └── agentTypeLabel.ts # AGENT_TYPE_LABEL / AGENT_TYPE_LABEL_DISPLAY
└── state/
└── ConsoleContext.tsx # ConsoleProvider + useConsoleContext
```

## Key Implementation Notes

### Ink 7 + React 19 keyboard handling
`useInput` silently fails inside `React.memo` components. All keyboard handling lives in `ConsoleAppShell` (non-memo). Refs (`selectedNameRef`, `agentsRef`) capture current values for use inside `useInput` closures without stale closure bugs.

### Layout stability
Every `<Box>` has explicit `width` + `flexShrink={0}`. Without this, Yoga recalculates and shifts layout on every selection change. `computeLayout()` is a pure function — easy to verify and test independently.

### Conversation cache
`conversationCache` is a module-level `Map<sessionFilePath, {mtime, messages}>` with LRU eviction via `cacheSet()` (max 50). On selection change, cached messages are shown immediately while the debounced fetch checks `statSync().mtimeMs`. If mtime matches cache, no JSONL parse occurs.

### Action dispatch
`runAction` resolves the CLI entry as `process.execPath + process.execArgv + process.argv[1]` — works in both dev (tsx/ts-node) and production. Subprocess uses `stdio: ['ignore', 'pipe', 'pipe']` so it never seizes the TUI terminal.

### Quiet poll optimization
Both hooks use `setState(prev => prev)` return to skip re-renders on unchanged data:
- `useAgentList`: `agentsEqual()` compares all fields; uses `Date.parse()` not `new Date()` to avoid GC pressure
- `useAgentConversation`: `messagesEqual()` compares role + content + timestamp

### Context stability
`ConsoleContext` wraps the value in `useMemo([list, manager, inputFocused])`. Since `useAgentList` returns `prev` when nothing changed, `list` is a stable reference across quiet polls — the `useMemo` dependency doesn't trigger, so consumers don't re-render.

## Error Handling

- `useAgentList` catches `listAgents()` errors and surfaces them via `error` in context; `AgentListPane` shows the error message
- `useAgentConversation` handles: no session file, no adapter, JSONL parse error — each shown in `PreviewPane`
- `runAction` captures stderr from subprocess; resolves with `{ exitCode, error }` never rejects
- All `setState` calls in async hooks guard with `mountedRef.current && token === runTokenRef.current`

## Performance Considerations

- Poll paused during input focus (`paused: inputFocused`) to reduce competing re-renders
- Terminal resize debounced at 80ms
- `React.memo` on all leaf components (`AgentListPane`, `PreviewPane`, `StatusFooter`, `ChatInput`, `FormatStatus`, `HeaderBar`)
- `inFlightRef` prevents concurrent `listAgents()` calls when interval fires before previous fetch completes; reset at start of each effect run to prevent blocked fetch after dependency change
53 changes: 53 additions & 0 deletions docs/ai/planning/2026-05-28-feature-agent-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
phase: planning
title: Project Planning & Task Breakdown
description: Break down work into actionable tasks and estimate timeline
---

# Project Planning & Task Breakdown

## Task Breakdown

### Phase 1: Core Shell & Data Hooks
- [x] Task 1.1: `useAgentList` hook — polls `manager.listAgents()` every 3s, equality-checks to skip quiet re-renders, guards stale setState with run token
- [x] Task 1.2: `ConsoleProvider` / `ConsoleContext` — provides agent list + manager via context; `useMemo` on value object
- [x] Task 1.3: `useTerminalSize` — debounced resize listener on `process.stdout`
- [x] Task 1.4: `ConsoleApp` shell — `ConsoleProvider` wrapper + `ConsoleAppShell` with all keyboard handling via `useInput`

### Phase 2: Agent List Pane
- [x] Task 2.1: `AgentListPane` — 2-line rows (status+name+type / summary), fixed widths to prevent layout shift, dividers between agents
- [x] Task 2.2: `FormatStatus` — status glyph + label with colour coding
- [x] Task 2.3: Sort agents by status (WAITING → RUNNING → IDLE → UNKNOWN) via `sortBy: 'status'`

### Phase 3: Conversation Preview
- [x] Task 3.1: `useAgentConversation` hook — polls every 3s, 150ms selection debounce, LRU module-level cache (max 50 entries), run-token race guard
- [x] Task 3.2: `PreviewPane` — renders last 20 messages with role colour + timestamp; each line wrapped in `<Box>` to guarantee row breaks in Ink 7
- [x] Task 3.3: `PreviewSection` — reads context + runs `useAgentConversation`, paused during input focus

### Phase 4: Chat Input & Actions
- [x] Task 4.1: `ChatInput` — fully controlled (value/onChange lifted to `ConsoleAppShell`); dynamic line-count reporting for layout
- [x] Task 4.2: `runAction` — spawns CLI subprocess (`agent open` / `agent send`) with `stdio: pipe`; resolves via `process.execPath + execArgv + argv[1]`
- [x] Task 4.3: Transient feedback messages — 4s auto-clear; shown in `StatusFooter`

### Phase 5: Header, Footer & Layout
- [x] Task 5.1: `HeaderBar` — agent count + app label
- [x] Task 5.2: `StatusFooter` — status counts, updated time, keybinding hints
- [x] Task 5.3: `computeLayout` — pure function mapping cols/rows/inputLines → all layout dimensions
- [x] Task 5.4: Narrow mode — hides preview pane when terminal < 120 cols; shows resize hint in footer

## Dependencies

- `@ai-devkit/agent-manager` — `AgentManager`, `AgentInfo`, `ConversationMessage`, `AgentStatus`
- `ink` 7.x — TUI rendering; `useInput`, `useApp`, `Box`, `Text`
- `ink-text-input` — controlled text input component
- `react` 19.x

## Risks & Mitigation

| Risk | Mitigation |
|------|-----------|
| `useInput` silent failure in memo components | All keyboard handling in single non-memo `ConsoleAppShell` |
| Layout shift on selection change | Fixed widths + `flexShrink={0}` on all boxes |
| Unbounded conversation cache | LRU eviction at 50 entries via `cacheSet()` |
| Subprocess blocking TUI terminal | `stdio: ['ignore', 'pipe', 'pipe']` |
| Stale fetch after effect re-run | `inFlightRef.current = false` reset at start of each effect |
47 changes: 47 additions & 0 deletions docs/ai/requirements/2026-05-28-feature-agent-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
phase: requirements
title: Requirements & Problem Understanding
description: Clarify the problem space, gather requirements, and define success criteria
---

# Requirements & Problem Understanding

## Problem Statement

Developers running multiple AI agents (Claude Code, Codex, Gemini CLI, OpenCode) have no unified view of what those agents are doing. They must switch terminal windows to check status, find logs, or send messages. This creates context-switching overhead and makes it easy to miss stuck or waiting agents.

## Goals & Objectives

**Primary goals**
- Provide a real-time TUI dashboard (`agent console`) that lists all detected running agents with live status
- Allow the user to inspect an agent's recent conversation without leaving the console
- Allow the user to send a message to a selected agent from within the TUI
- Allow the user to open a selected agent's terminal session

**Non-goals**
- Full conversation history (preview shows last 20 messages only)
- Multi-agent message broadcast
- Managing agent lifecycle (start/stop)

## User Stories & Use Cases

- As a developer, I want to see all my agents' statuses at a glance so I can detect stuck or idle agents quickly
- As a developer, I want to read the last few messages of any agent's conversation without switching windows
- As a developer, I want to send a message to an agent from the console so I can unblock it without interrupting my current context
- As a developer, I want to open an agent's full terminal UI from the console

## Success Criteria

- `agent console` renders within 1s of launch
- Agent list refreshes every 3s without noticeable layout shifts
- Keyboard navigation (j/k/o/i/q) works immediately on launch
- Conversation preview updates every 3s; fast cursor movement does not cause excessive I/O
- Action feedback (open/send) is displayed without unmounting the TUI
- Works in terminals ≥ 80 cols; preview pane shown when terminal ≥ 120 cols

## Constraints & Assumptions

- Terminal must support at least 80 columns and 24 rows for usable layout
- Ink 7 + React 19 ESM — `useInput` must live in a single non-memo component to avoid silent failures
- Agent session files are JSONL on disk; parsing happens synchronously per selection
- Actions (open/send) are dispatched by re-invoking the CLI as a subprocess so the TUI stays alive
89 changes: 89 additions & 0 deletions docs/ai/testing/2026-05-28-feature-agent-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
phase: testing
title: Testing Strategy & Coverage
description: Test coverage plan, test file locations, and results
---

# Testing Strategy & Coverage

## Scope

React components and hooks cannot be tested without `@testing-library/react` or `ink-testing-library` (not available in this project). Coverage targets all pure TypeScript logic: layout calculation, equality checks, LRU cache, time formatting, and subprocess dispatch.

## Test Files

| File | Covers | Tests |
|------|--------|-------|
| `src/__tests__/tui/console/computeLayout.test.ts` | `computeLayout()` in `ConsoleApp.tsx` | 10 |
| `src/__tests__/tui/console/render/formatRelative.test.ts` | `render/formatRelative.ts` | 8 |
| `src/__tests__/tui/console/hooks/conversationCache.test.ts` | `cacheSet`, `conversationCache`, `messagesEqual` | 11 |
| `src/__tests__/tui/console/hooks/agentsEqual.test.ts` | `agentsEqual` in `useAgentList.ts` | 11 |
| `src/__tests__/tui/console/actions/runAction.test.ts` | `runAction.ts` | 7 |

**Total new tests: 47** | **All passing**

## What Each Suite Validates

### `computeLayout`
- Fixed 48-col list pane in wide mode
- Right column fills remaining width minus separator
- Narrow mode uses `cols − 2` for list pane
- `contentHeight` never drops below `MIN_CONTENT_HEIGHT` (12) even on tiny terminals
- `inputBoxHeight` scales with `inputLines`
- `rightColWidth` clamped to 20 minimum; `inputInnerWidth` clamped to 4 minimum

### `formatRelative`
- All time buckets: now (< 5s), seconds, minutes, hours, days
- Future timestamps clamped to "now"
- String and Date inputs both accepted
- `undefined` returns `"—"`

### LRU Cache
- Store and retrieve
- Re-insert moves key to most-recent position (LRU refresh)
- Oldest key evicted when size hits `CACHE_MAX` (50)
- Never exceeds `CACHE_MAX` under sustained inserts

### `messagesEqual`
- Empty arrays equal
- Length mismatch → false
- role / content / timestamp field comparison
- Undefined timestamps handled

### `agentsEqual`
- Empty arrays equal
- Field-level comparison: name, status, type, summary, sessionFilePath, lastActive
- String `lastActive` compared correctly against `Date` (via `Date.parse()`)
- Order-sensitive multi-agent comparison

### `runAction`
- Success: exitCode 0, no error
- Non-zero exit + stderr → error string captured
- Non-zero exit + empty stderr → error undefined
- Spawn error (`ENOENT`) → exitCode null + error message
- `open` action argv shape: `['agent', 'open', '<name>']`
- `send` action argv shape: `['agent', 'send', '<msg>', '--id', '<name>']`
- `stdio: ['ignore', 'pipe', 'pipe']` verified (TUI terminal not seized)

## Coverage Notes

**Not covered by automated tests** (require ink-testing-library or manual QA):
- React component rendering: `AgentListPane`, `PreviewPane`, `StatusFooter`, `ChatInput`, `HeaderBar`
- Hook behaviour: `useAgentList`, `useAgentConversation`, `useTerminalSize`
- Keyboard navigation: j/k, o, i, q in `ConsoleAppShell`
- Narrow/wide layout transition on terminal resize

**Recommended manual QA scenarios:**
1. Rapid j/k navigation — verify 150ms debounce prevents excessive `statSync` calls
2. Resize terminal from < 120 cols to ≥ 120 cols — preview pane appears
3. Delete an agent mid-session — list resets to first agent without crash
4. Send message to agent — transient "Message sent" appears for 4s
5. Open action with bad agent name — transient error shown

## Results

```
Test Files 41 passed (41)
Tests 621 passed (621) ← includes 47 new agent-console tests
Duration 2.65s
```
Loading
Loading