Sub-Agent Activity Visibility Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Keep the UI active and informative when Claude spawns sub-agents (Task tool), preventing the stall timeout from killing the streaming indicator.
Architecture: Three-part fix: (1) server emits sub-agent-aware heartbeat with varying text to avoid client dedup, (2) client extends stall timeout for sessions with active sub-agents, (3) UI shows elapsed timer and description on Task tool bubbles.
Tech Stack: Node.js server (claude-cli.js), React hooks (useProjectWebSocketV2.js), React components (ChatInterface.jsx)
Task 1: Server โ Track Active Sub-Agents in Process Entry
Files:
- Modify:
claudecodeui/server/claude-cli.js:2643-2709
Step 1: Add sub-agent detection after logToolEvents() call
After the existing logToolEvents() call at line 2644, add detection for Task tool_use and tool_result to track active sub-agents in the process entry. Insert this block after line 2651 (the closing }/) of logToolEvents), before the AskUserQuestion detection at line 2653:
// Track active sub-agents (Task tool) for heartbeat awareness (fix for #41)
if (Array.isArray(messageContent)) {
for (const item of messageContent) {
if (!item || typeof item !== 'object') continue;
if (item.type === 'tool_use' && item.name === 'Task' && item.id) {
const entry = activeClaudeProcesses.get(effectiveSessionId());
if (entry) {
if (!entry.activeSubagentIds) entry.activeSubagentIds = new Set();
entry.activeSubagentIds.add(item.id);
if (!entry.subagentStartedAt) entry.subagentStartedAt = Date.now();
const description = item.input?.description || item.input?.prompt?.slice(0, 80) || 'processing';
console.log(`๐ค [SUBAGENT] Started: toolId=${item.id} description="${description}" sessionId=${effectiveSessionId() || 'none'}`);
// Emit immediate status so client knows a sub-agent is running
wsHolder.send({
type: 'claude-status',
sessionId: effectiveSessionId(),
projectName: projectNameDerived || null,
conversationId: conversationIdForEvent,
data: {
type: 'subagent-started',
message: `Sub-agent started: ${description}`,
toolId: item.id,
description
},
ts: Date.now(),
traceId: traceId || null
}, { context: 'subagent-status' });
}
}
if (item.type === 'tool_result' && item.tool_use_id) {
const entry = activeClaudeProcesses.get(effectiveSessionId());
if (entry?.activeSubagentIds?.has(item.tool_use_id)) {
entry.activeSubagentIds.delete(item.tool_use_id);
console.log(`๐ค [SUBAGENT] Completed: toolId=${item.tool_use_id} remaining=${entry.activeSubagentIds.size} sessionId=${effectiveSessionId() || 'none'}`);
if (entry.activeSubagentIds.size === 0) {
entry.subagentStartedAt = null;
}
}
}
}
}
Step 2: Verify the change compiles
Run: cd claudecodeui && npx vite build 2>&1 | tail -5
Expected: Build succeeds (server code isn't bundled by Vite, but check for syntax errors via node -c server/claude-cli.js)
Run: cd claudecodeui && node -c server/claude-cli.js
Expected: No output (syntax OK)
Step 3: Commit
git add claudecodeui/server/claude-cli.js
git commit -m "feat(server): track active sub-agents in process entry (issue #41)"
Task 2: Server โ Sub-Agent-Aware Heartbeat
Files:
- Modify:
claudecodeui/server/claude-cli.js:2360-2381
Step 1: Modify the heartbeat to send sub-agent-specific messages
Replace the heartbeat interval handler (lines 2364-2381) with sub-agent awareness. The key change: when sub-agents are active, always send a heartbeat with varying text (includes elapsed seconds) to bypass client-side dedup:
entry.heartbeatTimer = setInterval(() => {
const current = activeClaudeProcesses.get(sid);
if (!current) { stopHeartbeat(sid); return; }
const hasActiveSubagents = current.activeSubagentIds?.size > 0;
// For sub-agents: always send (they need keepalive). For normal: only if quiet.
if (!hasActiveSubagents) {
const quietThreshold = HEARTBEAT_INTERVAL_MS * 0.8;
if (Date.now() - current.lastActivityAt < quietThreshold) return;
}
let statusText;
let dataPayload;
if (hasActiveSubagents) {
const elapsed = Math.round((Date.now() - (current.subagentStartedAt || Date.now())) / 1000);
const count = current.activeSubagentIds.size;
statusText = count > 1
? `${count} sub-agents processing... (${elapsed}s)`
: `Sub-agent processing... (${elapsed}s)`;
dataPayload = {
type: 'subagent-heartbeat',
message: statusText,
subagentCount: count,
elapsedSeconds: elapsed
};
} else {
statusText = current.lastStatus?.message || 'Processing...';
dataPayload = { type: 'heartbeat', message: statusText };
}
console.log(`๐ [HEARTBEAT] session=${sid} status="${statusText}"${hasActiveSubagents ? ' (subagent)' : ''}`);
wsHolder.send({
type: 'claude-status',
sessionId: sid,
projectName: projectNameDerived || null,
conversationId: conversationIdForEvent,
data: dataPayload,
ts: Date.now(),
traceId: traceId || null
}, { context: 'heartbeat' });
}, HEARTBEAT_INTERVAL_MS);
Step 2: Verify syntax
Run: cd claudecodeui && node -c server/claude-cli.js
Expected: No output (syntax OK)
Step 3: Commit
git add claudecodeui/server/claude-cli.js
git commit -m "feat(server): sub-agent-aware heartbeat with varying status text (issue #41)"
Task 3: Client โ Track Active Sub-Agent Sessions and Extend Stall Timeout
Files:
- Modify:
claudecodeui/src/hooks/useProjectWebSocketV2.js:60-65,375-427,1003-1042,1170-1200,1207-1230
Step 1: Add sub-agent timeout constant and tracking ref
After STREAM_STALL_TIMEOUT_MS (line 65), add:
const SUBAGENT_STALL_TIMEOUT_MS = 600000; // 10 min for sub-agent runs (fix #41)
After streamedToolIdsRef (line 77), add:
const activeSubagentSessionsRef = useRef(new Set()); // sessions with running sub-agents (fix #41)
Step 2: Modify scheduleStallWatchdog() to use extended timeout
In scheduleStallWatchdog() at line 375, replace the timeout value. Change line 427:
}, STREAM_STALL_TIMEOUT_MS);
to:
}, activeSubagentSessionsRef.current.has(sid) ? SUBAGENT_STALL_TIMEOUT_MS : STREAM_STALL_TIMEOUT_MS);
Also update the debug log at line 382 to include the actual timeout used:
const timeoutMs = activeSubagentSessionsRef.current.has(sid) ? SUBAGENT_STALL_TIMEOUT_MS : STREAM_STALL_TIMEOUT_MS;
debugLog('stream', 'stall-timeout', { sid, reason, ms: timeoutMs });
Step 3: Track Task tool_use as sub-agent session
In the tool_use emission block (around line 1020-1038), after streamedToolIdsRef.current.add(part.id); (line 1023), add:
// Track sub-agent sessions for extended stall timeout (fix #41)
if (part.name === 'Task') {
activeSubagentSessionsRef.current.add(sid);
// Reschedule stall watchdog with extended timeout immediately
scheduleStallWatchdog(sid, 'subagent-started');
}
Step 4: Handle sub-agent heartbeat in claude-status handler
In the claude-status handler (line 1170), after the text extraction (line 1178) but before the if (text) check (line 1179), add handling for subagent-heartbeat type:
// Sub-agent heartbeat: ensure session stays in sub-agent tracking (fix #41)
if (statusData.type === 'subagent-heartbeat' || statusData.type === 'subagent-started') {
activeSubagentSessionsRef.current.add(latestMessage.sessionId);
}
Step 5: Clear sub-agent tracking on stream completion
In the claude-complete handler (around line 1207), after the existing cleanup, add:
activeSubagentSessionsRef.current.delete(sid);
Also in endStream() function body, when the stall timeout fires and calls endStream(), if the session has active sub-agents, do NOT actually end the stream. Modify the stall timeout handler (around line 421) โ before the endStream(sid, isCurrentlyViewing); call at line 421:
// Don't end stream if sub-agents are still active (fix #41)
if (activeSubagentSessionsRef.current.has(sid)) {
debugLog('stream', 'stall-timeout-suppressed', { sid, reason: 'active-subagent' });
// Reschedule with extended timeout
scheduleStallWatchdog(sid, 'subagent-active');
return;
}
Also in streamedToolIdsRef.current.clear() calls (lines 1326 and 1975), add adjacent cleanup:
activeSubagentSessionsRef.current.delete(sid);
Step 6: Verify build
Run: cd claudecodeui && npx vite build 2>&1 | tail -5
Expected: Build succeeds
Step 7: Commit
git add claudecodeui/src/hooks/useProjectWebSocketV2.js
git commit -m "feat(ui): extend stall timeout for sessions with active sub-agents (issue #41)"
Task 4: Client โ Add Task Tool Badge Label and Elapsed Timer
Files:
- Modify:
claudecodeui/src/components/ChatInterface.jsx:588-617,1142-1198
Step 1: Add Task tool badge label
In getToolBadgeLabel() (line 588), add a case for Task before the default at line 615:
case 'Task':
return 'Tool ยท Sub-Agent';
case 'Skill':
return 'Tool ยท Skill';
Step 2: Add Task tool description extraction
Check if getToolDescription() (line 545) handles Task. If not, ensure the descriptions object includes a Task entry. Look at getToolContext() to see how it extracts context from tool input. Add to the descriptions object in getToolDescription():
'Task': `${context || 'Running sub-agent'}`,
And in getToolContext(), add handling for Task tool input:
case 'Task': {
try {
const parsed = typeof toolInput === 'string' ? JSON.parse(toolInput) : toolInput;
return parsed?.description || parsed?.prompt?.slice(0, 60) || null;
} catch { return null; }
}
Step 3: Add elapsed time display for Task tool bubbles
In the tool bubble rendering (around line 1153-1182), for Task tool specifically when toolResultMissing is true, the existing spinner already shows. The elapsed time is already computed via toolExecutions and getElapsedTime (lines 1242-1248). However, since toolExecutions may not have an entry for streamed tool_use events (they're added during streaming), we need to use the message timestamp as fallback.
After the spinner SVG block (line 1176-1179), within the same !showTools conditional, add elapsed time display for sub-agents:
{message.toolName === 'Task' && message.toolResultMissing && (
<SubAgentElapsedTimer startTime={message.timestamp} />
)}
Step 4: Create SubAgentElapsedTimer component
Add this as a simple function component near the top of ChatInterface.jsx (around line 540, near the other helper functions):
// Elapsed timer for sub-agent tool bubbles โ updates every second (fix #41)
const SubAgentElapsedTimer = ({ startTime }) => {
const [elapsed, setElapsed] = React.useState(0);
React.useEffect(() => {
const start = typeof startTime === 'string' ? new Date(startTime).getTime() : (startTime || Date.now());
const update = () => setElapsed(Math.round((Date.now() - start) / 1000));
update();
const interval = setInterval(update, 1000);
return () => clearInterval(interval);
}, [startTime]);
if (elapsed < 2) return null; // Don't show for first 2 seconds
const mins = Math.floor(elapsed / 60);
const secs = elapsed % 60;
const display = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
return <span className="text-[10px] text-blue-400 dark:text-blue-500 ml-1">{display}</span>;
};
Step 5: Verify build
Run: cd claudecodeui && npx vite build 2>&1 | tail -5
Expected: Build succeeds
Step 6: Commit
git add claudecodeui/src/components/ChatInterface.jsx
git commit -m "feat(ui): add sub-agent badge label and elapsed timer on tool bubbles (issue #41)"
Task 5: Integration Test in Docker
Step 1: Build and run Docker container
Run: cd /Users/lindsay/Documents/work/sasha && ./docker-local.sh
Expected: Container starts on http://localhost:3006
Step 2: Trigger a sub-agent session
In the Sasha chat, send a message that triggers Claude to spawn a sub-agent (Task tool). A good prompt:
"Search the codebase for all files that mention 'heartbeat' and summarize what you find."
This should cause Claude to use the Task tool with an Explore agent.
Step 3: Verify behavior
- Watch for the tool bubble to appear with "Tool - Sub-Agent" badge
- Watch for the elapsed timer counting up (e.g., "12s", "1m 3s")
- Check browser console for
[HEARTBEAT]messages with(subagent)suffix - Verify the streaming indicator (spinner) stays active throughout
- Verify no stall timeout in debug logs (
stream-stall-timeoutevent should NOT appear) - Verify the tool bubble shows completion checkmark when sub-agent returns
Step 4: Check server logs
Run: Check docker container logs for [SUBAGENT] Started: and [HEARTBEAT] lines.
Step 5: Commit any fixes discovered during testing
Task 6: Update Icon Registry and Documentation
Step 1: Check if any new icons were added
The SubAgentElapsedTimer uses no new icons โ it reuses existing text styling. No icon registry update needed.
Step 2: Update the chat processing indicators doc
Modify: claudecodeui/docs-developer/features/chat-processing-indicators.md
Add a section about sub-agent visibility:
- Sub-agent heartbeat mechanism
- Extended stall timeout behavior
- Elapsed timer on Task tool bubbles
Step 3: Commit
git add claudecodeui/docs-developer/features/chat-processing-indicators.md
git commit -m "docs: document sub-agent visibility behavior (issue #41)"
