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:
+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