mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39: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:
+112
@@ -69,6 +69,11 @@ const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
|
||||
// - Single-char escapes: ESC = or ESC >
|
||||
const ANSI_ESCAPE_PATTERN = /\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[=>])/g;
|
||||
const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
|
||||
// Pattern to match Task tool invocations in terminal output
|
||||
// Matches: "Explore(Description)", "Task(Description)", "Bash(Description)", etc.
|
||||
// The prefix characters vary (●, ·, ✶, etc.) so we don't require them
|
||||
// We look for the tool name followed by (description)
|
||||
const TASK_TOOL_PATTERN = /\b(Explore|Task|Bash|Plan|general-purpose)\(([^)]+)\)/g;
|
||||
|
||||
// ============================================================================
|
||||
// Claude CLI PATH Resolution
|
||||
@@ -425,6 +430,11 @@ export class Session extends EventEmitter {
|
||||
toolsUpdate: (tools: ActiveBashTool[]) => void;
|
||||
} | null = null;
|
||||
|
||||
// Task descriptions parsed from terminal output (e.g., "Explore(Description)")
|
||||
// Used to correlate with SubagentWatcher discoveries for better window titles
|
||||
private _recentTaskDescriptions: Map<number, string> = new Map(); // timestamp -> description
|
||||
private static readonly TASK_DESCRIPTION_MAX_AGE_MS = 30000; // Keep descriptions for 30 seconds
|
||||
|
||||
constructor(config: Partial<SessionConfig> & {
|
||||
workingDir: string;
|
||||
mode?: SessionMode;
|
||||
@@ -893,6 +903,10 @@ export class Session extends EventEmitter {
|
||||
// Parse token count from status line (e.g., "123.4k tokens" or "5234 tokens")
|
||||
this.parseTokensFromStatusLine(data);
|
||||
|
||||
// Parse task descriptions from terminal output (e.g., "Explore(Check files)")
|
||||
// This enables correlating subagent windows with their short descriptions
|
||||
this.parseTaskDescriptionsFromTerminalData(data);
|
||||
|
||||
// Detect if Claude is working or at prompt
|
||||
// The prompt line contains "❯" when waiting for input
|
||||
if (data.includes('❯') || data.includes('\u276f')) {
|
||||
@@ -1305,10 +1319,108 @@ export class Session extends EventEmitter {
|
||||
} else if (trimmed) {
|
||||
this._textOutput.append(line + '\n');
|
||||
}
|
||||
|
||||
// Parse task descriptions from terminal output (e.g., "Explore(Description)")
|
||||
// This captures the short description from Claude Code's Task tool output
|
||||
this.parseTaskDescriptionsFromLine(cleanLine);
|
||||
}
|
||||
// Note: BufferAccumulator auto-trims when max size exceeded
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse task descriptions from raw terminal data (may contain multiple lines).
|
||||
* Called from interactive mode's onData handler.
|
||||
*/
|
||||
private parseTaskDescriptionsFromTerminalData(data: string): void {
|
||||
// Quick pre-check: skip if no parentheses present
|
||||
if (!data.includes('(') || !data.includes(')')) return;
|
||||
|
||||
// Split by newlines and process each line
|
||||
const lines = data.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
this.parseTaskDescriptionsFromLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse task descriptions from terminal output.
|
||||
* Claude Code outputs Task tool calls as "ToolName(Description)" in the terminal.
|
||||
* We capture these descriptions to use as window titles for subagents.
|
||||
*/
|
||||
private parseTaskDescriptionsFromLine(line: string): void {
|
||||
// Quick pre-check: skip expensive regex if no common tool patterns present
|
||||
if (!line.includes('(') || !line.includes(')')) return;
|
||||
|
||||
// Strip ANSI codes before matching - terminal output has embedded codes like [1mExplore[0m
|
||||
const cleanLine = line.replace(ANSI_ESCAPE_PATTERN, '');
|
||||
|
||||
// Reset regex lastIndex for global pattern
|
||||
TASK_TOOL_PATTERN.lastIndex = 0;
|
||||
|
||||
let match;
|
||||
while ((match = TASK_TOOL_PATTERN.exec(cleanLine)) !== null) {
|
||||
const description = match[2].trim();
|
||||
if (description && description.length > 0) {
|
||||
const now = Date.now();
|
||||
this._recentTaskDescriptions.set(now, description);
|
||||
|
||||
// Cleanup old entries
|
||||
this.cleanupOldTaskDescriptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove task descriptions older than TASK_DESCRIPTION_MAX_AGE_MS.
|
||||
*/
|
||||
private cleanupOldTaskDescriptions(): void {
|
||||
const cutoff = Date.now() - Session.TASK_DESCRIPTION_MAX_AGE_MS;
|
||||
for (const [timestamp] of this._recentTaskDescriptions) {
|
||||
if (timestamp < cutoff) {
|
||||
this._recentTaskDescriptions.delete(timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent task descriptions parsed from terminal output.
|
||||
* Returns descriptions sorted by timestamp (most recent first).
|
||||
*/
|
||||
getRecentTaskDescriptions(): Array<{ timestamp: number; description: string }> {
|
||||
this.cleanupOldTaskDescriptions();
|
||||
const results: Array<{ timestamp: number; description: string }> = [];
|
||||
for (const [timestamp, description] of this._recentTaskDescriptions) {
|
||||
results.push({ timestamp, description });
|
||||
}
|
||||
return results.sort((a, b) => b.timestamp - a.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a task description that was parsed close to a given timestamp.
|
||||
* Used to correlate with SubagentWatcher discoveries.
|
||||
*
|
||||
* @param subagentStartTime - The timestamp when the subagent was discovered
|
||||
* @param maxAgeMs - Maximum age difference to consider (default 10 seconds)
|
||||
* @returns The matching description or undefined
|
||||
*/
|
||||
findTaskDescriptionNear(subagentStartTime: number, maxAgeMs: number = 10000): string | undefined {
|
||||
this.cleanupOldTaskDescriptions();
|
||||
|
||||
// Find the most recent description that was parsed before or around the subagent start time
|
||||
let bestMatch: { timestamp: number; description: string } | undefined;
|
||||
let bestDiff = Infinity;
|
||||
|
||||
for (const [timestamp, description] of this._recentTaskDescriptions) {
|
||||
const diff = Math.abs(subagentStartTime - timestamp);
|
||||
if (diff < maxAgeMs && diff < bestDiff) {
|
||||
bestMatch = { timestamp, description };
|
||||
bestDiff = diff;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch?.description;
|
||||
}
|
||||
|
||||
// Parse token count from Claude's status line in interactive mode
|
||||
// Matches patterns like "123.4k tokens", "5234 tokens", "1.2M tokens"
|
||||
private parseTokensFromStatusLine(data: string): void {
|
||||
|
||||
+168
-18
@@ -27,6 +27,10 @@ export interface SubagentInfo {
|
||||
entryCount: number;
|
||||
fileSize: number;
|
||||
description?: string; // Task description from first user message
|
||||
model?: string; // Full model name (e.g., "claude-sonnet-4-20250514")
|
||||
modelShort?: 'haiku' | 'sonnet' | 'opus'; // Short model identifier
|
||||
totalInputTokens?: number; // Running total of input tokens
|
||||
totalOutputTokens?: number; // Running total of output tokens
|
||||
}
|
||||
|
||||
export interface SubagentToolCall {
|
||||
@@ -35,6 +39,8 @@ export interface SubagentToolCall {
|
||||
timestamp: string;
|
||||
tool: string;
|
||||
input: Record<string, unknown>;
|
||||
toolUseId?: string; // For linking to tool_result
|
||||
fullInput: Record<string, unknown>; // Complete input object (input is truncated for display)
|
||||
}
|
||||
|
||||
export interface SubagentProgress {
|
||||
@@ -44,6 +50,8 @@ export interface SubagentProgress {
|
||||
progressType: 'query_update' | 'search_results_received' | string;
|
||||
query?: string;
|
||||
resultCount?: number;
|
||||
hookEvent?: string; // e.g., "PostToolUse"
|
||||
hookName?: string; // e.g., "PostToolUse:Read"
|
||||
}
|
||||
|
||||
export interface SubagentMessage {
|
||||
@@ -61,25 +69,48 @@ export interface SubagentTranscriptEntry {
|
||||
sessionId: string;
|
||||
message?: {
|
||||
role: string;
|
||||
model?: string; // Model used for this message (e.g., "claude-sonnet-4-20250514")
|
||||
usage?: {
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
};
|
||||
content: string | Array<{
|
||||
type: 'text' | 'tool_use' | 'tool_result';
|
||||
text?: string;
|
||||
name?: string;
|
||||
id?: string; // tool_use_id for tool_use blocks
|
||||
tool_use_id?: string; // tool_use_id for tool_result blocks
|
||||
input?: Record<string, unknown>;
|
||||
content?: string;
|
||||
content?: string | Array<{ type: string; text?: string }>; // tool_result content
|
||||
is_error?: boolean; // For tool_result errors
|
||||
}>;
|
||||
};
|
||||
data?: {
|
||||
type: string;
|
||||
query?: string;
|
||||
resultCount?: number;
|
||||
hookEvent?: string; // e.g., "PostToolUse"
|
||||
hookName?: string; // e.g., "PostToolUse:Read"
|
||||
tool_name?: string; // Tool name for hook events
|
||||
};
|
||||
}
|
||||
|
||||
export interface SubagentToolResult {
|
||||
agentId: string;
|
||||
sessionId: string;
|
||||
timestamp: string;
|
||||
toolUseId: string;
|
||||
tool?: string; // Tool name (looked up from pending)
|
||||
preview: string; // First 500 chars of result
|
||||
contentLength: number; // Total length of result
|
||||
isError: boolean; // Whether result is an error
|
||||
}
|
||||
|
||||
export interface SubagentEvents {
|
||||
'subagent:discovered': (info: SubagentInfo) => void;
|
||||
'subagent:updated': (info: SubagentInfo) => void;
|
||||
'subagent:tool_call': (data: SubagentToolCall) => void;
|
||||
'subagent:tool_result': (data: SubagentToolResult) => void;
|
||||
'subagent:progress': (data: SubagentProgress) => void;
|
||||
'subagent:message': (data: SubagentMessage) => void;
|
||||
'subagent:completed': (info: SubagentInfo) => void;
|
||||
@@ -105,11 +136,24 @@ export class SubagentWatcher extends EventEmitter {
|
||||
private livenessInterval: NodeJS.Timeout | null = null;
|
||||
private _isRunning = false;
|
||||
private knownSubagentDirs = new Set<string>();
|
||||
// Map of agentId -> Map of toolUseId -> tool name (for linking tool_result to tool_call)
|
||||
private pendingToolCalls = new Map<string, Map<string, string>>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract short model identifier from full model name
|
||||
*/
|
||||
private extractModelShort(model: string): 'haiku' | 'sonnet' | 'opus' | undefined {
|
||||
const lower = model.toLowerCase();
|
||||
if (lower.includes('haiku')) return 'haiku';
|
||||
if (lower.includes('sonnet')) return 'sonnet';
|
||||
if (lower.includes('opus')) return 'opus';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ========== Public API ==========
|
||||
|
||||
/**
|
||||
@@ -787,8 +831,26 @@ export class SubagentWatcher extends EventEmitter {
|
||||
* Process a transcript entry and emit appropriate events
|
||||
*/
|
||||
private processEntry(entry: SubagentTranscriptEntry, agentId: string, sessionId: string): void {
|
||||
// Check if this is first user message and description is missing
|
||||
const info = this.agentInfo.get(agentId);
|
||||
|
||||
// Extract model from assistant messages (first one sets the model)
|
||||
if (info && entry.type === 'assistant' && entry.message?.model && !info.model) {
|
||||
info.model = entry.message.model;
|
||||
info.modelShort = this.extractModelShort(entry.message.model);
|
||||
this.emit('subagent:updated', info);
|
||||
}
|
||||
|
||||
// Aggregate token usage from messages
|
||||
if (info && entry.message?.usage) {
|
||||
if (entry.message.usage.input_tokens) {
|
||||
info.totalInputTokens = (info.totalInputTokens || 0) + entry.message.usage.input_tokens;
|
||||
}
|
||||
if (entry.message.usage.output_tokens) {
|
||||
info.totalOutputTokens = (info.totalOutputTokens || 0) + entry.message.usage.output_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is first user message and description is missing
|
||||
if (info && !info.description && entry.type === 'user' && entry.message?.content) {
|
||||
let text: string | undefined;
|
||||
if (typeof entry.message.content === 'string') {
|
||||
@@ -813,6 +875,11 @@ export class SubagentWatcher extends EventEmitter {
|
||||
progressType: entry.data.type,
|
||||
query: entry.data.query,
|
||||
resultCount: entry.data.resultCount,
|
||||
// Extract hook event info if present
|
||||
hookEvent: entry.data.hookEvent,
|
||||
hookName: entry.data.hookName || (entry.data.hookEvent && entry.data.tool_name
|
||||
? `${entry.data.hookEvent}:${entry.data.tool_name}`
|
||||
: undefined),
|
||||
};
|
||||
this.emit('subagent:progress', progress);
|
||||
} else if (entry.type === 'assistant' && entry.message?.content) {
|
||||
@@ -832,12 +899,22 @@ export class SubagentWatcher extends EventEmitter {
|
||||
} else {
|
||||
for (const content of entry.message.content) {
|
||||
if (content.type === 'tool_use' && content.name) {
|
||||
// Store toolUseId for linking to results
|
||||
if (content.id) {
|
||||
if (!this.pendingToolCalls.has(agentId)) {
|
||||
this.pendingToolCalls.set(agentId, new Map());
|
||||
}
|
||||
this.pendingToolCalls.get(agentId)!.set(content.id, content.name);
|
||||
}
|
||||
|
||||
const toolCall: SubagentToolCall = {
|
||||
agentId,
|
||||
sessionId,
|
||||
timestamp: entry.timestamp,
|
||||
tool: content.name,
|
||||
input: content.input || {},
|
||||
input: this.getTruncatedInput(content.name, content.input || {}),
|
||||
toolUseId: content.id,
|
||||
fullInput: content.input || {},
|
||||
};
|
||||
this.emit('subagent:tool_call', toolCall);
|
||||
|
||||
@@ -846,6 +923,22 @@ export class SubagentWatcher extends EventEmitter {
|
||||
if (agentInfo) {
|
||||
agentInfo.toolCallCount++;
|
||||
}
|
||||
} else if (content.type === 'tool_result' && content.tool_use_id) {
|
||||
// Extract tool result
|
||||
const resultContent = this.extractToolResultContent(content.content);
|
||||
const toolName = this.pendingToolCalls.get(agentId)?.get(content.tool_use_id);
|
||||
|
||||
const toolResult: SubagentToolResult = {
|
||||
agentId,
|
||||
sessionId,
|
||||
timestamp: entry.timestamp,
|
||||
toolUseId: content.tool_use_id,
|
||||
tool: toolName,
|
||||
preview: resultContent.substring(0, 500),
|
||||
contentLength: resultContent.length,
|
||||
isError: content.is_error || false,
|
||||
};
|
||||
this.emit('subagent:tool_result', toolResult);
|
||||
} else if (content.type === 'text' && content.text) {
|
||||
const text = content.text.trim();
|
||||
if (text.length > 0) {
|
||||
@@ -862,27 +955,84 @@ export class SubagentWatcher extends EventEmitter {
|
||||
}
|
||||
}
|
||||
} else if (entry.type === 'user' && entry.message?.content) {
|
||||
// Handle both string and array content formats
|
||||
let userText: string | undefined;
|
||||
// Handle both string and array content formats - also check for tool_result in user messages
|
||||
if (typeof entry.message.content === 'string') {
|
||||
userText = entry.message.content.trim();
|
||||
const userText = entry.message.content.trim();
|
||||
if (userText.length > 0 && userText.length < 500) {
|
||||
const message: SubagentMessage = {
|
||||
agentId,
|
||||
sessionId,
|
||||
timestamp: entry.timestamp,
|
||||
role: 'user',
|
||||
text: userText,
|
||||
};
|
||||
this.emit('subagent:message', message);
|
||||
}
|
||||
} else {
|
||||
const firstContent = entry.message.content[0];
|
||||
if (firstContent?.type === 'text' && firstContent.text) {
|
||||
userText = firstContent.text.trim();
|
||||
// Check for tool_result blocks in user messages (common pattern)
|
||||
for (const content of entry.message.content) {
|
||||
if (content.type === 'tool_result' && content.tool_use_id) {
|
||||
const resultContent = this.extractToolResultContent(content.content);
|
||||
const toolName = this.pendingToolCalls.get(agentId)?.get(content.tool_use_id);
|
||||
|
||||
const toolResult: SubagentToolResult = {
|
||||
agentId,
|
||||
sessionId,
|
||||
timestamp: entry.timestamp,
|
||||
toolUseId: content.tool_use_id,
|
||||
tool: toolName,
|
||||
preview: resultContent.substring(0, 500),
|
||||
contentLength: resultContent.length,
|
||||
isError: content.is_error || false,
|
||||
};
|
||||
this.emit('subagent:tool_result', toolResult);
|
||||
} else if (content.type === 'text' && content.text) {
|
||||
const userText = content.text.trim();
|
||||
if (userText.length > 0 && userText.length < 500) {
|
||||
const message: SubagentMessage = {
|
||||
agentId,
|
||||
sessionId,
|
||||
timestamp: entry.timestamp,
|
||||
role: 'user',
|
||||
text: userText,
|
||||
};
|
||||
this.emit('subagent:message', message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (userText && userText.length > 0 && userText.length < 500) {
|
||||
const message: SubagentMessage = {
|
||||
agentId,
|
||||
sessionId,
|
||||
timestamp: entry.timestamp,
|
||||
role: 'user',
|
||||
text: userText,
|
||||
};
|
||||
this.emit('subagent:message', message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from tool_result content field
|
||||
*/
|
||||
private extractToolResultContent(content: string | Array<{ type: string; text?: string }> | undefined): string {
|
||||
if (!content) return '';
|
||||
if (typeof content === 'string') return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.filter(c => c.type === 'text' && c.text)
|
||||
.map(c => c.text)
|
||||
.join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get truncated input for display (keeps primary param, truncates large content)
|
||||
*/
|
||||
private getTruncatedInput(_tool: string, input: Record<string, unknown>): Record<string, unknown> {
|
||||
const truncated: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (typeof value === 'string' && value.length > 100) {
|
||||
// Keep short preview of long strings
|
||||
truncated[key] = value.substring(0, 100) + '...';
|
||||
} else {
|
||||
truncated[key] = value;
|
||||
}
|
||||
}
|
||||
return truncated;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+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