mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
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:
+133
-10
@@ -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">✕</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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user