# Implementation Plan: pi-subagents UI/UX Overhaul + Live Session TUI

## Reference Sources

This plan is based on analysis of:
- **Claude Code reference**: `/Users/rhinesharar/claude-code-main/src/components/agents/` — full Ink/React agent management TUI
- **pi-subagents current**: `/Users/rhinesharar/pi-subagents/src/` — existing extension code
- **pi-tui API**: `@mariozechner/pi-tui` — Component, TUI, Input, Box, Text, SelectList
- **pi-coding-agent API**: `@mariozechner/pi-coding-agent` — ExtensionAPI, ExtensionCommandContext, AgentSession

The full spec is at `SPEC-LIVE-SESSION-TUI.md`. This document is the actionable implementation guide.

---

## Phase 1: Foundation (model picker, tool picker, model resolver)

### 1.1 `src/model-resolver.ts` — Add model scanning

**Goal**: Read `~/.pi/agent/models.json` and produce structured model data for the picker.

```typescript
export interface ModelEntry {
  id: string;          // "nvidia:glm-4.7"
  name: string;        // "nvidia:glm-4.7 (CCS)"
  provider: string;    // "ccs-proxy"
  contextWindow: number;
  reasoning: boolean;
  inputTypes: string[];
}

export interface ModelPickerSection {
  label: string;       // Provider name as header
  models: ModelPickerItem[];
}

export interface ModelPickerItem {
  value: string;       // "provider/modelId" — stored in agent config
  label: string;       // Human-readable name
  description: string; // "256k ctx · reasoning" style summary
}

export function scanModelsConfig(): Map<string, ModelEntry[]>;
  // Reads ~/.pi/agent/models.json
  // Returns Map<providerName, ModelEntry[]>
  // Handles: missing file → empty map, parse error → empty map + console.warn

export function getModelPickerSections(): ModelPickerSection[];
  // Builds sections from scanModelsConfig()
  // Prepends "inherit" option
  // Appends "Custom..." option
  // Groups by provider with provider name as section header

export function getModelLabel(modelId: string): string;
  // Resolves "provider/modelId" → human name from models.json
  // Fallback: returns modelId as-is
```

**Reference**: Claude Code's `src/utils/model/agent.ts` — `getAgentModelOptions()` returns [
  { value: 'sonnet', label: 'Sonnet', description: '...' },
  { value: 'opus', ... }, { value: 'haiku', ... }, { value: 'inherit', ... }
]

**Our version** scans actual configured models instead of hardcoded aliases.

### 1.2 `src/ui/model-picker.ts` — Model picker dialog

**Goal**: Interactive model selection showing all available models from models.json.

```typescript
export async function showModelPicker(
  ctx: ExtensionCommandContext,
  initialModel?: string
): Promise<string | undefined>;
  // Returns "provider/modelId" string, "inherit", or undefined (cancelled)
```

**Behavior:**
1. Call `getModelPickerSections()` to get available models grouped by provider
2. Show a series of selects: first provider section, then model within section
3. Alternative: flatten all models into one select with `(provider)` prefix for distinction

**Option A (recommended) — Flattened with provider prefix:**
```typescript
const options = [
  "inherit (parent model)",
  "─── ccs-proxy ───",  // non-selectable header
  "  nvidia:glm-4.7 (CCS) — 256k ctx · reasoning",
  "  nvidia:kimi-k2-thinking (CCS) — 256k ctx · reasoning",
  "─── ollama ───",
  "  kimi-k2.6:cloud — 256k ctx · reasoning",
  "  deepseek-v4-flash:cloud — 1M ctx · reasoning",
  ...
  "─── Custom ───",     // non-selectable header
  "  Custom... (type provider/modelId)"
]
```

**Reference**: Claude Code's `ModelSelector.tsx` uses `Select` with `getAgentModelOptions()`.

### 1.3 `src/ui/tool-picker.ts` — Categorized tool selector

**Goal**: Show tool categories (Read-only, Edit, Execution, MCP, Other) with batch toggle.

```typescript
export async function showToolPicker(
  ctx: ExtensionCommandContext,
  currentTools?: string[]
): Promise<string[] | undefined>;
  // Returns tool name array, undefined for "all tools", or null for cancelled
```

