From fb9d4e5b9f2970fb1ef1a569ceebb1a92c0fda3b Mon Sep 17 00:00:00 2001 From: teernisse Date: Sat, 28 Feb 2026 00:48:55 -0500 Subject: [PATCH] chore(plans): update implementation plans plans/PLAN-tool-result-display.md: - Add comprehensive plan for displaying tool results inline in conversation view, including truncation strategies and expand/collapse UI patterns plans/subagent-visibility.md: - Mark completed phases and update remaining work items - Reflects current state of subagent tracking implementation Co-Authored-By: Claude Opus 4.5 --- plans/PLAN-tool-result-display.md | 456 ++++++++ plans/subagent-visibility.md | 1638 +---------------------------- 2 files changed, 485 insertions(+), 1609 deletions(-) create mode 100644 plans/PLAN-tool-result-display.md diff --git a/plans/PLAN-tool-result-display.md b/plans/PLAN-tool-result-display.md new file mode 100644 index 0000000..494dcdf --- /dev/null +++ b/plans/PLAN-tool-result-display.md @@ -0,0 +1,456 @@ +# Plan: Tool Result Display in AMC Dashboard + +> **Status:** Draft — awaiting review and mockup phase +> **Author:** Claude + Taylor +> **Created:** 2026-02-27 + +## Summary + +Add the ability to view tool call results (diffs, bash output, file contents) directly in the AMC dashboard conversation view. Currently, users see that a tool was called but cannot see what it did. This feature brings Claude Code's result visibility to the multi-agent dashboard. + +### Goals + +1. **See code changes as they happen** — diffs from Edit/Write tools always visible +2. **Debug agent behavior** — inspect Bash output, Read content, search results +3. **Match Claude Code UX** — familiar expand/collapse behavior with latest results expanded + +### Non-Goals (v1) + +- Codex agent support (different JSONL format — deferred to v2) +- Copy-to-clipboard functionality +- Virtual scrolling / performance optimization +- Editor integration (clicking paths to open files) + +--- + +## User Workflows + +### Workflow 1: Watching an Active Session + +1. User opens a session card showing an active Claude agent +2. Agent calls Edit tool to modify a file +3. User immediately sees the diff expanded below the tool call pill +4. Agent calls Bash to run tests +5. User sees bash output expanded, previous Edit diff stays expanded (it's a diff) +6. Agent sends a text message explaining results +7. Bash output collapses (new assistant message arrived), Edit diff stays expanded + +### Workflow 2: Reviewing a Completed Session + +1. User opens a completed session to review what the agent did +2. All tool calls are collapsed by default (no "latest" assistant message) +3. Exception: Edit/Write diffs are still expanded +4. User clicks a Bash tool call to see what command ran and its output +5. User clicks "Show full output" when output is truncated +6. Lightweight modal opens with full scrollable content +7. User closes modal and continues reviewing + +### Workflow 3: Debugging a Failed Tool Call + +1. Agent runs a Bash command that fails +2. Tool result block shows with red-tinted background +3. stderr content is visible, clearly marked as error +4. User can see what went wrong without leaving the dashboard + +--- + +## Acceptance Criteria + +### Display Behavior + +- **AC-1:** Tool calls render as expandable elements showing tool name and summary +- **AC-2:** Clicking a collapsed tool call expands to show its result +- **AC-3:** Clicking an expanded tool call collapses it +- **AC-4:** Tool results in the most recent assistant message are expanded by default +- **AC-5:** When a new assistant message arrives, previous tool results collapse +- **AC-6:** Edit and Write tool diffs remain expanded regardless of message age +- **AC-7:** Tool calls without results display as non-expandable with muted styling + +### Diff Rendering + +- **AC-8:** Edit/Write results display structuredPatch data as syntax-highlighted diff +- **AC-9:** Diff additions render with VS Code dark theme green background (rgba(46, 160, 67, 0.15)) +- **AC-10:** Diff deletions render with VS Code dark theme red background (rgba(248, 81, 73, 0.15)) +- **AC-11:** Full file path displays above each diff block +- **AC-12:** Diff context lines use structuredPatch as-is (no recomputation) + +### Other Tool Types + +- **AC-13:** Bash results display stdout in monospace, stderr separately if present +- **AC-14:** Read results display file content with syntax highlighting based on file extension +- **AC-15:** Grep/Glob results display file list with match counts +- **AC-16:** WebFetch results display URL and response summary + +### Truncation + +- **AC-17:** Long outputs truncate at thresholds matching Claude Code behavior +- **AC-18:** Truncated outputs show "Show full output (N lines)" link +- **AC-19:** Clicking "Show full output" opens a dedicated lightweight modal +- **AC-20:** Modal displays full content with syntax highlighting, scrollable + +### Error States + +- **AC-21:** Failed tool calls display with red-tinted background +- **AC-22:** Error content (stderr, error messages) is clearly distinguishable from success content +- **AC-23:** is_error flag from tool_result determines error state + +### API Contract + +- **AC-24:** /api/conversation response includes tool results nested in tool_calls +- **AC-25:** Each tool_call has: name, id, input, result (when available) +- **AC-26:** Result structure varies by tool type (documented in IMP-SERVER) + +--- + +## Architecture + +### Why Two-Pass JSONL Parsing + +The Claude Code JSONL stores tool_use and tool_result as separate entries linked by tool_use_id. To nest results inside tool_calls for the API response, the server must: + +1. First pass: Build a map of tool_use_id → toolUseResult +2. Second pass: Parse messages, attaching results to matching tool_calls + +This adds parsing overhead but keeps the API contract simple. Alternatives considered: +- **Streaming/incremental:** More complex, doesn't help since we need full conversation anyway +- **Client-side joining:** Shifts complexity to frontend, increases payload size + +### Why Render Everything, Not Virtual Scroll + +Sessions typically have 20-80 tool calls. Modern browsers handle hundreds of DOM elements efficiently. Virtual scrolling adds significant complexity (measuring, windowing, scroll position management) for marginal benefit. + +Decision: Ship simple, measure real-world performance, optimize if >100ms render times observed. + +### Why Dedicated Modal Over Inline Expansion + +Full output can be thousands of lines. Inline expansion would: +- Push other content out of view +- Make scrolling confusing +- Lose context of surrounding conversation + +A modal provides a focused reading experience without disrupting conversation layout. + +### Component Structure + +``` +MessageBubble +├── Content (text) +├── Thinking (existing) +└── ToolCallList (new) + └── ToolCallItem (repeated) + ├── Header (pill: chevron, name, summary, status) + └── ResultContent (conditional) + ├── DiffResult (for Edit/Write) + ├── BashResult (for Bash) + ├── FileListResult (for Glob/Grep) + └── GenericResult (fallback) + +FullOutputModal (new, top-level) +├── Header (tool name, file path) +├── Content (full output, scrollable) +└── CloseButton +``` + +--- + +## Implementation Specifications + +### IMP-SERVER: Parse and Attach Tool Results + +**Fulfills:** AC-24, AC-25, AC-26 + +**Location:** `amc_server/mixins/conversation.py` + +**Changes to `_parse_claude_conversation`:** + +Two-pass parsing: +1. First pass: Scan all entries, build map of `tool_use_id` → `toolUseResult` +2. Second pass: Parse messages as before, but when encountering `tool_use`, lookup and attach result + +**Tool call schema after change:** +```python +{ + "name": "Edit", + "id": "toolu_abc123", + "input": {"file_path": "...", "old_string": "...", "new_string": "..."}, + "result": { + "content": "The file has been updated successfully.", + "is_error": False, + "structuredPatch": [...], + "filePath": "...", + # ... other fields from toolUseResult + } +} +``` + +**Result Structure by Tool Type:** + +| Tool | Result Fields | +|------|---------------| +| Edit | `structuredPatch`, `filePath`, `oldString`, `newString` | +| Write | `filePath`, content confirmation | +| Read | `file`, `type`, content in `content` field | +| Bash | `stdout`, `stderr`, `interrupted` | +| Glob | `filenames`, `numFiles`, `truncated` | +| Grep | `content`, `filenames`, `numFiles`, `numLines` | + +--- + +### IMP-TOOLCALL: Expandable Tool Call Component + +**Fulfills:** AC-1, AC-2, AC-3, AC-4, AC-5, AC-6, AC-7 + +**Location:** `dashboard/lib/markdown.js` (refactor `renderToolCalls`) + +**New function: `ToolCallItem`** + +Renders a single tool call with: +- Chevron for expand/collapse (when result exists and not Edit/Write) +- Tool name (bold, colored) +- Summary (from existing `getToolSummary`) +- Status icon (checkmark or X) +- Result content (when expanded) + +**State Management:** + +Track expanded state per message. When new assistant message arrives: +- Compare latest assistant message ID to stored ID +- If different, reset expanded set to empty +- Edit/Write tools bypass this logic (always expanded via CSS/logic) + +--- + +### IMP-DIFF: Diff Rendering Component + +**Fulfills:** AC-8, AC-9, AC-10, AC-11, AC-12 + +**Location:** `dashboard/lib/markdown.js` (new function `renderDiff`) + +**Add diff language to highlight.js:** +```javascript +import langDiff from 'https://esm.sh/highlight.js@11.11.1/lib/languages/diff'; +hljs.registerLanguage('diff', langDiff); +``` + +**Diff Renderer:** + +1. Convert `structuredPatch` array to unified diff text: + - Each hunk: `@@ -oldStart,oldLines +newStart,newLines @@` + - Followed by hunk.lines array +2. Syntax highlight with hljs diff language +3. Sanitize with DOMPurify before rendering +4. Wrap in container with file path header + +**CSS styling:** +- Container: dark border, rounded corners +- Header: muted background, monospace font, full file path +- Content: monospace, horizontal scroll +- Additions: `background: rgba(46, 160, 67, 0.15)` +- Deletions: `background: rgba(248, 81, 73, 0.15)` + +--- + +### IMP-BASH: Bash Output Component + +**Fulfills:** AC-13, AC-21, AC-22 + +**Location:** `dashboard/lib/markdown.js` (new function `renderBashResult`) + +Renders: +- `stdout` in monospace pre block +- `stderr` in separate block with error styling (if present) +- "Command interrupted" notice (if interrupted flag) + +Error state: `is_error` or presence of stderr triggers error styling (red tint, left border). + +--- + +### IMP-TRUNCATE: Output Truncation + +**Fulfills:** AC-17, AC-18 + +**Truncation Thresholds (match Claude Code):** + +| Tool Type | Max Lines | Max Chars | +|-----------|-----------|-----------| +| Bash stdout | 100 | 10000 | +| Bash stderr | 50 | 5000 | +| Read content | 500 | 50000 | +| Grep matches | 100 | 10000 | +| Glob files | 100 | 5000 | + +**Note:** These thresholds need verification against Claude Code behavior. May require adjustment based on testing. + +**Truncation Helper:** + +Takes content string, returns `{ text, truncated, totalLines }`. If truncated, result renderers show "Show full output (N lines)" link. + +--- + +### IMP-MODAL: Full Output Modal + +**Fulfills:** AC-19, AC-20 + +**Location:** `dashboard/components/FullOutputModal.js` (new file) + +**Structure:** +- Overlay (click to close) +- Modal container (click does NOT close) +- Header: title (tool name + file path), close button +- Content: scrollable pre/code block with syntax highlighting + +**Integration:** Modal state managed at App level or ChatMessages level. "Show full output" link sets state with content + metadata. + +--- + +### IMP-ERROR: Error State Styling + +**Fulfills:** AC-21, AC-22, AC-23 + +**Styling:** +- Tool call header: red-tinted background when `result.is_error` +- Status icon: red X instead of green checkmark +- Bash stderr: red text, italic, distinct from stdout +- Overall: left border accent in error color + +--- + +## Rollout Slices + +### Slice 1: Design Mockups (Pre-Implementation) + +**Goal:** Validate visual design before building + +**Deliverables:** +1. Create `/mockups` test route with static data +2. Implement 3-4 design variants (card-based, minimal, etc.) +3. Use real tool result data from session JSONL +4. User reviews and selects preferred design + +**Exit Criteria:** Design direction locked + +--- + +### Slice 2: Server-Side Tool Result Parsing + +**Goal:** API returns tool results nested in tool_calls + +**Deliverables:** +1. Two-pass parsing in `_parse_claude_conversation` +2. Tool results attached with `id` field +3. Unit tests for result attachment +4. Handle missing results gracefully (return tool_call without result) + +**Exit Criteria:** AC-24, AC-25, AC-26 pass + +--- + +### Slice 3: Basic Expand/Collapse UI + +**Goal:** Tool calls are expandable, show raw result content + +**Deliverables:** +1. Refactor `renderToolCalls` to `ToolCallList` component +2. Implement expand/collapse with chevron +3. Track expanded state per message +4. Collapse on new assistant message +5. Keep Edit/Write always expanded + +**Exit Criteria:** AC-1 through AC-7 pass + +--- + +### Slice 4: Diff Rendering + +**Goal:** Edit/Write show beautiful diffs + +**Deliverables:** +1. Add diff language to highlight.js +2. Implement `renderDiff` function +3. VS Code dark theme styling +4. Full file path header + +**Exit Criteria:** AC-8 through AC-12 pass + +--- + +### Slice 5: Other Tool Types + +**Goal:** Bash, Read, Glob, Grep render appropriately + +**Deliverables:** +1. `renderBashResult` with stdout/stderr separation +2. `renderFileContent` for Read +3. `renderFileList` for Glob/Grep +4. Generic fallback for unknown tools + +**Exit Criteria:** AC-13 through AC-16 pass + +--- + +### Slice 6: Truncation and Modal + +**Goal:** Long outputs truncate with modal expansion + +**Deliverables:** +1. Truncation helper with Claude Code thresholds +2. "Show full output" link +3. `FullOutputModal` component +4. Syntax highlighting in modal + +**Exit Criteria:** AC-17 through AC-20 pass + +--- + +### Slice 7: Error States and Polish + +**Goal:** Failed tools visually distinct, edge cases handled + +**Deliverables:** +1. Error state styling (red tint) +2. Muted styling for missing results +3. Test with interrupted sessions +4. Cross-browser testing + +**Exit Criteria:** AC-21 through AC-23 pass, feature complete + +--- + +## Open Questions + +1. **Exact Claude Code truncation thresholds** — need to verify against Claude Code source or experiment +2. **Performance with 100+ tool calls** — monitor after ship, optimize if needed +3. **Codex support timeline** — when should we prioritize v2? + +--- + +## Appendix: Research Findings + +### Claude Code JSONL Format + +Tool calls and results are stored as separate entries: + +```json +// Assistant sends tool_use +{"type": "assistant", "message": {"content": [{"type": "tool_use", "id": "toolu_abc", "name": "Edit", "input": {...}}]}} + +// Result in separate user entry +{"type": "user", "message": {"content": [{"type": "tool_result", "tool_use_id": "toolu_abc", "content": "Success"}]}, "toolUseResult": {...}} +``` + +The `toolUseResult` object contains rich structured data varying by tool type. + +### Missing Results Statistics + +Across 55 sessions with 2,063 tool calls: +- 11 missing results (0.5%) +- Affected tools: Edit (4), Read (2), Bash (1), others + +### Interrupt Handling + +User interrupts create a separate user message: +```json +{"type": "user", "message": {"content": [{"type": "text", "text": "[Request interrupted by user for tool use]"}]}} +``` + +Tool results for completed tools are still present; the interrupt message indicates the turn ended early. diff --git a/plans/subagent-visibility.md b/plans/subagent-visibility.md index 7bf368c..878b89a 100644 --- a/plans/subagent-visibility.md +++ b/plans/subagent-visibility.md @@ -1,1631 +1,51 @@ # Subagent & Agent Team Visibility for AMC -> **Version**: 1.0 > **Status**: Draft -> **Last Updated**: 2026-02-26 +> **Last Updated**: 2026-02-27 ## Summary -Add comprehensive visibility into Claude Code subagents and agent teams within AMC. This enables users to monitor, understand, and debug multi-agent workflows by surfacing subagent activity, team coordination, and task progress directly in the dashboard. +Add a button in the turn stats section showing the count of active subagents/team members. Clicking it opens a list with names and lifetime stats (time taken, tokens used). Mirrors Claude Code's own agent display. --- -## Problem Statement +## User Workflow -Claude Code supports two multi-agent patterns that are currently invisible to AMC: - -1. **Subagents** (via Task tool): Lightweight, isolated workers that execute in parallel and report results back to a parent session. Their conversations are stored in separate JSONL files but never surfaced. - -2. **Agent Teams**: Full independent sessions with bidirectional messaging, shared task lists, and coordinated work. Team state lives in separate directories but is never parsed. - -Without visibility into these, users cannot: -- See what subagents are doing in real-time -- Understand which tasks are being worked on by which agents -- Debug multi-agent coordination failures -- Monitor team member status and progress -- Trace the parent-child relationships in complex workflows - ---- - -## User Workflows - -### Workflow 1: Monitor Active Subagents - -**Actor**: User with a Claude Code session that spawned subagents - -**Flow**: -1. User views session card in AMC dashboard -2. Dashboard shows "3 subagents active" indicator -3. User clicks to expand subagent panel -4. Each subagent shows: type, status (running/complete), duration, last activity -5. User clicks a subagent to view its conversation in a modal -6. Real-time updates show subagent progress without refresh - -### Workflow 2: Team Dashboard - -**Actor**: User running an agent team - -**Flow**: -1. AMC discovers active team from `~/.claude/teams/` -2. Dashboard shows team card with: team name, description, member count -3. User expands to see all team members: name, type, model, status -4. Task list panel shows: tasks by status (pending/in_progress/completed), assignments, dependencies -5. User can view any teammate's conversation -6. Real-time updates show task transitions and team messaging - -### Workflow 3: Trace Parent-Child Relationships - -**Actor**: User debugging a multi-agent workflow - -**Flow**: -1. User views a subagent's conversation -2. UI shows "Spawned by: [parent session]" with link -3. User can navigate between parent and child conversations -4. Parent conversation shows inline markers: "[Spawned: code-reviewer → completed 2m ago]" -5. User understands the full workflow lineage +1. User views a session card in AMC +2. Turn stats area shows: `2h 15m | 84k tokens | 3 agents` +3. User clicks "3 agents" button +4. List opens showing: + ``` + claude-code-guide (running) 12m 42,000 tokens + Explore (completed) 3m 18,500 tokens + Explore (completed) 5m 23,500 tokens + ``` +5. List updates in real-time as agents complete --- ## Acceptance Criteria -### Subagent Discovery +### Discovery -- **AC-1**: AMC discovers subagent JSONL files in `{session_dir}/subagents/agent-*.jsonl` -- **AC-2**: Subagent metadata (agentId, type, status) is extracted from JSONL headers -- **AC-3**: Subagents are associated with their parent session via shared `sessionId` -- **AC-4**: Subagent discovery runs during state collection (existing poll interval) +- **AC-1**: Subagent JSONL files discovered at `{session_dir}/subagents/agent-*.jsonl` +- **AC-2**: Both regular subagents (Task tool) and team members (Task with `team_name`) are discovered from same location -### Subagent Display +### Status Detection -- **AC-5**: Session cards show subagent count badge when subagents exist -- **AC-6**: Expandable panel lists all subagents with: type, status, duration, last activity -- **AC-7**: Clicking a subagent opens its conversation in a modal -- **AC-8**: Subagent conversation parsing reuses existing `_parse_claude_conversation` logic +- **AC-3**: Subagent is "running" if: parent session is alive AND last assistant entry has `stop_reason != "end_turn"` +- **AC-4**: Subagent is "completed" if: last assistant entry has `stop_reason == "end_turn"` OR parent session is dead -### Subagent Status +### Stats Extraction -- **AC-9**: Running subagents show animated activity indicator -- **AC-10**: Completed subagents show completion status with final message summary -- **AC-11**: Subagent status is derived from: file mtime recency, presence of stop markers +- **AC-5**: Subagent name extracted from parent's Task tool invocation: use `name` if present (team member), else `subagent_type` +- **AC-6**: Lifetime duration = first entry timestamp to last entry timestamp (or now if running) +- **AC-7**: Lifetime tokens = sum of all assistant entries' `usage.input_tokens + usage.output_tokens` -### Team Discovery +### UI -- **AC-12**: AMC discovers teams from `~/.claude/teams/*/config.json` -- **AC-13**: Team config is parsed: name, description, leadAgentId, members[] -- **AC-14**: Team tasks are discovered from `~/.claude/tasks/{team_name}/` -- **AC-15**: Team member sessions are linked to their corresponding session files - -### Team Display - -- **AC-16**: Teams appear as distinct cards in the dashboard (separate from individual sessions) -- **AC-17**: Team card shows: name, description, member count, active task count -- **AC-18**: Expanded team view shows member list with: name, type, model, status -- **AC-19**: Task panel shows tasks grouped by status with assignment indicators - -### Parent-Child Linking - -- **AC-20**: Subagent conversations show "Spawned by: [parent]" header with navigation link -- **AC-21**: Parent conversations show inline markers for spawned subagents with status -- **AC-22**: Users can navigate bidirectionally between parent and child conversations - -### Real-Time Updates - -- **AC-23**: Subagent status updates in real-time via existing SSE/polling mechanism -- **AC-24**: Team state changes (task status, member joins) propagate without refresh -- **AC-25**: File mtime tracking extends to subagent and team state files - ---- - -## Architecture - -### Data Model Extensions - -```python -# Existing Session model gains new fields: -{ - "session_id": "uuid", - "agent": "claude", - # ... existing fields ... - - # NEW: Subagent information - "subagents": [ - { - "agent_id": "a510a6b", - "subagent_type": "Explore", # from Task tool input - "status": "running|completed", - "started_at": "iso-timestamp", - "completed_at": "iso-timestamp|null", - "last_activity_at": "iso-timestamp", - "message_count": 42, - "transcript_path": "/path/to/agent-xxx.jsonl" - } - ], - - # NEW: Team membership (if this session is a team member) - "team": { - "name": "mission-control", - "role": "lead|member", - "member_name": "team-lead" # from team config - } -} - -# NEW: Team model (separate from sessions) -{ - "type": "team", - "name": "mission-control", - "description": "Building Mission Control...", - "lead_agent_id": "team-lead@mission-control", - "lead_session_id": "uuid", - "members": [ - { - "agent_id": "team-lead@mission-control", - "name": "team-lead", - "agent_type": "team-lead", - "model": "claude-opus-4-5-20251101", - "status": "active|idle|offline", - "session_id": "uuid|null" # linked to session if found - } - ], - "tasks": { - "pending": 5, - "in_progress": 2, - "completed": 12, - "items": [...] # optional: full task list - }, - "created_at": "iso-timestamp", - "config_mtime_ns": 123456789 -} -``` - -### File System Locations - -| Data Type | Path Pattern | -|-----------|--------------| -| Parent session | `~/.claude/projects/{project}/{session_id}.jsonl` | -| Subagent transcript | `~/.claude/projects/{project}/{session_id}/subagents/agent-{agentId}.jsonl` | -| Team config | `~/.claude/teams/{team_name}/config.json` | -| Team inboxes | `~/.claude/teams/{team_name}/inboxes/` | -| Team tasks | `~/.claude/tasks/{team_name}/` | - -### Parsing Logic - -#### Subagent Discovery (in StateMixin) - -```python -def _discover_subagents(self, session_id, project_dir): - """Discover subagent JSONL files for a session.""" - subagents = [] - session_dir = self._get_session_dir(session_id, project_dir) - subagents_dir = session_dir / "subagents" - - if not subagents_dir.exists(): - return subagents - - for jsonl_file in subagents_dir.glob("agent-*.jsonl"): - # Extract agent_id from filename: agent-{agentId}.jsonl - agent_id = jsonl_file.stem.replace("agent-", "") - - # Parse first entry for metadata - metadata = self._parse_subagent_metadata(jsonl_file) - - subagents.append({ - "agent_id": agent_id, - "subagent_type": metadata.get("subagent_type", "unknown"), - "status": self._determine_subagent_status(jsonl_file), - "started_at": metadata.get("started_at"), - "last_activity_at": self._get_file_mtime_iso(jsonl_file), - "message_count": metadata.get("message_count", 0), - "transcript_path": str(jsonl_file), - }) - - return subagents -``` - -#### Subagent Type Extraction - -The subagent type is found in the parent session's Task tool invocation: - -```python -def _extract_subagent_type_from_parent(self, parent_jsonl, agent_id): - """Extract subagent_type from parent's Task tool call that spawned this agent.""" - for line in parent_jsonl.read_text().splitlines(): - entry = json.loads(line) - - # Look for progress entries with this agent's ID - if entry.get("type") == "progress": - data = entry.get("data", {}) - if data.get("agentId") == agent_id: - # The parentToolUseID links to the Task tool invocation - parent_tool_use_id = entry.get("parentToolUseID") - # Now find the Task tool_use with that ID to get subagent_type - ... -``` - -#### Team Discovery (new mixin: TeamMixin) - -```python -class TeamMixin: - TEAMS_DIR = Path.home() / ".claude" / "teams" - TASKS_DIR = Path.home() / ".claude" / "tasks" - - def _discover_teams(self): - """Discover all active teams.""" - teams = [] - - if not self.TEAMS_DIR.exists(): - return teams - - for team_dir in self.TEAMS_DIR.iterdir(): - if not team_dir.is_dir(): - continue - - config_file = team_dir / "config.json" - if not config_file.exists(): - continue - - try: - config = json.loads(config_file.read_text()) - team = self._build_team_model(config, team_dir.name) - teams.append(team) - except (json.JSONDecodeError, OSError): - continue - - return teams - - def _build_team_model(self, config, team_name): - """Build team model from config.json.""" - members = [] - for m in config.get("members", []): - members.append({ - "agent_id": m.get("agentId"), - "name": m.get("name"), - "agent_type": m.get("agentType"), - "model": m.get("model"), - "status": self._determine_member_status(m), - "session_id": self._find_member_session(m), - }) - - return { - "type": "team", - "name": config.get("name", team_name), - "description": config.get("description", ""), - "lead_agent_id": config.get("leadAgentId"), - "lead_session_id": config.get("leadSessionId"), - "members": members, - "tasks": self._load_team_tasks(team_name), - "created_at": self._timestamp_to_iso(config.get("createdAt")), - } -``` - -### API Extensions - -``` -GET /api/state - Response adds: - { - "sessions": [...], // existing, now includes subagents[] - "teams": [...], // NEW: active teams - "server_time": "..." - } - -GET /api/conversation/{session_id} - Existing endpoint works for subagents too (by agent_id) - -GET /api/subagent/{session_id}/{agent_id} - NEW: Get subagent conversation specifically - -GET /api/team/{team_name} - NEW: Get detailed team info including tasks - -GET /api/team/{team_name}/tasks - NEW: Get full task list for a team -``` - -### Dashboard Components - -#### SubagentPanel.js - -```javascript -function SubagentPanel({ subagents, onSelectSubagent }) { - if (!subagents?.length) return null; - - return ( -
-
- {subagents.length} subagents -
-
- {subagents.map(sa => ( - onSelectSubagent(sa)} - /> - ))} -
-
- ); -} - -function SubagentRow({ subagent, onClick }) { - const isRunning = subagent.status === 'running'; - - return ( -
- - {isRunning && } - - {subagent.subagent_type} - - {formatDuration(subagent.started_at, subagent.completed_at)} - -
- ); -} -``` - -#### TeamCard.js - -```javascript -function TeamCard({ team, onExpand }) { - const activeCount = team.members.filter(m => m.status === 'active').length; - const taskCounts = team.tasks; - - return ( -
-
- {team.name} - Team -
-
{team.description}
-
- {activeCount}/{team.members.length} members active - {taskCounts.in_progress} tasks in progress -
- -
- ); -} -``` - ---- - -## Implementation Specs - -### IMP-1: Subagent Discovery Mixin - -**Fulfills**: AC-1, AC-2, AC-3, AC-4 - -**File**: `amc_server/mixins/subagent.py` (new) - -```python -"""Subagent discovery and parsing mixin for AMC server.""" - -import json -import os -import time -from pathlib import Path - - -class SubagentMixin: - """Discovers and parses Claude Code subagent sessions.""" - - def _discover_subagents_for_session(self, session_id, project_dir): - """Discover all subagent JSONL files for a given parent session. - - Subagents are stored in: {session_dir}/subagents/agent-{agentId}.jsonl - They share the parent's sessionId but have their own agentId. - - Args: - session_id: Parent session UUID - project_dir: Parent session's project directory - - Returns: - List of subagent dicts with metadata - """ - subagents = [] - - # Get session directory path - session_dir = self._get_claude_session_dir(session_id, project_dir) - if not session_dir: - return subagents - - subagents_dir = session_dir / "subagents" - if not subagents_dir.exists(): - return subagents - - for jsonl_file in subagents_dir.glob("agent-*.jsonl"): - try: - agent_id = self._extract_agent_id_from_filename(jsonl_file.name) - metadata = self._parse_subagent_metadata(jsonl_file, session_id, project_dir) - - subagents.append({ - "agent_id": agent_id, - "subagent_type": metadata.get("subagent_type", "unknown"), - "prompt_summary": metadata.get("prompt_summary", ""), - "status": self._determine_subagent_status(jsonl_file, metadata), - "started_at": metadata.get("started_at"), - "completed_at": metadata.get("completed_at"), - "last_activity_at": self._get_file_mtime_iso(jsonl_file), - "message_count": metadata.get("message_count", 0), - "transcript_path": str(jsonl_file), - "mtime_ns": self._get_file_mtime_ns(jsonl_file), - }) - except Exception: - continue - - # Sort by started_at (most recent first) - subagents.sort(key=lambda s: s.get("started_at", ""), reverse=True) - return subagents - - def _extract_agent_id_from_filename(self, filename): - """Extract agentId from filename like 'agent-a510a6b.jsonl'.""" - # Handle both formats: agent-{id}.jsonl and agent-acompact-{id}.jsonl - stem = filename.replace(".jsonl", "") - if stem.startswith("agent-"): - return stem[6:] # Remove "agent-" prefix - return stem - - def _parse_subagent_metadata(self, jsonl_file, parent_session_id, parent_project_dir): - """Parse subagent JSONL to extract metadata. - - Reads first and last entries to determine: - - Agent ID (from agentId field) - - Subagent type (from parent's Task tool invocation) - - Start/end timestamps - - Message count - """ - metadata = { - "message_count": 0, - "started_at": None, - "completed_at": None, - "subagent_type": "unknown", - "prompt_summary": "", - } - - first_entry = None - last_entry = None - message_count = 0 - - try: - with open(jsonl_file, 'r') as f: - for line in f: - if not line.strip(): - continue - try: - entry = json.loads(line) - if not isinstance(entry, dict): - continue - - if first_entry is None: - first_entry = entry - last_entry = entry - - # Count user and assistant messages - if entry.get("type") in ("user", "assistant"): - message_count += 1 - except json.JSONDecodeError: - continue - except OSError: - return metadata - - metadata["message_count"] = message_count - - if first_entry: - metadata["started_at"] = first_entry.get("timestamp") - metadata["agent_id"] = first_entry.get("agentId") - - if last_entry: - metadata["completed_at"] = last_entry.get("timestamp") - - # Extract subagent_type from parent session's Task tool invocation - agent_id = metadata.get("agent_id") - if agent_id: - type_info = self._extract_subagent_type_from_parent( - parent_session_id, parent_project_dir, agent_id - ) - if type_info: - metadata["subagent_type"] = type_info.get("subagent_type", "unknown") - metadata["prompt_summary"] = type_info.get("prompt_summary", "") - - return metadata - - def _extract_subagent_type_from_parent(self, session_id, project_dir, agent_id): - """Extract subagent_type and prompt from parent's Task tool invocation. - - The parent session contains progress entries with: - - data.agentId matching our subagent - - parentToolUseID linking to the Task tool_use - - We find the Task tool_use to extract subagent_type and prompt. - """ - parent_file = self._get_claude_conversation_file(session_id, project_dir) - if not parent_file or not parent_file.exists(): - return None - - # First pass: find the parentToolUseID for this agent - parent_tool_use_id = None - try: - with open(parent_file, 'r') as f: - for line in f: - if not line.strip(): - continue - try: - entry = json.loads(line) - if entry.get("type") == "progress": - data = entry.get("data", {}) - if data.get("agentId") == agent_id: - parent_tool_use_id = entry.get("parentToolUseID") - break - except json.JSONDecodeError: - continue - except OSError: - return None - - if not parent_tool_use_id: - return None - - # Second pass: find the Task tool_use with that ID - try: - with open(parent_file, 'r') as f: - for line in f: - if not line.strip(): - continue - try: - entry = json.loads(line) - if entry.get("type") == "assistant": - message = entry.get("message", {}) - content = message.get("content", []) - if not isinstance(content, list): - continue - for part in content: - if not isinstance(part, dict): - continue - if part.get("type") == "tool_use" and part.get("name") == "Task": - if part.get("id") == parent_tool_use_id: - input_data = part.get("input", {}) - prompt = input_data.get("prompt", "") - return { - "subagent_type": input_data.get("subagent_type", "unknown"), - "prompt_summary": prompt[:100] + "..." if len(prompt) > 100 else prompt, - } - except json.JSONDecodeError: - continue - except OSError: - return None - - return None - - def _determine_subagent_status(self, jsonl_file, metadata): - """Determine if subagent is running or completed. - - Heuristics: - - If file modified within last 30 seconds: running - - If has completion markers: completed - - Otherwise: completed (assume finished) - """ - try: - mtime = jsonl_file.stat().st_mtime - age = time.time() - mtime - - # Recently active = running - if age < 30: - return "running" - - # Check message count - very few messages might indicate still starting - if metadata.get("message_count", 0) < 2 and age < 60: - return "running" - - return "completed" - except OSError: - return "completed" - - def _get_file_mtime_iso(self, path): - """Get file mtime as ISO timestamp.""" - try: - from datetime import datetime, timezone - mtime = path.stat().st_mtime - return datetime.fromtimestamp(mtime, timezone.utc).isoformat() - except OSError: - return None - - def _get_file_mtime_ns(self, path): - """Get file mtime in nanoseconds for change detection.""" - try: - return path.stat().st_mtime_ns - except OSError: - return None - - def _get_claude_session_dir(self, session_id, project_dir): - """Get the session directory path for a Claude Code session. - - Claude stores session data at: - ~/.claude/projects/{encoded_project_dir}/{session_id}/ - """ - if not project_dir: - return None - - claude_projects_dir = Path.home() / ".claude" / "projects" - - # Encode project_dir same way Claude does: replace / with - - encoded = project_dir.replace("/", "-") - if encoded.startswith("-"): - encoded = encoded # Keep leading dash - - session_dir = claude_projects_dir / encoded / session_id - if session_dir.exists() and session_dir.is_dir(): - return session_dir - - return None -``` - -### IMP-2: Team Discovery Mixin - -**Fulfills**: AC-12, AC-13, AC-14, AC-15 - -**File**: `amc_server/mixins/team.py` (new) - -```python -"""Agent team discovery and parsing mixin for AMC server.""" - -import json -import time -from datetime import datetime, timezone -from pathlib import Path - - -class TeamMixin: - """Discovers and parses Claude Code agent teams.""" - - TEAMS_DIR = Path.home() / ".claude" / "teams" - TASKS_DIR = Path.home() / ".claude" / "tasks" - - def _discover_teams(self): - """Discover all active agent teams. - - Teams are stored at ~/.claude/teams/{team_name}/config.json - - Returns: - List of team dicts with metadata - """ - teams = [] - - if not self.TEAMS_DIR.exists(): - return teams - - for team_dir in self.TEAMS_DIR.iterdir(): - if not team_dir.is_dir(): - continue - - config_file = team_dir / "config.json" - if not config_file.exists(): - continue - - try: - config = json.loads(config_file.read_text()) - team = self._build_team_model(config, team_dir.name) - - # Add mtime for change detection - team["config_mtime_ns"] = config_file.stat().st_mtime_ns - - teams.append(team) - except (json.JSONDecodeError, OSError): - continue - - # Sort by creation time (most recent first) - teams.sort(key=lambda t: t.get("created_at", ""), reverse=True) - return teams - - def _build_team_model(self, config, team_name): - """Build team model from config.json. - - Config structure: - { - "name": "team-name", - "description": "...", - "createdAt": 1772054633201, - "leadAgentId": "team-lead@team-name", - "leadSessionId": "uuid", - "members": [ - { - "agentId": "team-lead@team-name", - "name": "team-lead", - "agentType": "team-lead", - "model": "claude-opus-4-5-20251101", - "joinedAt": 1772054633201, - "tmuxPaneId": "", - "cwd": "/path/to/project", - "subscriptions": [] - } - ] - } - """ - members = [] - for m in config.get("members", []): - member = { - "agent_id": m.get("agentId"), - "name": m.get("name"), - "agent_type": m.get("agentType"), - "model": m.get("model"), - "joined_at": self._timestamp_to_iso(m.get("joinedAt")), - "cwd": m.get("cwd"), - "status": "unknown", # Will be enriched later - "session_id": None, # Will be linked if found - } - members.append(member) - - # Load task summary - tasks = self._load_team_tasks_summary(team_name) - - return { - "type": "team", - "name": config.get("name", team_name), - "description": config.get("description", ""), - "lead_agent_id": config.get("leadAgentId"), - "lead_session_id": config.get("leadSessionId"), - "members": members, - "member_count": len(members), - "tasks": tasks, - "created_at": self._timestamp_to_iso(config.get("createdAt")), - } - - def _load_team_tasks_summary(self, team_name): - """Load task summary for a team. - - Tasks are stored at ~/.claude/tasks/{team_name}/ - """ - tasks_dir = self.TASKS_DIR / team_name - - summary = { - "pending": 0, - "in_progress": 0, - "completed": 0, - "total": 0, - } - - if not tasks_dir.exists(): - return summary - - # Count tasks by parsing task files or .highwatermark - for task_file in tasks_dir.glob("*.json"): - try: - task = json.loads(task_file.read_text()) - status = task.get("status", "pending") - if status in summary: - summary[status] += 1 - summary["total"] += 1 - except (json.JSONDecodeError, OSError): - continue - - return summary - - def _load_team_tasks_full(self, team_name): - """Load full task list for a team (for detailed view).""" - tasks_dir = self.TASKS_DIR / team_name - tasks = [] - - if not tasks_dir.exists(): - return tasks - - for task_file in tasks_dir.glob("*.json"): - try: - task = json.loads(task_file.read_text()) - tasks.append({ - "id": task.get("id"), - "subject": task.get("subject"), - "description": task.get("description"), - "status": task.get("status", "pending"), - "owner": task.get("owner"), - "blocked_by": task.get("blockedBy", []), - "blocks": task.get("blocks", []), - "created_at": task.get("createdAt"), - }) - except (json.JSONDecodeError, OSError): - continue - - # Sort by ID (numeric order if possible) - tasks.sort(key=lambda t: t.get("id", "")) - return tasks - - def _timestamp_to_iso(self, ts): - """Convert millisecond timestamp to ISO format.""" - if not ts: - return None - try: - # Handle both milliseconds and seconds - if ts > 1e12: - ts = ts / 1000 # Convert from milliseconds - return datetime.fromtimestamp(ts, timezone.utc).isoformat() - except (ValueError, OSError): - return None - - def _link_team_members_to_sessions(self, team, sessions): - """Link team members to their corresponding session records. - - Updates member status and session_id based on active sessions. - """ - # Build session lookup by session_id - session_lookup = {s.get("session_id"): s for s in sessions} - - for member in team.get("members", []): - # Check if lead session matches - if member.get("agent_id") == team.get("lead_agent_id"): - lead_session_id = team.get("lead_session_id") - if lead_session_id in session_lookup: - session = session_lookup[lead_session_id] - member["session_id"] = lead_session_id - member["status"] = "active" if not session.get("is_dead") else "offline" - else: - member["status"] = "offline" - else: - # Non-lead members: try to find matching session by cwd - member["status"] = "unknown" -``` - -### IMP-3: State Mixin Integration - -**Fulfills**: AC-4, AC-23, AC-24, AC-25 - -**File**: `amc_server/mixins/state.py` (modify) - -Update `_build_state_payload` to include teams: - -```python -def _build_state_payload(self): - """Build `/api/state` payload data used by JSON and SSE endpoints.""" - sessions = self._collect_sessions() - teams = self._discover_teams() - - # Link team members to sessions - for team in teams: - self._link_team_members_to_sessions(team, sessions) - - return { - "sessions": sessions, - "teams": teams, - "server_time": datetime.now(timezone.utc).isoformat(), - } -``` - -Update `_collect_sessions` to call subagent discovery: - -```python -# In the session processing loop, after loading basic data: -if data.get("agent") == "claude": - subagents = self._discover_subagents_for_session( - data.get("session_id", ""), - data.get("project_dir", "") - ) - if subagents: - data["subagents"] = subagents -``` - -### IMP-4: API Route Extensions - -**Fulfills**: AC-7, AC-17 - -**File**: `amc_server/mixins/http.py` (modify) - -Add new routes: - -```python -# In _route_get(): - -elif path.startswith("/api/subagent/"): - # /api/subagent/{session_id}/{agent_id} - parts = path.split("/") - if len(parts) >= 5: - session_id = parts[3] - agent_id = parts[4] - self._serve_subagent_conversation(session_id, agent_id) - else: - self._send_error(400, "Invalid subagent path") - -elif path.startswith("/api/team/"): - # /api/team/{team_name} or /api/team/{team_name}/tasks - parts = path.split("/") - team_name = parts[3] if len(parts) > 3 else "" - if len(parts) > 4 and parts[4] == "tasks": - self._serve_team_tasks(team_name) - else: - self._serve_team_detail(team_name) -``` - -### IMP-5: Dashboard SubagentPanel Component - -**Fulfills**: AC-5, AC-6, AC-7, AC-9, AC-10 - -**File**: `dashboard/components/SubagentPanel.js` (new) - -```javascript -import { h } from 'preact'; -import { useState } from 'preact/hooks'; -import { AgentActivityIndicator } from './AgentActivityIndicator.js'; -import { formatDuration, formatRelativeTime } from '../utils/formatting.js'; - -export function SubagentPanel({ subagents, onSelectSubagent }) { - const [expanded, setExpanded] = useState(false); - - if (!subagents?.length) return null; - - const runningCount = subagents.filter(s => s.status === 'running').length; - - return ( -
- - - {expanded && ( -
- {subagents.map(sa => ( - onSelectSubagent(sa)} - /> - ))} -
- )} -
- ); -} - -function SubagentRow({ subagent, onClick }) { - const isRunning = subagent.status === 'running'; - const duration = formatDuration(subagent.started_at, subagent.completed_at); - - return ( -
- - {isRunning ? ( - - ) : ( - - )} - - {subagent.subagent_type} - - {subagent.message_count} msgs • {duration} - -
- ); -} -``` - -### IMP-6: Dashboard TeamCard Component - -**Fulfills**: AC-16, AC-17, AC-18, AC-19 - -**File**: `dashboard/components/TeamCard.js` (new) - -```javascript -import { h } from 'preact'; -import { useState } from 'preact/hooks'; -import { formatRelativeTime } from '../utils/formatting.js'; - -export function TeamCard({ team, sessions, onViewMember }) { - const [expanded, setExpanded] = useState(false); - - const activeMembers = team.members.filter(m => m.status === 'active'); - const tasks = team.tasks; - - return ( -
-
- {team.name} - Team -
- - {team.description && ( -
{team.description}
- )} - -
- - {activeMembers.length} - /{team.member_count} active - - - {tasks.in_progress} - in progress - - - {tasks.completed} - completed - -
- - - - {expanded && ( -
- - -
- )} -
- ); -} - -function TeamMemberList({ members, onViewMember }) { - return ( -
-

Members

- {members.map(m => ( -
m.session_id && onViewMember(m.session_id)} - > - - {m.name} - {m.agent_type} - {m.model?.split('-').slice(-2, -1)[0]} -
- ))} -
- ); -} - -function TeamTaskSummary({ tasks }) { - return ( -
-

Tasks

-
-
-
-
- {tasks.pending} pending - {tasks.in_progress} in progress - {tasks.completed} done -
-
- ); -} -``` - -### IMP-7: Styles for Subagent/Team UI - -**Fulfills**: AC-5, AC-6, AC-9, AC-16, AC-17 - -**File**: `dashboard/styles.css` (additions) - -```css -/* Subagent Panel */ -.subagent-panel { - margin-top: 8px; - border-top: 1px solid var(--border-color, #333); - padding-top: 8px; -} - -.subagent-toggle { - display: flex; - align-items: center; - gap: 8px; - background: transparent; - border: none; - color: var(--text-secondary, #999); - cursor: pointer; - padding: 4px 0; - width: 100%; - text-align: left; -} - -.subagent-toggle:hover { - color: var(--text-primary, #fff); -} - -.subagent-count { - flex: 1; - font-size: 12px; -} - -.expand-arrow { - font-size: 10px; - transition: transform 0.2s; -} - -.expand-arrow.expanded { - transform: rotate(90deg); -} - -.subagent-list { - margin-top: 8px; - display: flex; - flex-direction: column; - gap: 4px; -} - -.subagent-row { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 8px; - background: var(--surface-secondary, #222); - border-radius: 4px; - cursor: pointer; - font-size: 12px; -} - -.subagent-row:hover { - background: var(--surface-hover, #333); -} - -.subagent-row.running { - border-left: 2px solid var(--color-active, #4CAF50); -} - -.subagent-row.completed { - border-left: 2px solid var(--color-done, #666); -} - -.subagent-status { - display: flex; - align-items: center; - justify-content: center; - width: 16px; -} - -.status-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--color-done, #666); -} - -.subagent-type { - font-weight: 500; - color: var(--text-primary, #fff); -} - -.subagent-meta { - margin-left: auto; - color: var(--text-secondary, #999); -} - -/* Team Card */ -.team-card { - background: var(--surface-primary, #1a1a1a); - border: 1px solid var(--border-color, #333); - border-radius: 8px; - padding: 16px; - margin-bottom: 12px; -} - -.team-header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 8px; -} - -.team-name { - font-size: 16px; - font-weight: 600; - color: var(--text-primary, #fff); -} - -.team-badge { - font-size: 10px; - padding: 2px 6px; - background: var(--color-team, #9C27B0); - color: #fff; - border-radius: 4px; - text-transform: uppercase; - font-weight: 600; -} - -.team-description { - font-size: 13px; - color: var(--text-secondary, #999); - margin-bottom: 12px; -} - -.team-stats { - display: flex; - gap: 16px; - margin-bottom: 12px; -} - -.team-stats .stat { - display: flex; - align-items: baseline; - gap: 4px; -} - -.stat-value { - font-size: 18px; - font-weight: 600; - color: var(--text-primary, #fff); -} - -.stat-label { - font-size: 12px; - color: var(--text-secondary, #999); -} - -.team-expand { - background: var(--surface-secondary, #222); - border: 1px solid var(--border-color, #333); - color: var(--text-primary, #fff); - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; - width: 100%; -} - -.team-expand:hover { - background: var(--surface-hover, #333); -} - -.team-details { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid var(--border-color, #333); -} - -.team-members h4, -.team-tasks h4 { - font-size: 12px; - text-transform: uppercase; - color: var(--text-secondary, #999); - margin-bottom: 8px; -} - -.team-member { - display: flex; - align-items: center; - gap: 8px; - padding: 8px; - background: var(--surface-secondary, #222); - border-radius: 4px; - margin-bottom: 4px; - cursor: pointer; -} - -.team-member:hover { - background: var(--surface-hover, #333); -} - -.member-status { - width: 8px; - height: 8px; - border-radius: 50%; -} - -.member-status.active { - background: var(--color-active, #4CAF50); -} - -.member-status.idle { - background: var(--color-warning, #FF9800); -} - -.member-status.offline, -.member-status.unknown { - background: var(--color-done, #666); -} - -.member-name { - font-weight: 500; - color: var(--text-primary, #fff); -} - -.member-type { - color: var(--text-secondary, #999); - font-size: 12px; -} - -.member-model { - margin-left: auto; - font-size: 11px; - color: var(--text-tertiary, #666); -} - -.team-tasks { - margin-top: 16px; -} - -.task-progress { - height: 8px; - background: var(--surface-secondary, #222); - border-radius: 4px; - overflow: hidden; - margin-bottom: 8px; -} - -.progress-bar { - height: 100%; - display: flex; -} - -.progress-bar::before { - content: ''; - height: 100%; - width: calc(var(--completed) / var(--total) * 100%); - background: var(--color-active, #4CAF50); -} - -.progress-bar::after { - content: ''; - height: 100%; - width: calc(var(--in-progress) / var(--total) * 100%); - background: var(--color-warning, #FF9800); -} - -.task-counts { - display: flex; - gap: 12px; - font-size: 12px; -} - -.task-counts .pending { - color: var(--text-secondary, #999); -} - -.task-counts .in-progress { - color: var(--color-warning, #FF9800); -} - -.task-counts .completed { - color: var(--color-active, #4CAF50); -} -``` - -### IMP-8: Parent-Child Navigation - -**Fulfills**: AC-20, AC-21, AC-22 - -**File**: `dashboard/components/ConversationModal.js` (modifications) - -Add header showing parent/child relationship: - -```javascript -function ConversationHeader({ session, subagent, onNavigateToParent }) { - if (subagent) { - return ( -
- {subagent.subagent_type} - -
- ); - } - - return ( -
- {session.session_id.slice(0, 8)} - {session.subagents?.length > 0 && ( - - {session.subagents.length} subagent{session.subagents.length !== 1 ? 's' : ''} - - )} -
- ); -} -``` - -### IMP-9: App.js State Integration - -**Fulfills**: AC-23, AC-24 - -**File**: `dashboard/components/App.js` (modifications) - -Update state management to include teams: - -```javascript -// Add teams to state -const [teams, setTeams] = useState([]); - -// Update fetchState to handle teams -const fetchState = useCallback(async () => { - const response = await fetch('/api/state'); - const data = await response.json(); - setSessions(data.sessions || []); - setTeams(data.teams || []); // NEW -}, []); - -// Add handler for subagent selection -const handleSelectSubagent = useCallback((session, subagent) => { - setSelectedSubagent({ session, subagent }); - setModalOpen(true); -}, []); - -// Render teams section -{teams.length > 0 && ( -
-

