# Subagent & Agent Team Visibility for AMC > **Version**: 1.0 > **Status**: Draft > **Last Updated**: 2026-02-26 ## 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. --- ## Problem Statement 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 --- ## Acceptance Criteria ### Subagent 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) ### Subagent Display - **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 ### Subagent Status - **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 ### Team Discovery - **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