mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 07:29: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:
+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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user