**Behavior:**
- First show five options: "All tools", "Read-only only", "Edit only", "Read + Edit", "Custom..."
- "All tools" → return undefined (omit tools field → agent gets all)
- "Custom..." → show categorized tool list with toggle selection

**Category mapping:**
| Category | Tools |
|----------|-------|
| Read-only | read, grep, find, ls |
| Edit | edit, write |
| Execution | bash |
| MCP | (dynamic, from current session) |
| Other | Anything not in above categories |

**Reference**: Claude Code's `ToolSelector.tsx` uses categorized buckets with checkbox UI.

---

## Phase 2: Agent Management UI

### 2.1 `src/ui/agent-list.ts` — Rich agent list

```typescript
export async function showAgentList(ctx: ExtensionCommandContext): Promise<void>;
```

**Behavior:**
1. Get `getAllTypes()` with `getAgentConfig()` for each
2. Group into sections: "Built-in", "Project (.pi/agents/)", "Personal (~/.pi/agent/agents/)"
3. Format each entry as: `name · model` with source indicators
4. On select → call `showAgentDetail(ctx, name)`
5. Source indicator: `•` project, `◦` global, `✕` disabled

**Reference**: Claude Code's `AgentsList.tsx` — groups by source, shows model, handles overrides.

### 2.2 `src/ui/agent-detail.ts` — Agent detail card

```typescript
export async function showAgentDetail(
  ctx: ExtensionCommandContext,
  agentName: string
): Promise<void>;
```

**Behavior:**
1. Show agent info as a notification (or a sequence of selects if info is long)
2. Then show action menu:
   - For custom agents: `["Edit", "Disable", "Delete", "Back"]`
   - For built-in (no override): `["Eject (export as .md)", "Disable", "Back"]`
   - For built-in (with override): `["Edit", "Disable", "Reset to default", "Delete", "Back"]`
3. Reuse existing `findAgentFile`, `ejectAgent`, `disableAgent`, `enableAgent` from index.ts

**Info to display:**
```
Agent: code-reviewer
  Source: Project (.pi/agents/)
  Model: inherit (parent model)
  Tools: read, grep, find, ls
  Prompt: replace
  Memory: none
  Thinking: inherit
  Max turns: unlimited
  Extensions: inherit all
  Skills: inherit all
```

**Reference**: Claude Code's `AgentDetail.tsx` — full card with all config fields.

### 2.3 `src/ui/agent-editor.ts` — Inline editing

```typescript
export async function showAgentEditor(
  ctx: ExtensionCommandContext,
  agentName: string
): Promise<void>;
```

**Behavior:**
1. Show edit menu: `["Open in editor", "Edit tools", "Edit model", "Edit description", "Edit system prompt", "Back"]`
2. "Open in editor" → `ctx.ui.editor()` with the file content
3. "Edit tools" → `showToolPicker()`, then write updated file
4. "Edit model" → `showModelPicker()`, then write updated file
5. "Edit description" → `ctx.ui.input()`, then update frontmatter
6. "Edit system prompt" → `ctx.ui.editor()`, update body

**Reference**: Claude Code's `AgentEditor.tsx` — sub-modes for tools/model/color editing.

### 2.4 `src/ui/create-wizard.ts` — Multi-step creation wizard

```typescript
export async function showCreateWizard(ctx: ExtensionCommandContext): Promise<void>;
```

**Steps:**
1. **Method**: `["AI-generated — Describe and let AI craft the agent", "Manual — Step by step"]`
2a. **Generate path**: Input description → Input name → Spawn general-purpose agent to write file → Done
2b. **Manual path**: Name → Description → Tools (tool-picker) → Model (model-picker) → Thinking (select) → Extensions (select) → Skills (select) → Memory (select) → System prompt (editor)
3. **Location**: `["Project (.pi/agents/)", "Personal (~/.pi/agent/agents/)"]`
4. **Write file** and `reloadCustomAgents()`

**Reference**: Claude Code's `CreateAgentWizard.tsx` with 12 wizard steps.

### 2.5 `src/ui/agent-menu.ts` — Command hub

