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:
arkon
2026-01-26 09:06:49 +01:00
co-authored by Claude Opus 4.5
parent f95ffd12e2
commit f06240f4ef
10 changed files with 388 additions and 11 deletions
+155 -7
View File
@@ -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}">&#x1F4CA;</span>
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${id}')" title="Session options">&#x2699;</span>
<span class="tab-close" onclick="event.stopPropagation(); app.requestCloseSession('${id}')">&times;</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();
+53 -1
View File
@@ -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">&#x21BB; 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">
+15
View File
@@ -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;