mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
feat: use Task tool description for subagent window titles + auto-minimize completed
- Extract short description from TaskTracker (params.description) for subagent windows - Correlate subagents with sessions by working directory and timing - Auto-minimize subagent windows when agents complete - Persist minimized/open window states to server for cross-browser sync - Restore window states on reconnection/page reload - New API: GET/PUT /api/subagent-window-states - State file: ~/.claudeman/subagent-window-states.json Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ When user says "COM": 1) Increment version in BOTH `package.json` AND `CLAUDE.md
|
||||
|
||||
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
|
||||
|
||||
**Version**: 0.1377 (must match `package.json`)
|
||||
**Version**: 0.1378 (must match `package.json`)
|
||||
|
||||
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1377",
|
||||
"version": "0.1378",
|
||||
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
### Fix for Issue #65
|
||||
**Send/wait state stuck**
|
||||
|
||||
Stuck in waiting_update - the controller is trying to send a command but may not be receiving expected response.
|
||||
|
||||
Check terminal output:
|
||||
```bash
|
||||
curl -sk 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/terminal?tail=10000' | tail -100
|
||||
```
|
||||
|
||||
Try restarting respawn:
|
||||
```bash
|
||||
curl -sk -X POST 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/respawn/stop'
|
||||
curl -sk -X POST 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/respawn/start'
|
||||
```
|
||||
|
||||
|
||||
### Fix for Issue #66
|
||||
**Send/wait state stuck**
|
||||
|
||||
Stuck in waiting_update - the controller is trying to send a command but may not be receiving expected response.
|
||||
|
||||
Check terminal output:
|
||||
```bash
|
||||
curl -sk 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/terminal?tail=10000' | tail -100
|
||||
```
|
||||
|
||||
Try restarting respawn:
|
||||
```bash
|
||||
curl -sk -X POST 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/respawn/stop'
|
||||
curl -sk -X POST 'https://localhost:3000/api/sessions/5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98/respawn/start'
|
||||
```
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
### Issue #65 [WARNING] - 2026-01-26 08:50:53
|
||||
**State stuck: waiting_update for 10+ minutes**
|
||||
|
||||
The respawn controller has been in 'waiting_update' state for over 10 minutes.
|
||||
Time since activity: 389ms
|
||||
Detection status: {"stopHookReceived":false,"stopHookTime":null,"idlePromptReceived":false,"idlePromptTime":null,"completionMessageDetected":false,"completionMessageTime":null,"outputSilent":false,"msSinceLastOutput":389,"tokensStable":true,"lastTokenCount":9900,"msSinceTokenChange":270313,"workingPatternsAbsent":true,"msSinceLastWorking":327049,"aiCheck":{"status":"ready","lastVerdict":"IDLE","lastReasoning":"The terminal output is empty/blank, which typically indicates the session is at rest waiting for input. There are no spinner characters, no activity text, no partial output, and no signs of active processing. An empty terminal with no visible activity suggests the session has completed any previous work and is waiting for new input.","lastCheckDurationMs":6555,"cooldownEndsAt":null,"consecutiveErrors":0,"totalChecks":1,"disabledReason":null},"confidenceLevel":35,"statusText":"Respawn step: waiting_update","waitingFor":"Step completion","activeTimers":[{"name":"step-confirm","remainingMs":5106,"totalMs":10000},{"name":"no-output-fallback","remainingMs":29611,"totalMs":30000},{"name":"pre-filter","remainingMs":9611,"totalMs":10000},{"name":"auto-accept","remainingMs":7611,"totalMs":8000}],"recentActions":[{"type":"timer","detail":"Started auto-accept: 8s (plan mode detection)","timestamp":1769413853255},{"type":"timer-cancel","detail":"auto-accept: restarting","timestamp":1769413853255},{"type":"timer","detail":"Started pre-filter: 10s (checking idle conditions)","timestamp":1769413853255},{"type":"timer-cancel","detail":"pre-filter: restarting","timestamp":1769413853255},{"type":"timer","detail":"Started no-output-fallback: 30s (fallback if no output at all)","timestamp":1769413853255},{"type":"timer-cancel","detail":"no-output-fallback: restarting","timestamp":1769413853255},{"type":"timer","detail":"Started auto-accept: 8s (plan mode detection)","timestamp":1769413853126},{"type":"timer-cancel","detail":"auto-accept: restarting","timestamp":1769413853126},{"type":"timer","detail":"Started pre-filter: 10s (checking idle conditions)","timestamp":1769413853126},{"type":"timer-cancel","detail":"pre-filter: restarting","timestamp":1769413853126}],"currentPhase":"Waiting for update to complete","nextAction":"Will send /clear when done"}
|
||||
|
||||
|
||||
### Issue #66 [WARNING] - 2026-01-26 09:01:06
|
||||
**State stuck: waiting_update for 10+ minutes**
|
||||
|
||||
The respawn controller has been in 'waiting_update' state for over 10 minutes.
|
||||
Time since activity: 115ms
|
||||
Detection status: {"stopHookReceived":false,"stopHookTime":null,"idlePromptReceived":false,"idlePromptTime":null,"completionMessageDetected":false,"completionMessageTime":null,"outputSilent":false,"msSinceLastOutput":115,"tokensStable":false,"lastTokenCount":9900,"msSinceTokenChange":6401,"workingPatternsAbsent":false,"msSinceLastWorking":4171,"aiCheck":{"status":"ready","lastVerdict":"IDLE","lastReasoning":"The terminal output is empty/blank, which typically indicates the session is at rest waiting for input. There are no spinner characters, no activity text, no partial output, and no signs of active processing. An empty terminal with no visible activity suggests the session has completed any previous work and is waiting for new input.","lastCheckDurationMs":6555,"cooldownEndsAt":null,"consecutiveErrors":0,"totalChecks":1,"disabledReason":null},"confidenceLevel":0,"statusText":"Respawn step: waiting_update","waitingFor":"Step completion","activeTimers":[{"name":"step-confirm","remainingMs":5852,"totalMs":10000},{"name":"no-output-fallback","remainingMs":29885,"totalMs":30000},{"name":"pre-filter","remainingMs":9885,"totalMs":10000},{"name":"auto-accept","remainingMs":7885,"totalMs":8000}],"recentActions":[{"type":"timer","detail":"Started auto-accept: 8s (plan mode detection)","timestamp":1769414466626},{"type":"timer-cancel","detail":"auto-accept: restarting","timestamp":1769414466626},{"type":"timer","detail":"Started pre-filter: 10s (checking idle conditions)","timestamp":1769414466626},{"type":"timer-cancel","detail":"pre-filter: restarting","timestamp":1769414466626},{"type":"timer","detail":"Started no-output-fallback: 30s (fallback if no output at all)","timestamp":1769414466626},{"type":"timer-cancel","detail":"no-output-fallback: restarting","timestamp":1769414466626},{"type":"timer","detail":"Started auto-accept: 8s (plan mode detection)","timestamp":1769414466529},{"type":"timer-cancel","detail":"auto-accept: restarting","timestamp":1769414466529},{"type":"timer","detail":"Started pre-filter: 10s (checking idle conditions)","timestamp":1769414466529},{"type":"timer-cancel","detail":"pre-filter: restarting","timestamp":1769414466529}],"currentPhase":"Waiting for update to complete","nextAction":"Will send /clear when done"}
|
||||
|
||||
+3
-1
@@ -184,7 +184,9 @@ export class RunSummaryTracker {
|
||||
const now = Date.now();
|
||||
|
||||
// Update idle time if was previously idle
|
||||
if (!this.isCurrentlyActive && this.stats.lastIdleAt) {
|
||||
// Note: We count the initial idle period (from session start to first working state)
|
||||
// even if lastIdleAt hasn't been set yet
|
||||
if (!this.isCurrentlyActive) {
|
||||
this.stats.totalTimeIdleMs += now - this.lastStatusChangeAt;
|
||||
}
|
||||
|
||||
|
||||
@@ -242,6 +242,28 @@ export class SubagentWatcher extends EventEmitter {
|
||||
return this.agentInfo.get(agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a subagent's description.
|
||||
* Used to set the short description from TaskTracker when available.
|
||||
*/
|
||||
updateDescription(agentId: string, description: string): boolean {
|
||||
const info = this.agentInfo.get(agentId);
|
||||
if (info) {
|
||||
info.description = description;
|
||||
this.emit('subagent:updated', info);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find sessions whose working directory matches a project hash.
|
||||
* Returns the project hash for a given working directory.
|
||||
*/
|
||||
getProjectHashForDir(workingDir: string): string {
|
||||
return this.getProjectHash(workingDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent subagents (modified within specified minutes)
|
||||
*/
|
||||
|
||||
+155
-7
@@ -1661,6 +1661,15 @@ class ClaudemanApp {
|
||||
}
|
||||
this.renderSubagentPanel();
|
||||
this.updateSubagentWindows();
|
||||
|
||||
// Auto-minimize completed subagent windows
|
||||
if (this.subagentWindows.has(data.agentId)) {
|
||||
const windowData = this.subagentWindows.get(data.agentId);
|
||||
if (windowData && !windowData.minimized) {
|
||||
this.closeSubagentWindow(data.agentId); // This minimizes to tab
|
||||
this.saveSubagentWindowStates(); // Persist the minimized state
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1722,6 +1731,9 @@ class ClaudemanApp {
|
||||
this.subagents.set(s.agentId, s);
|
||||
});
|
||||
this.renderSubagentPanel();
|
||||
|
||||
// Restore subagent window states (minimized/open) after subagents are loaded
|
||||
this.restoreSubagentWindowStates();
|
||||
}
|
||||
|
||||
// Auto-select first session if any
|
||||
@@ -1871,7 +1883,6 @@ class ClaudemanApp {
|
||||
<span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span>
|
||||
${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()">${taskStats.running}</span>` : ''}
|
||||
${subagentBadge}
|
||||
<span class="tab-summary" onclick="event.stopPropagation(); app.openRunSummary('${id}')" title="Run Summary" data-session-id="${id}">📊</span>
|
||||
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${id}')" title="Session options">⚙</span>
|
||||
<span class="tab-close" onclick="event.stopPropagation(); app.requestCloseSession('${id}')">×</span>
|
||||
</div>`);
|
||||
@@ -1929,6 +1940,9 @@ class ClaudemanApp {
|
||||
|
||||
// Re-render tabs to update badge
|
||||
this.renderSessionTabs();
|
||||
|
||||
// Persist the state change
|
||||
this.saveSubagentWindowStates();
|
||||
}
|
||||
|
||||
// Toggle subagent dropdown visibility on click
|
||||
@@ -1992,6 +2006,9 @@ class ClaudemanApp {
|
||||
// Re-render tabs to update badge
|
||||
this.renderSessionTabs();
|
||||
this.updateConnectionLines();
|
||||
|
||||
// Persist the state change
|
||||
this.saveSubagentWindowStates();
|
||||
}
|
||||
|
||||
getSessionName(session) {
|
||||
@@ -3355,18 +3372,21 @@ class ClaudemanApp {
|
||||
|
||||
closeSessionOptions() {
|
||||
this.editingSessionId = null;
|
||||
// Stop run summary auto-refresh if it was running
|
||||
this.stopRunSummaryAutoRefresh();
|
||||
document.getElementById('sessionOptionsModal').classList.remove('active');
|
||||
}
|
||||
|
||||
// ========== Run Summary Modal ==========
|
||||
|
||||
async openRunSummary(sessionId) {
|
||||
// Open session options modal and switch to summary tab
|
||||
this.openSessionOptions(sessionId);
|
||||
this.switchOptionsTab('summary');
|
||||
|
||||
this.runSummarySessionId = sessionId;
|
||||
this.runSummaryFilter = 'all';
|
||||
|
||||
// Show modal
|
||||
document.getElementById('runSummaryModal').classList.add('active');
|
||||
|
||||
// Reset filter buttons
|
||||
document.querySelectorAll('.run-summary-filters .filter-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.filter === 'all');
|
||||
@@ -3379,12 +3399,14 @@ class ClaudemanApp {
|
||||
closeRunSummary() {
|
||||
this.runSummarySessionId = null;
|
||||
this.stopRunSummaryAutoRefresh();
|
||||
document.getElementById('runSummaryModal').classList.remove('active');
|
||||
// Close session options modal (summary is now a tab in it)
|
||||
this.closeSessionOptions();
|
||||
}
|
||||
|
||||
async refreshRunSummary() {
|
||||
if (!this.runSummarySessionId) return;
|
||||
await this.loadRunSummary(this.runSummarySessionId);
|
||||
const sessionId = this.runSummarySessionId || this.editingSessionId;
|
||||
if (!sessionId) return;
|
||||
await this.loadRunSummary(sessionId);
|
||||
}
|
||||
|
||||
toggleRunSummaryAutoRefresh() {
|
||||
@@ -3632,6 +3654,12 @@ class ClaudemanApp {
|
||||
document.getElementById('respawn-tab').classList.toggle('hidden', tabName !== 'respawn');
|
||||
document.getElementById('context-tab').classList.toggle('hidden', tabName !== 'context');
|
||||
document.getElementById('ralph-tab').classList.toggle('hidden', tabName !== 'ralph');
|
||||
document.getElementById('summary-tab').classList.toggle('hidden', tabName !== 'summary');
|
||||
|
||||
// Load run summary data when switching to summary tab
|
||||
if (tabName === 'summary' && this.editingSessionId) {
|
||||
this.loadRunSummary(this.editingSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
getRalphConfig() {
|
||||
@@ -4008,6 +4036,120 @@ class ClaudemanApp {
|
||||
return this.loadAppSettingsFromStorage();
|
||||
}
|
||||
|
||||
// ========== Subagent Window State Persistence ==========
|
||||
|
||||
/**
|
||||
* Save subagent window states (minimized/open) to server for cross-browser persistence.
|
||||
* Called when a window is minimized, restored, or auto-minimized on completion.
|
||||
*/
|
||||
async saveSubagentWindowStates() {
|
||||
// Build state object: which agents are minimized per session
|
||||
const minimizedState = {};
|
||||
for (const [sessionId, agentIds] of this.minimizedSubagents) {
|
||||
minimizedState[sessionId] = Array.from(agentIds);
|
||||
}
|
||||
|
||||
// Also track which windows are open (not minimized)
|
||||
const openWindows = [];
|
||||
for (const [agentId, windowData] of this.subagentWindows) {
|
||||
if (!windowData.minimized) {
|
||||
openWindows.push({
|
||||
agentId,
|
||||
position: windowData.position || null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const windowStates = { minimized: minimizedState, open: openWindows };
|
||||
|
||||
// Save to localStorage for quick restore
|
||||
localStorage.setItem('claudeman-subagent-window-states', JSON.stringify(windowStates));
|
||||
|
||||
// Save to server for cross-browser persistence
|
||||
try {
|
||||
await fetch('/api/subagent-window-states', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(windowStates)
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to save subagent window states to server:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load subagent window states from server (or localStorage fallback).
|
||||
* Called on page load to restore minimized/open window states.
|
||||
*/
|
||||
async loadSubagentWindowStates() {
|
||||
let states = null;
|
||||
|
||||
// Try server first for cross-browser sync
|
||||
try {
|
||||
const res = await fetch('/api/subagent-window-states');
|
||||
if (res.ok) {
|
||||
states = await res.json();
|
||||
// Also update localStorage
|
||||
localStorage.setItem('claudeman-subagent-window-states', JSON.stringify(states));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load subagent window states from server:', err);
|
||||
}
|
||||
|
||||
// Fallback to localStorage
|
||||
if (!states) {
|
||||
try {
|
||||
const saved = localStorage.getItem('claudeman-subagent-window-states');
|
||||
if (saved) {
|
||||
states = JSON.parse(saved);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load subagent window states from localStorage:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return states || { minimized: {}, open: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore subagent window states after loading subagents.
|
||||
* Opens windows that were open before, keeps minimized ones minimized.
|
||||
*/
|
||||
async restoreSubagentWindowStates() {
|
||||
const states = await this.loadSubagentWindowStates();
|
||||
|
||||
// Restore minimized state
|
||||
for (const [sessionId, agentIds] of Object.entries(states.minimized || {})) {
|
||||
if (Array.isArray(agentIds) && agentIds.length > 0) {
|
||||
// Verify agents still exist
|
||||
const validAgents = agentIds.filter(id => this.subagents.has(id));
|
||||
if (validAgents.length > 0) {
|
||||
this.minimizedSubagents.set(sessionId, new Set(validAgents));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore open windows (for non-completed agents only)
|
||||
for (const { agentId, position } of (states.open || [])) {
|
||||
const agent = this.subagents.get(agentId);
|
||||
// Only restore window if agent exists and is still active/idle (not completed)
|
||||
if (agent && agent.status !== 'completed') {
|
||||
this.openSubagentWindow(agentId);
|
||||
// Restore position if saved
|
||||
if (position) {
|
||||
const windowData = this.subagentWindows.get(agentId);
|
||||
if (windowData && windowData.element) {
|
||||
windowData.element.style.left = position.left;
|
||||
windowData.element.style.top = position.top;
|
||||
windowData.position = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.renderSessionTabs(); // Update tab badges
|
||||
}
|
||||
|
||||
// ========== Help Modal ==========
|
||||
|
||||
showHelp() {
|
||||
@@ -5402,6 +5544,9 @@ class ClaudemanApp {
|
||||
// No animation, just update connection lines
|
||||
this.updateConnectionLines();
|
||||
}
|
||||
|
||||
// Persist the state change (new window opened)
|
||||
this.saveSubagentWindowStates();
|
||||
}
|
||||
|
||||
closeSubagentWindow(agentId) {
|
||||
@@ -5424,6 +5569,9 @@ class ClaudemanApp {
|
||||
|
||||
// Update tab badge to show minimized agents
|
||||
this.renderSessionTabs();
|
||||
|
||||
// Persist the state change
|
||||
this.saveSubagentWindowStates();
|
||||
} else {
|
||||
// No parent session - fully remove the window
|
||||
windowData.element.remove();
|
||||
|
||||
@@ -326,6 +326,7 @@
|
||||
<button class="modal-tab-btn active" data-tab="respawn" onclick="app.switchOptionsTab('respawn')">Respawn</button>
|
||||
<button class="modal-tab-btn" data-tab="context" onclick="app.switchOptionsTab('context')">Context</button>
|
||||
<button class="modal-tab-btn" data-tab="ralph" onclick="app.switchOptionsTab('ralph')">Ralph / Todo</button>
|
||||
<button class="modal-tab-btn" data-tab="summary" onclick="app.switchOptionsTab('summary')">Summary</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Respawn Tab Content -->
|
||||
@@ -478,11 +479,62 @@
|
||||
<button class="btn-toolbar btn-primary" onclick="app.saveRalphConfig()">Save Ralph Config</button>
|
||||
</div>
|
||||
</div><!-- End ralph-tab -->
|
||||
|
||||
<!-- Summary Tab Content -->
|
||||
<div class="modal-tab-content hidden" id="summary-tab">
|
||||
<!-- Stats Cards -->
|
||||
<div class="run-summary-stats" id="runSummaryStats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="rsStat-cycles">0</div>
|
||||
<div class="stat-label">Respawn Cycles</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="rsStat-tokens">0</div>
|
||||
<div class="stat-label">Tokens Used</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="rsStat-active">0m</div>
|
||||
<div class="stat-label">Active Time</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="rsStat-issues">0</div>
|
||||
<div class="stat-label">Issues</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Buttons -->
|
||||
<div class="run-summary-filters">
|
||||
<button class="filter-btn active" data-filter="all" onclick="app.filterRunSummary('all')">All</button>
|
||||
<button class="filter-btn" data-filter="errors" onclick="app.filterRunSummary('errors')">Errors</button>
|
||||
<button class="filter-btn" data-filter="warnings" onclick="app.filterRunSummary('warnings')">Warnings</button>
|
||||
<button class="filter-btn" data-filter="respawn" onclick="app.filterRunSummary('respawn')">Respawn</button>
|
||||
<button class="filter-btn" data-filter="idle" onclick="app.filterRunSummary('idle')">Idle/Working</button>
|
||||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="run-summary-timeline" id="runSummaryTimeline">
|
||||
<p class="empty-message">Select a session to view summary</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer with actions -->
|
||||
<div class="run-summary-footer">
|
||||
<span class="run-summary-session-info" id="runSummarySessionInfo"></span>
|
||||
<div class="run-summary-actions">
|
||||
<label class="auto-refresh-label">
|
||||
<input type="checkbox" id="runSummaryAutoRefresh" onchange="app.toggleRunSummaryAutoRefresh()">
|
||||
Auto-refresh
|
||||
</label>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.refreshRunSummary()" title="Refresh summary">↻ Refresh</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.exportRunSummary('json')" title="Export as JSON">Export JSON</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.exportRunSummary('md')" title="Export as Markdown">Export MD</button>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- End summary-tab -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Run Summary Modal -->
|
||||
<!-- Run Summary Modal (legacy, kept for backwards compatibility) -->
|
||||
<div class="modal" id="runSummaryModal">
|
||||
<div class="modal-backdrop" onclick="app.closeRunSummary()"></div>
|
||||
<div class="modal-content modal-lg">
|
||||
|
||||
@@ -2103,6 +2103,21 @@ body {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.run-summary-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.run-summary-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.modal-footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -321,6 +321,13 @@ export class WebServer extends EventEmitter {
|
||||
*/
|
||||
private setupSubagentWatcherListeners(): void {
|
||||
subagentWatcher.on('subagent:discovered', (info: SubagentInfo) => {
|
||||
// Try to get a better description from the session's TaskTracker
|
||||
// The TaskTracker has access to params.description from the Task tool call
|
||||
const betterDescription = this.getTaskDescriptionForSubagent(info);
|
||||
if (betterDescription && betterDescription !== info.description) {
|
||||
info.description = betterDescription;
|
||||
subagentWatcher.updateDescription(info.agentId, betterDescription);
|
||||
}
|
||||
this.broadcast('subagent:discovered', info);
|
||||
});
|
||||
|
||||
@@ -349,6 +356,54 @@ export class WebServer extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short description from TaskTracker for a discovered subagent.
|
||||
*
|
||||
* When Claude Code spawns a subagent via the Task tool, it passes two parameters:
|
||||
* - description: Short title like "Explore Python scripts" (3-5 words)
|
||||
* - prompt: Full detailed task prompt
|
||||
*
|
||||
* The TaskTracker parses the Task tool call and has access to params.description,
|
||||
* while the SubagentWatcher only sees the JSONL file which contains the full prompt.
|
||||
*
|
||||
* This method correlates them by finding the session whose working directory
|
||||
* matches the subagent's project hash, then checking for recent tasks.
|
||||
*/
|
||||
private getTaskDescriptionForSubagent(info: SubagentInfo): string | undefined {
|
||||
// Find the session whose working directory matches this subagent's project
|
||||
for (const session of this.sessions.values()) {
|
||||
const sessionProjectHash = subagentWatcher.getProjectHashForDir(session.workingDir);
|
||||
if (sessionProjectHash === info.projectHash) {
|
||||
// Found the owning session - check its TaskTracker for recent tasks
|
||||
// Get all tasks from the tracker
|
||||
const allTasks = Array.from(session.taskTracker.getAllTasks().values());
|
||||
if (allTasks.length > 0) {
|
||||
// Sort by start time descending to get most recent first
|
||||
const recentTasks = allTasks.sort((a, b) => (b.startTime || 0) - (a.startTime || 0));
|
||||
|
||||
// Check if there's a task that started recently (within 5 seconds of subagent discovery)
|
||||
const subagentStartTime = new Date(info.startedAt).getTime();
|
||||
for (const task of recentTasks) {
|
||||
const taskStartTime = task.startTime || 0;
|
||||
const timeDiff = Math.abs(subagentStartTime - taskStartTime);
|
||||
// If the task started within 5 seconds of the subagent, use its description
|
||||
if (timeDiff < 5000 && task.description) {
|
||||
return task.description;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use the most recent running task's description
|
||||
const runningTask = recentTasks.find(t => t.status === 'running');
|
||||
if (runningTask?.description) {
|
||||
return runningTask.description;
|
||||
}
|
||||
}
|
||||
break; // Found the session, no need to continue
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async setupRoutes(): Promise<void> {
|
||||
// Serve static files
|
||||
await this.app.register(fastifyStatic, {
|
||||
@@ -1639,6 +1694,36 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
// ============ Subagent Window State Endpoints ============
|
||||
// Persists minimized/open window states for cross-browser sync
|
||||
const windowStatesPath = join(homedir(), '.claudeman', 'subagent-window-states.json');
|
||||
|
||||
this.app.get('/api/subagent-window-states', async () => {
|
||||
try {
|
||||
if (existsSync(windowStatesPath)) {
|
||||
const content = readFileSync(windowStatesPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to read subagent window states:', err);
|
||||
}
|
||||
return { minimized: {}, open: [] };
|
||||
});
|
||||
|
||||
this.app.put('/api/subagent-window-states', async (req) => {
|
||||
const states = req.body as Record<string, unknown>;
|
||||
try {
|
||||
const dir = dirname(windowStatesPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
writeFileSync(windowStatesPath, JSON.stringify(states, null, 2));
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
||||
}
|
||||
});
|
||||
|
||||
// ============ Screen Management Endpoints ============
|
||||
|
||||
// Get all tracked screens with stats
|
||||
@@ -2186,6 +2271,9 @@ export class WebServer extends EventEmitter {
|
||||
this.broadcast('session:completion', { id: session.id, result, cost });
|
||||
this.broadcast('session:updated', session.toDetailedState());
|
||||
this.persistSessionState(session);
|
||||
// Track tokens in run summary (completion event has updated token values)
|
||||
const tracker = this.runSummaryTrackers.get(session.id);
|
||||
if (tracker) tracker.recordTokens(session.inputTokens, session.outputTokens);
|
||||
});
|
||||
|
||||
session.on('exit', (code) => {
|
||||
|
||||
Reference in New Issue
Block a user