```typescript
export async function showAgentsMenu(
  ctx: ExtensionCommandContext,
  manager: AgentManager,
  scheduler: SubagentScheduler
): Promise<void>;
```

**Menu options:**
```
Agent types (N)
Running agents (N)
Scheduled jobs (N)
Create new agent
Settings
```

**Reference**: Current `_showAgentsMenu()` in index.ts — refactor into this module.

---

## Phase 3: Session Event Infrastructure

### 3.1 `src/session-events.ts` — Event types + stream parser

```typescript
export type AgentSessionEvent =
  | { type: 'token'; text: string }
  | { type: 'tool_start'; toolName: string; args: any; timestamp: number }
  | { type: 'tool_end'; toolName: string; output: string; timestamp: number }
  | { type: 'turn_end'; turnNumber: number }
  | { type: 'status'; status: AgentStatus }
  | { type: 'usage'; input: number; output: number; total: number }
  | { type: 'error'; error: Error }
  | { type: 'message'; message: any };

export class SessionEventStream {
  constructor(session: AgentSession, record: AgentRecord);
  
  subscribe(callback: (event: AgentSessionEvent) => void): () => void;
  // Returns unsubscribe function
  
  inject(text: string): void;
  // Inject a user message into the session
  
  interrupt(): void;
  // Gracefully interrupt the agent
  
  close(): void;
  // Clean up subscriptions
}
```

**How the stream parser works:**
The parser monitors the session's message list via the existing `session.subscribe()` callback. On each tick:
1. Compare current messages vs previous snapshot
2. Identify new or changed messages
3. Emit appropriate events:
   - New assistant text since last check → `token` events
   - New tool_use blocks → `tool_start` events
   - Completed tool_result blocks → `tool_end` events
   - New turn boundary → `turn_end` event

This avoids modifying AgentSession internals. All events are derived from the existing message subscription.

---

## Phase 4: Live Session TUI Components

### 4.1 `src/ui/session-stream-pane.ts` — Streaming token renderer

```typescript
export class SessionStreamPane implements Component {
  constructor(events: SessionEventStream, theme: Theme);
  
  render(width: number): string[];
  handleInput(data: string): void;
  invalidate(): void;
}
```

**Rendering:**
- Builds lines from accumulated messages
- Word-wraps to `width`
- Applies color: user=accent, assistant=normal, tool=muted, error=error
- Handles scroll offset for scrollback

**Reference**: `conversation-viewer.ts` — `buildContentLines()` method.

### 4.2 `src/ui/session-tool-calls.ts` — Tool call visualization

```typescript
export class SessionToolCalls implements Component {
  constructor(events: SessionEventStream, theme: Theme);
  
  render(width: number): string[];
  handleInput(data: string): void;
  invalidate(): void;
}
```

**Rendering:**
- Shows active tool calls as bordered boxes
- Shows completed tool calls dimmed
- Shows tool output truncated to fit

```
╭─ grep ─────────────────────────────────╮
│ searching for "catch" in src/*.ts       │
│ Found 7 occurrences                     │
╰─────────────────────────────────────────╯
```

### 4.3 `src/ui/session-status-bar.ts` — Status bar

```typescript
export class SessionStatusBar implements Component {
  constructor(events: SessionEventStream, record: AgentRecord, theme: Theme);
  
  render(width: number): string[];
  handleInput(data: string): void;
  invalidate(): void;
}
```

**Rendering:**
```
◉ code-reviewer · reviewing src/agent-manager.ts  42s · 3 tools · 2 turns · 12.4k tokens
```

- Animated spinner (frames from `SPINNER` array)
- Agent type + description (truncated)
- Elapsed time (auto-updating via interval)
- Tool use count
- Turn count / max turns
- Token usage

### 4.4 `src/ui/session-input-harness.ts` — Input injection interface

```typescript
export class SessionInputHarness implements Component, Focusable {
  focused = false;
  
  constructor(events: SessionEventStream, theme: Theme);
  
  render(width: number): string[];
  handleInput(data: string): void;
  invalidate(): void;
}
```

