feat: add real-time subagent visibility - monitor Claude Code background agents

- Add SubagentWatcher service that discovers and tails Claude Code's subagent
  transcript files (~/.claude/projects/.../subagents/agent-*.jsonl)
- Track tool calls, progress events, and messages in real-time via file watching
- Add SSE events for subagent:discovered, subagent:tool_call, subagent:progress,
  subagent:message, subagent:completed
- Add REST API endpoints: GET /api/subagents, /api/subagents/:id,
  /api/subagents/:id/transcript
- Add subagent panel in Monitor section showing active agents and their activity
- Add floating draggable windows that auto-open for each active agent
- Add standalone CLI tool: scripts/watch-subagents.ts for terminal monitoring

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-25 14:33:12 +01:00
co-authored by Claude Opus 4.5
parent 4c34966827
commit ad24020cdc
6 changed files with 1907 additions and 0 deletions
+477
View File
@@ -372,6 +372,14 @@ class ClaudemanApp {
// Ralph loop/todo state per session
this.ralphStates = new Map(); // Map<sessionId, { loop, todos }>
// Subagent (Claude Code background agent) tracking
this.subagents = new Map(); // Map<agentId, SubagentInfo>
this.subagentActivity = new Map(); // Map<agentId, activity[]> - recent tool calls/progress
this.activeSubagentId = null; // Currently selected subagent for detail view
this.subagentPanelVisible = false;
this.subagentWindows = new Map(); // Map<agentId, { element, position }>
this.subagentWindowZIndex = 1000;
this.ralphStatePanelCollapsed = true; // Default to collapsed
// Tab alert states: Map<sessionId, 'action' | 'idle'>
@@ -1286,6 +1294,76 @@ class ClaudemanApp {
message: data.reason || 'Claude has finished responding',
});
});
// ========== Subagent Events (Claude Code Background Agents) ==========
this.eventSource.addEventListener('subagent:discovered', (e) => {
const data = JSON.parse(e.data);
this.subagents.set(data.agentId, data);
this.subagentActivity.set(data.agentId, []);
this.renderSubagentPanel();
// Auto-open window for new active agents
if (data.status === 'active') {
this.openSubagentWindow(data.agentId);
}
});
this.eventSource.addEventListener('subagent:tool_call', (e) => {
const data = JSON.parse(e.data);
const activity = this.subagentActivity.get(data.agentId) || [];
activity.push({ type: 'tool', ...data });
if (activity.length > 100) activity.shift(); // Keep last 100 entries
this.subagentActivity.set(data.agentId, activity);
if (this.activeSubagentId === data.agentId) {
this.renderSubagentDetail();
}
this.renderSubagentPanel();
// Update floating window
if (this.subagentWindows.has(data.agentId)) {
this.renderSubagentWindowContent(data.agentId);
}
});
this.eventSource.addEventListener('subagent:progress', (e) => {
const data = JSON.parse(e.data);
const activity = this.subagentActivity.get(data.agentId) || [];
activity.push({ type: 'progress', ...data });
if (activity.length > 100) activity.shift();
this.subagentActivity.set(data.agentId, activity);
if (this.activeSubagentId === data.agentId) {
this.renderSubagentDetail();
}
// Update floating window
if (this.subagentWindows.has(data.agentId)) {
this.renderSubagentWindowContent(data.agentId);
}
});
this.eventSource.addEventListener('subagent:message', (e) => {
const data = JSON.parse(e.data);
const activity = this.subagentActivity.get(data.agentId) || [];
activity.push({ type: 'message', ...data });
if (activity.length > 100) activity.shift();
this.subagentActivity.set(data.agentId, activity);
if (this.activeSubagentId === data.agentId) {
this.renderSubagentDetail();
}
// Update floating window
if (this.subagentWindows.has(data.agentId)) {
this.renderSubagentWindowContent(data.agentId);
}
});
this.eventSource.addEventListener('subagent:completed', (e) => {
const data = JSON.parse(e.data);
const existing = this.subagents.get(data.agentId);
if (existing) {
existing.status = 'completed';
this.subagents.set(data.agentId, existing);
}
this.renderSubagentPanel();
this.updateSubagentWindows();
});
}
setConnectionStatus(status) {
@@ -1339,6 +1417,15 @@ class ClaudemanApp {
this.updateCost();
this.renderSessionTabs();
// Load subagents
if (data.subagents) {
this.subagents.clear();
data.subagents.forEach(s => {
this.subagents.set(s.agentId, s);
});
this.renderSubagentPanel();
}
// Auto-select first session if any
if (this.sessions.size > 0 && !this.activeSessionId) {
const firstSession = this.sessions.values().next().value;
@@ -3714,6 +3801,396 @@ class ClaudemanApp {
return this.getRalphTaskIcon(status);
}
// ========== Subagent Panel (Claude Code Background Agents) ==========
toggleSubagentPanel() {
this.subagentPanelVisible = !this.subagentPanelVisible;
const panel = this.$('subagentPanel');
if (panel) {
panel.style.display = this.subagentPanelVisible ? 'block' : 'none';
}
if (this.subagentPanelVisible) {
this.renderSubagentPanel();
}
}
renderSubagentPanel() {
const panel = this.$('subagentPanel');
const list = this.$('subagentList');
const badge = this.$('subagentBadge');
if (!list) return;
// Update badge count
const activeCount = Array.from(this.subagents.values()).filter(s => s.status === 'active').length;
if (badge) {
badge.textContent = activeCount > 0 ? activeCount : '';
badge.style.display = activeCount > 0 ? 'inline-block' : 'none';
}
// If panel is not visible and there are active subagents, show indicator
if (!this.subagentPanelVisible && activeCount === 0) {
return;
}
// Render subagent list
if (this.subagents.size === 0) {
list.innerHTML = '<div class="subagent-empty">No background agents detected</div>';
return;
}
const html = [];
const sorted = Array.from(this.subagents.values()).sort((a, b) => {
// Active first, then by last activity
if (a.status === 'active' && b.status !== 'active') return -1;
if (b.status === 'active' && a.status !== 'active') return 1;
return new Date(b.lastActivityAt).getTime() - new Date(a.lastActivityAt).getTime();
});
for (const agent of sorted) {
const isActive = this.activeSubagentId === agent.agentId;
const statusClass = agent.status === 'active' ? 'active' : agent.status === 'idle' ? 'idle' : 'completed';
const activity = this.subagentActivity.get(agent.agentId) || [];
const lastActivity = activity[activity.length - 1];
const lastTool = lastActivity?.type === 'tool' ? lastActivity.tool : null;
const hasWindow = this.subagentWindows.has(agent.agentId);
html.push(`
<div class="subagent-item ${statusClass} ${isActive ? 'selected' : ''}"
onclick="app.selectSubagent('${agent.agentId}')">
<div class="subagent-header">
<span class="subagent-icon">🤖</span>
<span class="subagent-id">${agent.agentId.substring(0, 7)}</span>
<span class="subagent-status ${statusClass}">${agent.status}</span>
<button class="subagent-window-btn" onclick="event.stopPropagation(); app.${hasWindow ? 'closeSubagentWindow' : 'openSubagentWindow'}('${agent.agentId}')" title="${hasWindow ? 'Close window' : 'Open in window'}">
${hasWindow ? '✕' : '⧉'}
</button>
</div>
<div class="subagent-meta">
<span class="subagent-tools">${agent.toolCallCount} tools</span>
${lastTool ? `<span class="subagent-last-tool">${this.getToolIcon(lastTool)} ${lastTool}</span>` : ''}
</div>
</div>
`);
}
list.innerHTML = html.join('');
}
selectSubagent(agentId) {
this.activeSubagentId = agentId;
this.renderSubagentPanel();
this.renderSubagentDetail();
}
renderSubagentDetail() {
const detail = this.$('subagentDetail');
if (!detail) return;
if (!this.activeSubagentId) {
detail.innerHTML = '<div class="subagent-empty">Select an agent to view details</div>';
return;
}
const agent = this.subagents.get(this.activeSubagentId);
const activity = this.subagentActivity.get(this.activeSubagentId) || [];
if (!agent) {
detail.innerHTML = '<div class="subagent-empty">Agent not found</div>';
return;
}
const activityHtml = activity.slice(-30).map(a => {
const time = new Date(a.timestamp).toLocaleTimeString();
if (a.type === 'tool') {
return `<div class="subagent-activity tool">
<span class="time">${time}</span>
<span class="icon">${this.getToolIcon(a.tool)}</span>
<span class="name">${a.tool}</span>
<span class="detail">${this.getToolDetail(a.tool, a.input)}</span>
</div>`;
} else if (a.type === 'progress') {
const icon = a.progressType === 'query_update' ? '⟳' : '✓';
return `<div class="subagent-activity progress">
<span class="time">${time}</span>
<span class="icon">${icon}</span>
<span class="detail">${a.query || a.progressType}</span>
</div>`;
} else if (a.type === 'message') {
const preview = a.text.length > 100 ? a.text.substring(0, 100) + '...' : a.text;
return `<div class="subagent-activity message">
<span class="time">${time}</span>
<span class="icon">💬</span>
<span class="detail">${this.escapeHtml(preview)}</span>
</div>`;
}
return '';
}).join('');
detail.innerHTML = `
<div class="subagent-detail-header">
<span class="subagent-id">Agent ${agent.agentId}</span>
<span class="subagent-status ${agent.status}">${agent.status}</span>
<button class="subagent-transcript-btn" onclick="app.viewSubagentTranscript('${agent.agentId}')">
View Full Transcript
</button>
</div>
<div class="subagent-detail-stats">
<span>Tools: ${agent.toolCallCount}</span>
<span>Entries: ${agent.entryCount}</span>
<span>Size: ${(agent.fileSize / 1024).toFixed(1)}KB</span>
</div>
<div class="subagent-activity-log">
${activityHtml || '<div class="subagent-empty">No activity yet</div>'}
</div>
`;
}
getToolIcon(tool) {
const icons = {
WebSearch: '🔍',
WebFetch: '🌐',
Read: '📖',
Write: '📝',
Edit: '✏️',
Bash: '💻',
Glob: '📁',
Grep: '🔎',
Task: '🤖',
};
return icons[tool] || '🔧';
}
getToolDetail(tool, input) {
if (!input) return '';
if (tool === 'WebSearch' && input.query) return `"${input.query}"`;
if (tool === 'WebFetch' && input.url) return input.url;
if (tool === 'Read' && input.file_path) return input.file_path;
if ((tool === 'Write' || tool === 'Edit') && input.file_path) return input.file_path;
if (tool === 'Bash' && input.command) {
const cmd = input.command;
return cmd.length > 40 ? cmd.substring(0, 40) + '...' : cmd;
}
if (tool === 'Glob' && input.pattern) return input.pattern;
if (tool === 'Grep' && input.pattern) return input.pattern;
return '';
}
async viewSubagentTranscript(agentId) {
try {
const res = await fetch(`/api/subagents/${agentId}/transcript?format=formatted`);
const data = await res.json();
if (!data.success) {
alert('Failed to load transcript');
return;
}
// Show in a modal or new window
const content = data.data.formatted.join('\n');
const win = window.open('', '_blank', 'width=800,height=600');
win.document.write(`
<html>
<head>
<title>Subagent ${agentId} Transcript</title>
<style>
body { background: #1a1a2e; color: #eee; font-family: monospace; padding: 20px; }
pre { white-space: pre-wrap; word-wrap: break-word; }
</style>
</head>
<body>
<h2>Subagent ${agentId} Transcript (${data.data.entryCount} entries)</h2>
<pre>${this.escapeHtml(content)}</pre>
</body>
</html>
`);
} catch (err) {
console.error('Failed to load transcript:', err);
alert('Failed to load transcript: ' + err.message);
}
}
// ========== Subagent Floating Windows ==========
openSubagentWindow(agentId) {
// If window already exists, focus it
if (this.subagentWindows.has(agentId)) {
const existing = this.subagentWindows.get(agentId);
existing.element.style.zIndex = ++this.subagentWindowZIndex;
return;
}
const agent = this.subagents.get(agentId);
if (!agent) return;
// Calculate position (cascade from top-left)
const windowCount = this.subagentWindows.size;
const offsetX = 50 + (windowCount % 5) * 30;
const offsetY = 100 + (windowCount % 5) * 30;
// Create window element
const win = document.createElement('div');
win.className = 'subagent-window';
win.id = `subagent-window-${agentId}`;
win.style.left = `${offsetX}px`;
win.style.top = `${offsetY}px`;
win.style.zIndex = ++this.subagentWindowZIndex;
win.innerHTML = `
<div class="subagent-window-header">
<div class="subagent-window-title">
<span class="icon">🤖</span>
<span class="id">${agentId.substring(0, 7)}</span>
<span class="status ${agent.status}">${agent.status}</span>
</div>
<div class="subagent-window-actions">
<button onclick="app.minimizeSubagentWindow('${agentId}')" title="Minimize">─</button>
<button onclick="app.closeSubagentWindow('${agentId}')" title="Close">×</button>
</div>
</div>
<div class="subagent-window-body" id="subagent-window-body-${agentId}">
<div class="subagent-empty">Loading activity...</div>
</div>
`;
document.body.appendChild(win);
// Make draggable
this.makeWindowDraggable(win, win.querySelector('.subagent-window-header'));
// Store reference
this.subagentWindows.set(agentId, {
element: win,
minimized: false,
});
// Render content
this.renderSubagentWindowContent(agentId);
// Focus on click
win.addEventListener('mousedown', () => {
win.style.zIndex = ++this.subagentWindowZIndex;
});
}
closeSubagentWindow(agentId) {
const windowData = this.subagentWindows.get(agentId);
if (windowData) {
windowData.element.remove();
this.subagentWindows.delete(agentId);
}
}
minimizeSubagentWindow(agentId) {
const windowData = this.subagentWindows.get(agentId);
if (windowData) {
windowData.element.style.display = 'none';
windowData.minimized = true;
}
}
restoreSubagentWindow(agentId) {
const windowData = this.subagentWindows.get(agentId);
if (windowData) {
windowData.element.style.display = 'flex';
windowData.element.style.zIndex = ++this.subagentWindowZIndex;
windowData.minimized = false;
}
}
makeWindowDraggable(win, handle) {
let isDragging = false;
let startX, startY, startLeft, startTop;
handle.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(win.style.left) || 0;
startTop = parseInt(win.style.top) || 0;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
win.style.left = `${startLeft + dx}px`;
win.style.top = `${startTop + dy}px`;
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
renderSubagentWindowContent(agentId) {
const body = document.getElementById(`subagent-window-body-${agentId}`);
if (!body) return;
const activity = this.subagentActivity.get(agentId) || [];
if (activity.length === 0) {
body.innerHTML = '<div class="subagent-empty">No activity yet</div>';
return;
}
const html = activity.slice(-100).map(a => {
const time = new Date(a.timestamp).toLocaleTimeString();
if (a.type === 'tool') {
return `<div class="activity-line">
<span class="time">${time}</span>
<span class="tool-icon">${this.getToolIcon(a.tool)}</span>
<span class="tool-name">${a.tool}</span>
<span class="tool-detail">${this.escapeHtml(this.getToolDetail(a.tool, a.input))}</span>
</div>`;
} else if (a.type === 'progress') {
const icon = a.progressType === 'query_update' ? '⟳' : '✓';
return `<div class="activity-line progress-line">
<span class="time">${time}</span>
<span class="tool-icon">${icon}</span>
<span class="tool-detail">${this.escapeHtml(a.query || a.progressType)}</span>
</div>`;
} else if (a.type === 'message') {
const preview = a.text.length > 150 ? a.text.substring(0, 150) + '...' : a.text;
return `<div class="message-line">
<span class="time">${time}</span> 💬 ${this.escapeHtml(preview)}
</div>`;
}
return '';
}).join('');
body.innerHTML = html;
body.scrollTop = body.scrollHeight;
}
// Update all open subagent windows
updateSubagentWindows() {
for (const agentId of this.subagentWindows.keys()) {
this.renderSubagentWindowContent(agentId);
// Update status in header
const agent = this.subagents.get(agentId);
if (agent) {
const statusEl = document.querySelector(`#subagent-window-${agentId} .status`);
if (statusEl) {
statusEl.className = `status ${agent.status}`;
statusEl.textContent = agent.status;
}
}
}
}
// Open windows for all active subagents
openAllActiveSubagentWindows() {
for (const [agentId, agent] of this.subagents) {
if (agent.status === 'active' && !this.subagentWindows.has(agentId)) {
this.openSubagentWindow(agentId);
}
}
}
// ========== Screen Sessions (in Monitor Panel) ==========
async loadScreens() {
+16
View File
@@ -272,6 +272,22 @@
<div class="monitor-section-body" id="backgroundTasksBody">
</div>
</div>
<div class="monitor-section" id="subagentSection">
<div class="monitor-section-header">
<span>Subagents</span>
<span class="subagent-badge" id="subagentBadge" style="display: none;"></span>
</div>
<div class="monitor-section-body">
<div class="subagent-container">
<div class="subagent-list" id="subagentList">
<div class="subagent-empty">No background agents detected</div>
</div>
<div class="subagent-detail" id="subagentDetail">
<div class="subagent-empty">Select an agent to view details</div>
</div>
</div>
</div>
</div>
</div>
</div>
+319
View File
@@ -2997,3 +2997,322 @@ kbd {
border-radius: 8px 8px 0 0;
}
}
/* ========== Subagent Panel (Claude Code Background Agents) ========== */
.subagent-container {
display: flex;
gap: 0.5rem;
height: 200px;
}
.subagent-list {
flex: 0 0 200px;
overflow-y: auto;
border-right: 1px solid var(--border);
padding-right: 0.5rem;
}
.subagent-detail {
flex: 1;
overflow-y: auto;
padding-left: 0.5rem;
}
.subagent-empty {
color: var(--text-muted);
font-size: 0.75rem;
text-align: center;
padding: 1rem;
}
.subagent-badge {
background: var(--accent);
color: white;
font-size: 0.65rem;
padding: 0.1rem 0.4rem;
border-radius: 8px;
margin-left: 0.5rem;
}
.subagent-item {
padding: 0.5rem;
border-radius: 6px;
background: var(--bg-elevated);
margin-bottom: 0.4rem;
cursor: pointer;
transition: background 0.15s;
}
.subagent-item:hover {
background: var(--bg-card);
}
.subagent-item.selected {
background: var(--bg-card);
border: 1px solid var(--accent);
}
.subagent-header {
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 0.3rem;
}
.subagent-icon {
font-size: 0.9rem;
}
.subagent-id {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--text);
}
.subagent-status {
font-size: 0.6rem;
padding: 0.1rem 0.3rem;
border-radius: 4px;
margin-left: auto;
}
.subagent-status.active {
background: var(--success);
color: white;
}
.subagent-status.idle {
background: var(--warning);
color: black;
}
.subagent-status.completed {
background: var(--text-muted);
color: white;
}
.subagent-window-btn {
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 0.1rem 0.3rem;
font-size: 0.75rem;
margin-left: auto;
}
.subagent-window-btn:hover {
color: var(--accent);
}
.subagent-meta {
display: flex;
gap: 0.5rem;
font-size: 0.65rem;
color: var(--text-muted);
}
.subagent-last-tool {
color: var(--accent);
}
/* Subagent Detail */
.subagent-detail-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.subagent-detail-header .subagent-id {
font-size: 0.85rem;
font-weight: 600;
}
.subagent-transcript-btn {
margin-left: auto;
font-size: 0.65rem;
padding: 0.2rem 0.5rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
cursor: pointer;
}
.subagent-transcript-btn:hover {
background: var(--bg-elevated);
}
.subagent-detail-stats {
display: flex;
gap: 1rem;
font-size: 0.7rem;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.subagent-activity-log {
font-family: var(--font-mono);
font-size: 0.7rem;
max-height: 150px;
overflow-y: auto;
}
.subagent-activity {
display: flex;
gap: 0.4rem;
padding: 0.2rem 0;
border-bottom: 1px solid var(--border-subtle);
}
.subagent-activity .time {
color: var(--text-muted);
flex: 0 0 60px;
}
.subagent-activity .icon {
flex: 0 0 20px;
}
.subagent-activity .name {
color: var(--accent);
flex: 0 0 80px;
}
.subagent-activity .detail {
color: var(--text-dim);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.subagent-activity.tool .icon {
color: var(--accent);
}
.subagent-activity.progress .icon {
color: var(--warning);
}
.subagent-activity.message .icon {
color: var(--text);
}
/* ========== Subagent Windows (Floating) ========== */
.subagent-window {
position: fixed;
width: 500px;
height: 400px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 1000;
display: flex;
flex-direction: column;
overflow: hidden;
}
.subagent-window-header {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--bg-elevated);
border-bottom: 1px solid var(--border);
cursor: move;
}
.subagent-window-title {
display: flex;
align-items: center;
gap: 0.4rem;
flex: 1;
}
.subagent-window-title .icon {
font-size: 1rem;
}
.subagent-window-title .id {
font-family: var(--font-mono);
font-size: 0.8rem;
}
.subagent-window-title .status {
font-size: 0.6rem;
padding: 0.1rem 0.3rem;
border-radius: 4px;
}
.subagent-window-title .status.active {
background: var(--success);
color: white;
}
.subagent-window-actions {
display: flex;
gap: 0.3rem;
}
.subagent-window-actions button {
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 0.2rem 0.4rem;
font-size: 0.9rem;
}
.subagent-window-actions button:hover {
color: var(--text);
}
.subagent-window-body {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
font-family: var(--font-mono);
font-size: 0.75rem;
}
.subagent-window-body .activity-line {
display: flex;
gap: 0.4rem;
padding: 0.15rem 0;
}
.subagent-window-body .activity-line .time {
color: var(--text-muted);
flex: 0 0 70px;
}
.subagent-window-body .activity-line .tool-icon {
flex: 0 0 20px;
}
.subagent-window-body .activity-line .tool-name {
color: var(--accent);
flex: 0 0 90px;
}
.subagent-window-body .activity-line .tool-detail {
color: var(--text-dim);
flex: 1;
}
.subagent-window-body .message-line {
padding: 0.3rem 0;
color: var(--text);
}
.subagent-window-body .progress-line {
color: var(--warning);
font-style: italic;
}
+89
View File
@@ -27,6 +27,7 @@ import { getStore } from '../state-store.js';
import { generateClaudeMd } from '../templates/claude-md.js';
import { parseRalphLoopConfig, extractCompletionPhrase } from '../ralph-config.js';
import { writeHooksConfig } from '../hooks-config.js';
import { subagentWatcher, type SubagentInfo, type SubagentToolCall, type SubagentProgress, type SubagentMessage } from '../subagent-watcher.js';
import { v4 as uuidv4 } from 'uuid';
import { createRequire } from 'node:module';
@@ -288,6 +289,39 @@ export class WebServer extends EventEmitter {
// Initialize spawn orchestrator
this.spawnOrchestrator = new SpawnOrchestrator();
this.setupSpawnOrchestratorListeners();
// Set up subagent watcher listeners
this.setupSubagentWatcherListeners();
}
/**
* Set up event listeners for subagent watcher.
* Broadcasts real-time subagent activity to SSE clients.
*/
private setupSubagentWatcherListeners(): void {
subagentWatcher.on('subagent:discovered', (info: SubagentInfo) => {
this.broadcast('subagent:discovered', info);
});
subagentWatcher.on('subagent:tool_call', (data: SubagentToolCall) => {
this.broadcast('subagent:tool_call', data);
});
subagentWatcher.on('subagent:progress', (data: SubagentProgress) => {
this.broadcast('subagent:progress', data);
});
subagentWatcher.on('subagent:message', (data: SubagentMessage) => {
this.broadcast('subagent:message', data);
});
subagentWatcher.on('subagent:completed', (info: SubagentInfo) => {
this.broadcast('subagent:completed', info);
});
subagentWatcher.on('subagent:error', (error: Error, agentId?: string) => {
console.error(`[SubagentWatcher] Error${agentId ? ` for ${agentId}` : ''}:`, error.message);
});
}
private async setupRoutes(): Promise<void> {
@@ -1554,6 +1588,53 @@ export class WebServer extends EventEmitter {
return { success: true, data: { agentId } };
});
// ========== Subagent Monitoring (Claude Code Background Agents) ==========
// List all known subagents
this.app.get('/api/subagents', async (req) => {
const { minutes } = req.query as { minutes?: string };
const subagents = minutes
? subagentWatcher.getRecentSubagents(parseInt(minutes, 10))
: subagentWatcher.getSubagents();
return { success: true, data: subagents };
});
// Get subagents for a specific session (by working directory)
this.app.get('/api/sessions/:id/subagents', async (req) => {
const { id } = req.params as { id: string };
const session = this.sessions.get(id);
if (!session) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${id} not found`);
}
const subagents = subagentWatcher.getSubagentsForSession(session.workingDir);
return { success: true, data: subagents };
});
// Get a specific subagent's info
this.app.get('/api/subagents/:agentId', async (req) => {
const { agentId } = req.params as { agentId: string };
const info = subagentWatcher.getSubagent(agentId);
if (!info) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, `Subagent ${agentId} not found`);
}
return { success: true, data: info };
});
// Get a subagent's transcript
this.app.get('/api/subagents/:agentId/transcript', async (req) => {
const { agentId } = req.params as { agentId: string };
const { limit, format } = req.query as { limit?: string; format?: 'raw' | 'formatted' };
const limitNum = limit ? parseInt(limit, 10) : undefined;
const transcript = await subagentWatcher.getTranscript(agentId, limitNum);
if (format === 'formatted') {
const formatted = subagentWatcher.formatTranscript(transcript);
return { success: true, data: { formatted, entryCount: transcript.length } };
}
return { success: true, data: transcript };
});
// ========== Hook Events ==========
this.app.post('/api/hook-event', async (req) => {
@@ -2314,6 +2395,7 @@ export class WebServer extends EventEmitter {
scheduledRuns: Array.from(this.scheduledRuns.values()),
respawnStatus,
globalStats: this.store.getAggregateStats(activeSessionTokens),
subagents: subagentWatcher.getRecentSubagents(60), // Last hour of subagent activity
timestamp: Date.now(),
};
}
@@ -2520,6 +2602,10 @@ export class WebServer extends EventEmitter {
// Restore screen sessions from previous run
await this.restoreScreenSessions();
// Start subagent watcher for Claude Code background agent visibility
subagentWatcher.start();
console.log('Subagent watcher started - monitoring ~/.claude/projects for background agent activity');
}
private async restoreScreenSessions(): Promise<void> {
@@ -2719,6 +2805,9 @@ export class WebServer extends EventEmitter {
// Flush state store to prevent data loss from debounced saves
this.store.flushAll();
// Stop subagent watcher
subagentWatcher.stop();
await this.app.close();
}
}