fix: correct Task tool description extraction for subagent window titles

- Add parseTaskDescriptionsFromTerminalData() to parse raw terminal data in
  interactive sessions (not just one-shot mode)
- Strip ANSI codes before regex matching since terminal output has embedded
  codes like [1mExplore[0m
- Fix getTaskDescriptionForSubagent() to check ALL sessions with matching
  project hash (not just the first one found) since multiple sessions can
  share the same working directory
- Add tool_result event forwarding for subagent SSE broadcasts

The pattern Explore(Description) in terminal output is now correctly parsed
and used as the subagent window title instead of extracting from the prompt.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-26 10:42:04 +01:00
co-authored by Claude Opus 4.5
parent f06240f4ef
commit fc940e153b
5 changed files with 519 additions and 54 deletions
+133 -10
View File
@@ -437,6 +437,7 @@ class ClaudemanApp {
// 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.subagentToolResults = new Map(); // Map<agentId, Map<toolUseId, result>> - tool results by toolUseId
this.activeSubagentId = null; // Currently selected subagent for detail view
this.subagentPanelVisible = false;
this.subagentWindows = new Map(); // Map<agentId, { element, position }>
@@ -1652,6 +1653,29 @@ class ClaudemanApp {
}
});
this.eventSource.addEventListener('subagent:tool_result', (e) => {
const data = JSON.parse(e.data);
// Store tool result by toolUseId for later lookup
if (!this.subagentToolResults.has(data.agentId)) {
this.subagentToolResults.set(data.agentId, new Map());
}
this.subagentToolResults.get(data.agentId).set(data.toolUseId, data);
// Add to activity stream
const activity = this.subagentActivity.get(data.agentId) || [];
activity.push({ type: 'tool_result', ...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);
@@ -5051,6 +5075,9 @@ class ClaudemanApp {
const lastTool = lastActivity?.type === 'tool' ? lastActivity.tool : null;
const hasWindow = this.subagentWindows.has(agent.agentId);
const canKill = agent.status === 'active' || agent.status === 'idle';
const modelBadge = agent.modelShort
? `<span class="subagent-model-badge ${agent.modelShort}">${agent.modelShort}</span>`
: '';
const displayName = agent.description || agent.agentId.substring(0, 7);
html.push(`
@@ -5061,6 +5088,7 @@ class ClaudemanApp {
<div class="subagent-header">
<span class="subagent-icon">🤖</span>
<span class="subagent-id" title="${this.escapeHtml(agent.description || agent.agentId)}">${this.escapeHtml(displayName.length > 40 ? displayName.substring(0, 40) + '...' : displayName)}</span>
${modelBadge}
<span class="subagent-status ${statusClass}">${agent.status}</span>
${canKill ? `<button class="subagent-kill-btn" onclick="event.stopPropagation(); app.killSubagent('${agent.agentId}')" title="Kill agent">&#x2715;</button>` : ''}
<button class="subagent-window-btn" onclick="event.stopPropagation(); app.${hasWindow ? 'closeSubagentWindow' : 'openSubagentWindow'}('${agent.agentId}')" title="${hasWindow ? 'Close window' : 'Open in window'}">
@@ -5104,18 +5132,36 @@ class ClaudemanApp {
const activityHtml = activity.slice(-30).map(a => {
const time = new Date(a.timestamp).toLocaleTimeString();
if (a.type === 'tool') {
return `<div class="subagent-activity tool">
const toolDetail = this.getToolDetailExpanded(a.tool, a.input, a.fullInput, a.toolUseId);
return `<div class="subagent-activity tool" data-tool-use-id="${a.toolUseId || ''}">
<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>
<span class="detail">${toolDetail.primary}</span>
${toolDetail.hasMore ? `<button class="tool-expand-btn" onclick="app.toggleToolParams('${a.toolUseId}')">▶</button>` : ''}
${toolDetail.hasMore ? `<div class="tool-params-expanded" id="tool-params-${a.toolUseId}" style="display:none;"><pre>${this.escapeHtml(JSON.stringify(a.fullInput || a.input, null, 2))}</pre></div>` : ''}
</div>`;
} else if (a.type === 'progress') {
const icon = a.progressType === 'query_update' ? '⟳' : '✓';
return `<div class="subagent-activity progress">
} else if (a.type === 'tool_result') {
const icon = a.isError ? '❌' : '📄';
const statusClass = a.isError ? 'error' : '';
const sizeInfo = a.contentLength > 500 ? ` (${this.formatBytes(a.contentLength)})` : '';
const preview = a.preview.length > 80 ? a.preview.substring(0, 80) + '...' : a.preview;
return `<div class="subagent-activity tool-result ${statusClass}">
<span class="time">${time}</span>
<span class="icon">${icon}</span>
<span class="detail">${a.query || a.progressType}</span>
<span class="name">${a.tool || 'result'}</span>
<span class="detail">${this.escapeHtml(preview)}${sizeInfo}</span>
</div>`;
} else if (a.type === 'progress') {
// Check for hook events
const isHook = a.hookEvent || a.hookName;
const icon = isHook ? '🪝' : (a.progressType === 'query_update' ? '⟳' : '✓');
const hookClass = isHook ? ' hook' : '';
const displayText = isHook ? (a.hookName || a.hookEvent) : (a.query || a.progressType);
return `<div class="subagent-activity progress${hookClass}">
<span class="time">${time}</span>
<span class="icon">${icon}</span>
<span class="detail">${displayText}</span>
</div>`;
} else if (a.type === 'message') {
const preview = a.text.length > 100 ? a.text.substring(0, 100) + '...' : a.text;
@@ -5129,9 +5175,17 @@ class ClaudemanApp {
}).join('');
const detailTitle = agent.description || `Agent ${agent.agentId}`;
const modelBadge = agent.modelShort
? `<span class="subagent-model-badge ${agent.modelShort}">${agent.modelShort}</span>`
: '';
const tokenStats = (agent.totalInputTokens || agent.totalOutputTokens)
? `<span>Tokens: ${this.formatTokenCount(agent.totalInputTokens || 0)}↓ ${this.formatTokenCount(agent.totalOutputTokens || 0)}↑</span>`
: '';
detail.innerHTML = `
<div class="subagent-detail-header">
<span class="subagent-id" title="${this.escapeHtml(agent.description || agent.agentId)}">${this.escapeHtml(detailTitle.length > 60 ? detailTitle.substring(0, 60) + '...' : detailTitle)}</span>
${modelBadge}
<span class="subagent-status ${agent.status}">${agent.status}</span>
<button class="subagent-transcript-btn" onclick="app.viewSubagentTranscript('${agent.agentId}')">
View Full Transcript
@@ -5141,6 +5195,7 @@ class ClaudemanApp {
<span>Tools: ${agent.toolCallCount}</span>
<span>Entries: ${agent.entryCount}</span>
<span>Size: ${(agent.fileSize / 1024).toFixed(1)}KB</span>
${tokenStats}
</div>
<div class="subagent-activity-log">
${activityHtml || '<div class="subagent-empty">No activity yet</div>'}
@@ -5148,6 +5203,31 @@ class ClaudemanApp {
`;
}
toggleToolParams(toolUseId) {
const el = document.getElementById(`tool-params-${toolUseId}`);
if (!el) return;
const btn = el.previousElementSibling;
if (el.style.display === 'none') {
el.style.display = 'block';
if (btn) btn.textContent = '▼';
} else {
el.style.display = 'none';
if (btn) btn.textContent = '▶';
}
}
formatTokenCount(count) {
if (count >= 1000000) return (count / 1000000).toFixed(1) + 'M';
if (count >= 1000) return (count / 1000).toFixed(1) + 'k';
return count.toString();
}
formatBytes(bytes) {
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + 'MB';
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + 'KB';
return bytes + 'B';
}
getToolIcon(tool) {
const icons = {
WebSearch: '🔍',
@@ -5178,6 +5258,16 @@ class ClaudemanApp {
return '';
}
getToolDetailExpanded(tool, input, fullInput, toolUseId) {
const primary = this.getToolDetail(tool, input);
// Check if there are additional params beyond the primary one
const primaryKeys = ['query', 'url', 'file_path', 'command', 'pattern'];
const inputKeys = Object.keys(fullInput || input || {});
const extraKeys = inputKeys.filter(k => !primaryKeys.includes(k));
const hasMore = extraKeys.length > 0 || (fullInput && JSON.stringify(fullInput).length > 100);
return { primary, hasMore, fullInput: fullInput || input };
}
async killSubagent(agentId) {
try {
const res = await fetch(`/api/subagents/${agentId}`, { method: 'DELETE' });
@@ -5449,11 +5539,15 @@ class ClaudemanApp {
const windowTitle = agent.description || agentId.substring(0, 7);
const truncatedTitle = windowTitle.length > 50 ? windowTitle.substring(0, 50) + '...' : windowTitle;
const modelBadge = agent.modelShort
? `<span class="subagent-model-badge ${agent.modelShort}">${agent.modelShort}</span>`
: '';
win.innerHTML = `
<div class="subagent-window-header">
<div class="subagent-window-title" title="${this.escapeHtml(agent.description || agentId)}">
<span class="icon">🤖</span>
<span class="id">${this.escapeHtml(truncatedTitle)}</span>
${modelBadge}
<span class="status ${agent.status}">${agent.status}</span>
</div>
<div class="subagent-window-actions">
@@ -5700,12 +5794,26 @@ class ClaudemanApp {
<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">
} else if (a.type === 'tool_result') {
const icon = a.isError ? '❌' : '📄';
const statusClass = a.isError ? ' error' : '';
const sizeInfo = a.contentLength > 500 ? ` (${this.formatBytes(a.contentLength)})` : '';
const preview = a.preview.length > 60 ? a.preview.substring(0, 60) + '...' : a.preview;
return `<div class="activity-line result-line${statusClass}">
<span class="time">${time}</span>
<span class="tool-icon">${icon}</span>
<span class="tool-detail">${this.escapeHtml(a.query || a.progressType)}</span>
<span class="tool-name">${a.tool || '→'}</span>
<span class="tool-detail">${this.escapeHtml(preview)}${sizeInfo}</span>
</div>`;
} else if (a.type === 'progress') {
// Check for hook events
const isHook = a.hookEvent || a.hookName;
const icon = isHook ? '🪝' : (a.progressType === 'query_update' ? '⟳' : '✓');
const displayText = isHook ? (a.hookName || a.hookEvent) : (a.query || a.progressType);
return `<div class="activity-line progress-line${isHook ? ' hook-line' : ''}">
<span class="time">${time}</span>
<span class="tool-icon">${icon}</span>
<span class="tool-detail">${this.escapeHtml(displayText)}</span>
</div>`;
} else if (a.type === 'message') {
const preview = a.text.length > 150 ? a.text.substring(0, 150) + '...' : a.text;
@@ -5750,6 +5858,21 @@ class ClaudemanApp {
titleContainer.title = agent.description || agentId;
}
// Update or add model badge
let modelBadge = win.querySelector('.subagent-window-title .subagent-model-badge');
if (agent.modelShort) {
if (!modelBadge) {
modelBadge = document.createElement('span');
modelBadge.className = `subagent-model-badge ${agent.modelShort}`;
const statusEl = win.querySelector('.subagent-window-title .status');
if (statusEl) {
statusEl.insertAdjacentElement('beforebegin', modelBadge);
}
}
modelBadge.className = `subagent-model-badge ${agent.modelShort}`;
modelBadge.textContent = agent.modelShort;
}
// Update status
const statusEl = win.querySelector('.subagent-window-title .status');
if (statusEl) {
+92
View File
@@ -3971,6 +3971,98 @@ kbd {
color: var(--text);
}
/* Model badges */
.subagent-model-badge {
font-size: 0.55rem;
padding: 0.1rem 0.35rem;
border-radius: 4px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.subagent-model-badge.haiku {
background: #4ade80;
color: #052e16;
}
.subagent-model-badge.sonnet {
background: #60a5fa;
color: #1e3a5f;
}
.subagent-model-badge.opus {
background: #c084fc;
color: #3b0764;
}
/* Hook events */
.subagent-activity.progress.hook .icon {
color: #f59e0b;
}
.activity-line.hook-line .tool-icon {
color: #f59e0b;
}
/* Tool result activity */
.subagent-activity.tool-result .icon {
color: var(--success);
}
.subagent-activity.tool-result.error .icon {
color: var(--red);
}
.activity-line.result-line .tool-icon {
color: var(--success);
}
.activity-line.result-line.error .tool-icon {
color: var(--red);
}
.activity-line.result-line .tool-detail {
color: var(--text-dim);
font-style: italic;
}
/* Tool params expansion */
.tool-expand-btn {
background: transparent;
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text-muted);
cursor: pointer;
font-size: 0.6rem;
padding: 0.1rem 0.3rem;
margin-left: 0.3rem;
transition: all 0.15s;
}
.tool-expand-btn:hover {
background: var(--bg-hover);
color: var(--text);
}
.tool-params-expanded {
background: var(--bg-dark);
border-left: 2px solid var(--accent);
margin: 0.3rem 0 0.3rem 2rem;
padding: 0.4rem 0.6rem;
border-radius: 0 4px 4px 0;
font-size: 0.65rem;
max-height: 150px;
overflow-y: auto;
}
.tool-params-expanded pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
color: var(--text-dim);
}
/* ========== Subagent Windows (Floating) ========== */
.subagent-window {
+14 -26
View File
@@ -28,7 +28,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 { subagentWatcher, type SubagentInfo, type SubagentToolCall, type SubagentProgress, type SubagentMessage, type SubagentToolResult } from '../subagent-watcher.js';
import { TranscriptWatcher } from '../transcript-watcher.js';
import { v4 as uuidv4 } from 'uuid';
import { createRequire } from 'node:module';
@@ -339,6 +339,10 @@ export class WebServer extends EventEmitter {
this.broadcast('subagent:tool_call', data);
});
subagentWatcher.on('subagent:tool_result', (data: SubagentToolResult) => {
this.broadcast('subagent:tool_result', data);
});
subagentWatcher.on('subagent:progress', (data: SubagentProgress) => {
this.broadcast('subagent:progress', data);
});
@@ -370,35 +374,19 @@ export class WebServer extends EventEmitter {
* 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
// Find sessions whose working directory matches this subagent's project
// Check ALL matching sessions since multiple sessions may have the same working directory
const subagentStartTime = new Date(info.startedAt).getTime();
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;
}
// Found a matching session - check for recent task descriptions parsed from terminal
// The Session parses "Explore(Description)" patterns from Claude Code output
const description = session.findTaskDescriptionNear(subagentStartTime, 15000);
if (description) {
return description;
}
break; // Found the session, no need to continue
}
}
return undefined;