**Rendering:**
```
═══════════════════════════════════════════════════════════
> _  (cursor blinks here)
Ctrl+C interrupt · Enter send · ↑↓ history · Esc close
```

**Behavior:**
- When focused, captures keyboard input
- Enter → send buffer as injection via `events.inject(text)`
- Ctrl+C → `events.interrupt()`
- Escape → defocus / close
- ↑↓ → input history navigation
- Visual indicator when agent is processing (show "● processing..." instead of input prompt)

### 4.5 `src/ui/live-session.ts` — Full live session TUI

```typescript
export async function showLiveSession(
  ctx: ExtensionCommandContext,
  session: AgentSession,
  record: AgentRecord,
  activity: AgentActivity | undefined,
): Promise<void>;
```

**Implementation:**
```typescript
export async function showLiveSession(
  ctx: ExtensionCommandContext,
  session: AgentSession,
  record: AgentRecord,
  activity: AgentActivity | undefined,
): Promise<void> {
  const events = new SessionEventStream(session, record);
  
  await ctx.ui.custom<undefined>(
    (tui, theme, _keybindings, done) => {
      const statusBar = new SessionStatusBar(events, record, theme);
      const streamPane = new SessionStreamPane(events, theme);
      const toolCalls = new SessionToolCalls(events, theme);
      const harness = new SessionInputHarness(events, theme);
      
      // Composite layout via Container or manual layout
      const layout = new SessionLayout(tui, {
        statusBar, streamPane, toolCalls, harness,
        done,
      });
      
      return layout;
    },
    {
      overlay: true,
      overlayOptions: { anchor: "center", width: "90%", maxHeight: "90%" },
    },
  );
  
  events.close();
}
```

**Layout management:**
The live session is rendered as a TUI overlay via `ctx.ui.custom()`. The custom component manages its own layout:

```
┌───────────────────────────────────────────────────────┐
│ SessionStatusBar (1 line, fixed height)                │
├───────────────────────────────────────────────────────┤
│ SessionStreamPane (remaining height - 3 for harness)   │
│ (scrollable, auto-scroll by default)                   │
│                                                        │
│ SessionToolCalls (interleaved inline if enabled)        │
│                                                        │
├═══════════════════════════════════════════════════════┤
│ SessionInputHarness (3 lines, fixed, bottom-pinned)    │
│ > _                                                   │
│ Ctrl+C interrupt · Enter send · Esc close              │
└───────────────────────────────────────────────────────┘
```

---

## Phase 5: Integration

### 5.1 Agent widget enhancement

Enhance `agent-widget.ts` to show more detail about running agents and provide keyboard shortcuts to open the live session view.

### 5.2 Index.ts changes

Replace inline menu functions with calls to the new UI modules:

```typescript
// Old: inline _showAgentsMenu
// New:
import { showAgentsMenu } from "./ui/agent-menu.js";

// In the command handler:
async function handleAgentsCommand(ctx: ExtensionCommandContext) {
  await showAgentsMenu(ctx, manager, scheduler);
}
```

### 5.3 Agent tool integration

When the Agent tool spawns a subagent and a session is available, the tool result could offer an interactive flag:

```
Agent "code-reviewer" completed. Use /agents to view session or get_subagent_result() for output.
```

For the live session view, when a user selects a running agent from the widget or agent list, open the live session overlay.

---

## Test Plan

### Unit tests
1. `model-resolver.ts` `scanModelsConfig()` with fixture models.json
2. `model-resolver.ts` `getModelPickerSections()` formatting
3. `session-events.ts` `SessionEventStream` with mock AgentSession

### Integration tests
1. Walk through full create wizard (generate path + manual path)
2. Edit agent tools → verify file written correctly
3. Edit agent model → verify `model:` field in frontmatter
4. List agents → verify source grouping and indicators
5. Live session overlay opens and closes correctly
6. Input harness injects message (test with echo-responder agent)

### Manual tests
1. Verify all existing `.pi/agents/*.md` files remain valid after changes
2. Verify model picker shows all providers from models.json
3. Verify tool picker categorizes correctly
4. Live session: verify streaming tokens render in real-time
5. Live session: verify scrollback works
6. Live session: verify input injection reaches the agent