Agent Teams

- {teams.map(team => ( - - ))} -
-)} -``` - ---- - -## Rollout Slices - -### Slice 1: Subagent Discovery (Backend) - -**Goal**: Discover and expose subagent metadata through the API - -**Tasks**: -1. Create `SubagentMixin` class in `amc_server/mixins/subagent.py` -2. Implement `_discover_subagents_for_session()` -3. Implement `_parse_subagent_metadata()` -4. Implement `_extract_subagent_type_from_parent()` -5. Integrate with `StateMixin._collect_sessions()` -6. Add unit tests for subagent parsing - -**Verification**: -- `GET /api/state` returns `subagents[]` for sessions with subagents -- Subagent type is correctly extracted from parent -- Status detection (running/completed) works - -### Slice 2: Subagent UI (Frontend) - -**Goal**: Display subagents in the dashboard - -**Tasks**: -1. Create `SubagentPanel.js` component -2. Add subagent styles to `styles.css` -3. Integrate panel into `SessionCard.js` -4. Add subagent conversation endpoint (`/api/subagent/{session_id}/{agent_id}`) -5. Add modal view for subagent conversations -6. Test with real sessions - -**Verification**: -- Session cards show subagent badge -- Expanding shows subagent list -- Clicking subagent opens conversation modal -- Running subagents show activity indicator - -### Slice 3: Team Discovery (Backend) - -**Goal**: Discover and expose team metadata through the API - -**Tasks**: -1. Create `TeamMixin` class in `amc_server/mixins/team.py` -2. Implement `_discover_teams()` -3. Implement `_build_team_model()` -4. Implement `_load_team_tasks_summary()` -5. Implement `_link_team_members_to_sessions()` -6. Integrate with `StateMixin._build_state_payload()` -7. Add unit tests - -**Verification**: -- `GET /api/state` returns `teams[]` when teams exist -- Team members are linked to sessions -- Task counts are accurate - -### Slice 4: Team UI (Frontend) - -**Goal**: Display teams in the dashboard - -**Tasks**: -1. Create `TeamCard.js` component -2. Create `TeamMemberList` and `TeamTaskSummary` subcomponents -3. Add team styles to `styles.css` -4. Integrate into `App.js` -5. Add team detail endpoint (`/api/team/{team_name}`) -6. Add navigation from team member to session - -**Verification**: -- Teams appear in dashboard -- Member list shows status -- Task progress bar renders correctly -- Can navigate to member sessions - -### Slice 5: Polish & Integration - -**Goal**: Complete integration and edge case handling - -**Tasks**: -1. Add parent-child navigation in conversation modals -2. Handle stale/cleaned-up subagent files gracefully -3. Add loading states for async data -4. Test with real multi-agent workflows -5. Performance testing with many subagents -6. Documentation update - -**Verification**: -- End-to-end workflow works -- No errors with missing/stale files -- Performance acceptable with 10+ subagents -- README updated - ---- - -## Testing Strategy - -### Unit Tests - -1. **Subagent parsing**: - - Extract agent_id from various filename formats - - Parse metadata from sample JSONL - - Extract subagent_type from parent Task tool calls - - Status detection logic - -2. **Team parsing**: - - Parse sample config.json - - Load task summaries - - Timestamp conversion - - Member-session linking - -### Integration Tests - -1. **API endpoints**: - - `/api/state` includes subagents and teams - - `/api/subagent/{session_id}/{agent_id}` returns conversation - - `/api/team/{team_name}` returns team details - -### Manual Testing - -1. Run Claude Code with Task tool to create subagents -2. Verify subagents appear in dashboard -3. Create agent team with TeamCreate -4. Verify team appears in dashboard -5. Test navigation between parent and child conversations - ---- - -## Open Questions - -1. **Performance**: With many subagents (10+), should we lazy-load conversations or batch? - -2. **Staleness**: How long should completed subagent data persist? Follow session cleanup rules? - -3. **Team member sessions**: How to reliably link team members to their sessions when multiple sessions exist? - -4. **Real-time updates**: Should we add file watchers for subagent/team directories, or rely on polling? - ---- - -## References - -- Claude Code Task tool documentation -- Claude Code agent teams documentation -- Existing AMC codebase (mixins, dashboard) -- Sample JSONL files from actual sessions +- **AC-8**: Turn stats area shows agent count button when subagents exist +- **AC-9**: Button shows count + running indicator (e.g., "3 agents" or "2 agents (1 running)") +- **AC-10**: Clicking button opens popover with: name, status, duration, token count +- **AC-11**: Running agents show activity indicator +- **AC-12**: List updates via existing polling/SSE