diff --git a/src-tauri/src/models/message.rs b/src-tauri/src/models/message.rs index 774d98e..cc051a6 100644 --- a/src-tauri/src/models/message.rs +++ b/src-tauri/src/models/message.rs @@ -10,6 +10,48 @@ pub enum MessageRole { Tool, } +/// A single tool call record from a subagent's execution transcript. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentToolCall { + pub tool_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub input_preview: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub output_preview: Option, + pub is_error: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentExecutionStats { + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub total_duration_ms: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub total_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub total_tool_use_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub read_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub search_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bash_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub edit_file_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lines_added: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lines_removed: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub other_tool_count: Option, + /// Tool calls extracted from the subagent's own JSONL transcript. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub tool_calls: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ContentBlock { @@ -31,6 +73,8 @@ pub enum ContentBlock { tool_use_id: Option, output_preview: Option, is_error: bool, + #[serde(skip_serializing_if = "Option::is_none")] + agent_stats: Option, }, Thinking { text: String, diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index e60c453..47386b6 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -15,7 +15,10 @@ pub use conversation::{ SidebarData, }; pub use folder::{FolderCommandInfo, FolderDetail, FolderHistoryEntry, OpenedConversation}; -pub use message::{ContentBlock, MessageRole, MessageTurn, TurnRole, TurnUsage, UnifiedMessage}; +pub use message::{ + AgentExecutionStats, AgentToolCall, ContentBlock, MessageRole, MessageTurn, TurnRole, + TurnUsage, UnifiedMessage, +}; pub use system::{ GitCredentials, GitDetectResult, GitHubAccountsSettings, GitHubTokenValidation, GitSettings, SystemLanguageSettings, SystemProxySettings, diff --git a/src-tauri/src/parsers/claude.rs b/src-tauri/src/parsers/claude.rs index 0776555..0a34f50 100644 --- a/src-tauri/src/parsers/claude.rs +++ b/src-tauri/src/parsers/claude.rs @@ -558,7 +558,7 @@ impl ClaudeParser { MessageRole::User }; - // Check toolUseResult.structuredPatch for real line numbers + // Check toolUseResult for structured patch and agent execution stats if let Some(tur) = value.get("toolUseResult") { if let Some(sp) = tur.get("structuredPatch") { let fp = tur @@ -581,6 +581,41 @@ impl ClaudeParser { } } } + + // Extract agent execution stats from toolUseResult + if tur.get("agentType").is_some() { + let mut stats = extract_agent_execution_stats(tur); + // Load tool calls from subagent's own JSONL transcript + if let Some(agent_id) = + tur.get("agentId").and_then(|v| v.as_str()) + { + // Reject path traversal: agentId must be alphanumeric + if !agent_id.is_empty() + && !agent_id.contains('/') + && !agent_id.contains('\\') + && !agent_id.contains("..") + { + let subagent_dir = + path.with_extension("").join("subagents"); + let subagent_path = subagent_dir + .join(format!("agent-{}.jsonl", agent_id)); + if subagent_path.exists() { + stats.tool_calls = + parse_subagent_tool_calls(&subagent_path); + } + } + } + for block in content.iter_mut() { + if let ContentBlock::ToolResult { + ref mut agent_stats, + .. + } = block + { + *agent_stats = Some(stats); + break; + } + } + } } messages.push(UnifiedMessage { @@ -776,6 +811,7 @@ impl ClaudeParser { tool_use_id: matching_id, output_preview, is_error, + agent_stats: None, }); } else { messages.push(UnifiedMessage { @@ -785,6 +821,7 @@ impl ClaudeParser { tool_use_id: matching_id, output_preview, is_error, + agent_stats: None, }], timestamp: parse_timestamp(&value).unwrap_or_else(Utc::now), usage: None, @@ -912,6 +949,7 @@ fn extract_user_content(value: &serde_json::Value) -> Vec { tool_use_id, output_preview: output, is_error, + agent_stats: None, }); } _ => {} @@ -1053,6 +1091,161 @@ fn extract_usage(value: &serde_json::Value) -> Option { }) } +fn extract_agent_execution_stats(tur: &serde_json::Value) -> AgentExecutionStats { + let tool_stats = tur.get("toolStats"); + AgentExecutionStats { + agent_type: tur + .get("agentType") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + status: tur + .get("status") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + total_duration_ms: tur.get("totalDurationMs").and_then(|v| v.as_u64()), + total_tokens: tur.get("totalTokens").and_then(|v| v.as_u64()), + total_tool_use_count: tur + .get("totalToolUseCount") + .and_then(|v| v.as_u64()) + .map(|v| v as u32), + read_count: tool_stats + .and_then(|s| s.get("readCount")) + .and_then(|v| v.as_u64()) + .map(|v| v as u32), + search_count: tool_stats + .and_then(|s| s.get("searchCount")) + .and_then(|v| v.as_u64()) + .map(|v| v as u32), + bash_count: tool_stats + .and_then(|s| s.get("bashCount")) + .and_then(|v| v.as_u64()) + .map(|v| v as u32), + edit_file_count: tool_stats + .and_then(|s| s.get("editFileCount")) + .and_then(|v| v.as_u64()) + .map(|v| v as u32), + lines_added: tool_stats + .and_then(|s| s.get("linesAdded")) + .and_then(|v| v.as_u64()) + .map(|v| v as u32), + lines_removed: tool_stats + .and_then(|s| s.get("linesRemoved")) + .and_then(|v| v.as_u64()) + .map(|v| v as u32), + other_tool_count: tool_stats + .and_then(|s| s.get("otherToolCount")) + .and_then(|v| v.as_u64()) + .map(|v| v as u32), + tool_calls: Vec::new(), + } +} + +/// Parse a subagent's JSONL transcript and extract its tool calls. +/// +/// The subagent JSONL has the same format as the main session: +/// assistant messages with tool_use blocks, followed by user messages +/// with tool_result blocks. We pair them by tool_use_id and produce +/// a compact list of `AgentToolCall` records. +fn parse_subagent_tool_calls(path: &PathBuf) -> Vec { + let file = match fs::File::open(path) { + Ok(f) => f, + Err(_) => return Vec::new(), + }; + let reader = BufReader::new(file); + + // Collect tool_use entries and build a result map + let mut calls: Vec<(String, String, Option)> = Vec::new(); // (id, name, input) + let mut results: std::collections::HashMap, bool)> = + std::collections::HashMap::new(); + + for line in reader.lines() { + let line = match line { + Ok(l) => l, + Err(_) => continue, + }; + if line.trim().is_empty() { + continue; + } + let value: serde_json::Value = match serde_json::from_str(&line) { + Ok(v) => v, + Err(_) => continue, + }; + + let msg_type = value.get("type").and_then(|t| t.as_str()).unwrap_or(""); + + if msg_type == "assistant" { + if let Some(content) = value + .get("message") + .and_then(|m| m.get("content")) + .and_then(|c| c.as_array()) + { + for item in content { + let block_type = item.get("type").and_then(|t| t.as_str()).unwrap_or(""); + if block_type == "tool_use" || block_type == "server_tool_use" { + let id = item + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let name = item + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + let input = item.get("input").map(|v| { + truncate_str(&v.to_string(), 500) + }); + if !id.is_empty() { + calls.push((id, name, input)); + } + } + } + } + } else if msg_type == "user" { + if let Some(content) = value + .get("message") + .and_then(|m| m.get("content")) + .and_then(|c| c.as_array()) + { + for item in content { + let block_type = item.get("type").and_then(|t| t.as_str()).unwrap_or(""); + if block_type == "tool_result" || block_type == "server_tool_result" { + let id = item + .get("tool_use_id") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let is_error = item + .get("is_error") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let output = extract_tool_result_text(item) + .map(|s| truncate_str(&s, 500)); + if !id.is_empty() { + results.insert(id, (output, is_error)); + } + } + } + } + } + } + + calls + .into_iter() + .map(|(id, name, input)| { + let (output, is_error) = results + .remove(&id) + .unwrap_or((None, false)); + AgentToolCall { + tool_name: name, + input_preview: input, + output_preview: output, + is_error, + } + }) + .collect() +} + fn extract_tool_result_text(item: &serde_json::Value) -> Option { let content = item.get("content")?; if let Some(text) = content.as_str() { diff --git a/src-tauri/src/parsers/cline.rs b/src-tauri/src/parsers/cline.rs index 8741199..4711b55 100644 --- a/src-tauri/src/parsers/cline.rs +++ b/src-tauri/src/parsers/cline.rs @@ -370,6 +370,7 @@ fn parse_user_message_parts(content: &serde_json::Value) -> UserMessageParts { tool_use_id: None, output_preview: Some(truncate_str(&output, 2000)), is_error, + agent_stats: None, }); // If the tool result also contains , extract it @@ -565,6 +566,7 @@ fn parse_content_blocks(content: &serde_json::Value) -> Vec { tool_use_id, output_preview, is_error, + agent_stats: None, }); } "thinking" => { diff --git a/src-tauri/src/parsers/codex.rs b/src-tauri/src/parsers/codex.rs index 4508f67..d48ed07 100644 --- a/src-tauri/src/parsers/codex.rs +++ b/src-tauri/src/parsers/codex.rs @@ -719,6 +719,7 @@ impl CodexParser { tool_use_id, output_preview: output, is_error, + agent_stats: None, }], timestamp, usage: None, diff --git a/src-tauri/src/parsers/gemini.rs b/src-tauri/src/parsers/gemini.rs index 3953816..a4a349a 100644 --- a/src-tauri/src/parsers/gemini.rs +++ b/src-tauri/src/parsers/gemini.rs @@ -434,6 +434,7 @@ impl GeminiParser { tool_use_id, output_preview, is_error, + agent_stats: None, }); } } diff --git a/src-tauri/src/parsers/openclaw.rs b/src-tauri/src/parsers/openclaw.rs index c8e47a5..b34913b 100644 --- a/src-tauri/src/parsers/openclaw.rs +++ b/src-tauri/src/parsers/openclaw.rs @@ -1053,6 +1053,7 @@ fn extract_tool_result_content(value: &serde_json::Value) -> Vec { tool_use_id, output_preview: output, is_error, + agent_stats: None, }); blocks @@ -1238,7 +1239,7 @@ mod tests { assert_eq!(blocks.len(), 1); assert!(matches!( &blocks[0], - ContentBlock::ToolResult { tool_use_id, output_preview, is_error } + ContentBlock::ToolResult { tool_use_id, output_preview, is_error, .. } if tool_use_id.as_deref() == Some("call_123") && output_preview.as_deref() == Some("file contents here") && !is_error diff --git a/src-tauri/src/parsers/opencode.rs b/src-tauri/src/parsers/opencode.rs index c500acd..3f00202 100644 --- a/src-tauri/src/parsers/opencode.rs +++ b/src-tauri/src/parsers/opencode.rs @@ -393,6 +393,7 @@ impl OpenCodeParser { tool_use_id: call_id, output_preview, is_error: is_error_status(status) || has_error_field, + agent_stats: None, }); } "file" => { diff --git a/src/components/message/agent-tool-call.tsx b/src/components/message/agent-tool-call.tsx new file mode 100644 index 0000000..8f203a6 --- /dev/null +++ b/src/components/message/agent-tool-call.tsx @@ -0,0 +1,269 @@ +import { memo, useMemo, useState, type ReactNode } from "react" +import type { AdaptedContentPart } from "@/lib/adapters/ai-elements-adapter" +import type { AgentToolCall } from "@/lib/types" +import { tryParseJson, extractJsonField } from "./content-parts-renderer" +import { MessageResponse } from "@/components/ai-elements/message" +import { Shimmer } from "@/components/ai-elements/shimmer" +import { getStatusBadge } from "@/components/ai-elements/tool" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { cn } from "@/lib/utils" +import { + ChevronDownIcon, + ChevronRightIcon, + CompassIcon, + MapIcon, + TerminalIcon, + WrenchIcon, +} from "lucide-react" +import { useTranslations } from "next-intl" + +// ── helpers ──────────────────────────────────────────────────────────── + +const ICON_CLASS = "size-4 text-muted-foreground" + +function getAgentIcon(subagentType: string | null) { + const t = subagentType?.toLowerCase() ?? "" + if (t.includes("explore")) return + if (t.includes("plan")) return + if (t.includes("bash")) return + return +} + +function getAccentColor(subagentType: string | null): string { + const t = subagentType?.toLowerCase() ?? "" + if (t.includes("explore")) + return "border-l-blue-500/50 dark:border-l-blue-400/40" + if (t.includes("plan")) + return "border-l-amber-500/50 dark:border-l-amber-400/40" + if (t.includes("bash")) + return "border-l-green-500/50 dark:border-l-green-400/40" + return "border-l-purple-500/50 dark:border-l-purple-400/40" +} + +function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms` + const sec = ms / 1000 + if (sec < 60) return `${sec.toFixed(1)}s` + return `${(sec / 60).toFixed(1)}m` +} + +/** Convert AgentToolCall[] to AdaptedContentPart[] for reuse with ToolCallPart */ +function adaptToolCalls( + calls: AgentToolCall[], + parentId: string +): AdaptedContentPart[] { + return calls.map( + (call, i): Extract => ({ + type: "tool-call", + toolCallId: `${parentId}-sub-${i}`, + toolName: call.tool_name, + input: call.input_preview ?? null, + state: call.is_error ? "output-error" : "output-available", + output: call.output_preview ?? null, + errorText: call.is_error ? (call.output_preview ?? undefined) : undefined, + }) + ) +} + +// ── main component ──────────────────────────────────────────────────── + +export const AgentToolCallPart = memo(function AgentToolCallPart({ + part, + renderToolCall, +}: { + part: Extract + /** Render a single tool-call part — injected by the parent to avoid + * circular imports (content-parts-renderer → agent-tool-call → renderer). */ + renderToolCall: ( + part: Extract, + key: string + ) => ReactNode +}) { + const t = useTranslations("Folder.chat.contentParts") + const tTool = useTranslations("Folder.chat.tool") + + const isRunning = + part.state === "input-available" || part.state === "input-streaming" + const isError = part.state === "output-error" + + const [bodyOpen, setBodyOpen] = useState(isRunning || isError) + const [promptOpen, setPromptOpen] = useState(false) + + const parsed = useMemo( + () => (part.input ? tryParseJson(part.input) : null), + [part.input] + ) + + const subagentType = useMemo( + () => + (parsed?.subagent_type as string | undefined) ?? + (part.input ? extractJsonField(part.input, "subagent_type") : null), + [parsed, part.input] + ) + + const description = useMemo( + () => + (parsed?.description as string | undefined) ?? + (part.input ? extractJsonField(part.input, "description") : null), + [parsed, part.input] + ) + + const prompt = useMemo( + () => + (parsed?.prompt as string | undefined) ?? + (part.input ? extractJsonField(part.input, "prompt") : null), + [parsed, part.input] + ) + + const model = useMemo( + () => + (parsed?.model as string | undefined) ?? + (part.input ? extractJsonField(part.input, "model") : null), + [parsed, part.input] + ) + + const icon = useMemo(() => getAgentIcon(subagentType), [subagentType]) + const accentColor = useMemo( + () => getAccentColor(subagentType), + [subagentType] + ) + + const title = useMemo(() => { + const prefix = subagentType ?? "Agent" + return description ? `${prefix}: ${description}` : prefix + }, [subagentType, description]) + + const statusLabel = + part.state === "input-available" + ? tTool("status.inputAvailable") + : part.state === "input-streaming" + ? tTool("status.inputStreaming") + : part.state === "output-available" + ? tTool("status.outputAvailable") + : tTool("status.outputError") + + const agentStats = part.agentStats ?? null + const adaptedToolCalls = useMemo( + () => adaptToolCalls(agentStats?.tool_calls ?? [], part.toolCallId), + [agentStats?.tool_calls, part.toolCallId] + ) + + const durationSuffix = useMemo(() => { + if (!agentStats?.total_duration_ms) return null + return formatDuration(agentStats.total_duration_ms) + }, [agentStats]) + + return ( + +
+ {/* Header — clickable to toggle body */} + +
+ {icon} + + {title} + + {!bodyOpen && durationSuffix && ( + + {durationSuffix} + + )} +
+
+ {getStatusBadge(part.state, statusLabel)} + +
+
+ + {/* Collapsible body */} + +
+ {/* Model + duration summary */} + {(model || durationSuffix) && ( +
+ {model && ( + + {t("agentModelLabel")}:{" "} + {model} + + )} + {durationSuffix && {durationSuffix}} +
+ )} + + {/* Collapsible prompt */} + {prompt && ( + + + + {t("agentPromptLabel")} + + +
+                    {prompt}
+                  
+
+
+ )} + + {/* Subagent tool calls — rendered with the same ToolCallPart + as the outer conversation for consistent appearance */} + {adaptedToolCalls.length > 0 && ( +
+ {adaptedToolCalls.map((tc, i) => + renderToolCall( + tc as Extract, + `subagent-tc-${i}` + ) + )} +
+ )} + + {/* Running indicator */} + {isRunning && !part.output && ( + + Running... + + )} + + {/* Error output */} + {isError && part.errorText && ( +
+
+                  {part.errorText}
+                
+
+ )} + + {/* Final output */} + {part.output && ( +
+ {part.output} +
+ )} +
+
+
+
+ ) +}) diff --git a/src/components/message/content-parts-renderer.tsx b/src/components/message/content-parts-renderer.tsx index eb42cb7..32fea62 100644 --- a/src/components/message/content-parts-renderer.tsx +++ b/src/components/message/content-parts-renderer.tsx @@ -23,6 +23,7 @@ import { ReasoningTrigger, ReasoningContent, } from "@/components/ai-elements/reasoning" +import { AgentToolCallPart } from "./agent-tool-call" import { FileTextIcon, FilePenLineIcon, @@ -45,7 +46,7 @@ import { // ── helpers ──────────────────────────────────────────────────────────── /** Try JSON.parse; return null on failure. */ -function tryParseJson(s: string): Record | null { +export function tryParseJson(s: string): Record | null { try { const v = JSON.parse(s) return typeof v === "object" && v !== null && !Array.isArray(v) ? v : null @@ -55,7 +56,7 @@ function tryParseJson(s: string): Record | null { } /** Regex-extract a JSON string value for a given key (works on truncated JSON). */ -function extractJsonField(input: string, key: string): string | null { +export function extractJsonField(input: string, key: string): string | null { const re = new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`) const m = input.match(re) return m?.[1]?.replace(/\\"/g, '"').replace(/\\\\/g, "\\") ?? null @@ -191,7 +192,7 @@ function shortPath(p: string): string { } /** Truncate text to maxLen, appending "…" if truncated. */ -function ellipsis(s: string, maxLen: number): string { +export function ellipsis(s: string, maxLen: number): string { return s.length > maxLen ? s.slice(0, maxLen - 1) + "…" : s } @@ -2240,6 +2241,19 @@ const ToolCallPart = memo(function ToolCallPart({ toolNameLower === "exitplanmode" || isFileTool) && !part.errorText + // Agent/subagent tools get a dedicated container rendering + if (toolNameLower === "agent") { + return ( + ( + // Strip agentStats to prevent recursive Agent nesting + + )} + /> + ) + } + // Cline: attempt_completion — render as an expanded card with result + progress if (toolNameLower === "attempt_completion") { const parsedCompletion = tryParseJson(part.input ?? "") @@ -2297,8 +2311,7 @@ const ToolCallPart = memo(function ToolCallPart({ output={part.output} /> )} - {(toolNameLower === "task" || toolNameLower === "agent") && - part.output ? ( + {toolNameLower === "task" && part.output ? (
{part.output}
diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 5de8eb8..2fbc280 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1703,6 +1703,13 @@ "subjectLabel": "الموضوع", "taskLabel": "المهمة", "nameLabel": "الاسم:", + "agentPromptLabel": "المطالبة", + "agentModelLabel": "النموذج", + "agentStatsBash": "الأوامر", + "agentStatsRead": "الملفات المقروءة", + "agentStatsSearch": "عمليات البحث", + "agentStatsEdit": "التعديلات", + "agentStatsOther": "أخرى", "field": { "file": "ملف", "notebook": "دفتر", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 95fffaf..7ed34c4 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -1703,6 +1703,13 @@ "subjectLabel": "Betreff", "taskLabel": "Aufgabe", "nameLabel": "Bezeichnung:", + "agentPromptLabel": "Eingabe", + "agentModelLabel": "Modell", + "agentStatsBash": "Befehle", + "agentStatsRead": "Dateien gelesen", + "agentStatsSearch": "Suchen", + "agentStatsEdit": "Bearbeitungen", + "agentStatsOther": "Sonstige", "field": { "file": "Datei", "notebook": "Notizbuch", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 7f82421..a9f6bf9 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1703,6 +1703,13 @@ "subjectLabel": "Subject", "taskLabel": "Task", "nameLabel": "Name:", + "agentPromptLabel": "Prompt", + "agentModelLabel": "Model", + "agentStatsBash": "Commands", + "agentStatsRead": "Files read", + "agentStatsSearch": "Searches", + "agentStatsEdit": "Edits", + "agentStatsOther": "Other", "field": { "file": "File", "notebook": "Notebook", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 5aaf42e..56e2291 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -1703,6 +1703,13 @@ "subjectLabel": "Asunto", "taskLabel": "Tarea", "nameLabel": "Nombre:", + "agentPromptLabel": "Instrucción", + "agentModelLabel": "Modelo", + "agentStatsBash": "Comandos", + "agentStatsRead": "Archivos leídos", + "agentStatsSearch": "Búsquedas", + "agentStatsEdit": "Ediciones", + "agentStatsOther": "Otros", "field": { "file": "Archivo", "notebook": "Cuaderno", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 8900853..831e21d 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1703,6 +1703,13 @@ "subjectLabel": "Sujet", "taskLabel": "Tâche", "nameLabel": "Nom :", + "agentPromptLabel": "Instruction", + "agentModelLabel": "Modèle", + "agentStatsBash": "Commandes", + "agentStatsRead": "Fichiers lus", + "agentStatsSearch": "Recherches", + "agentStatsEdit": "Modifications", + "agentStatsOther": "Autres", "field": { "file": "Fichier", "notebook": "Carnet", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index a7df72b..9cbf922 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -1703,6 +1703,13 @@ "subjectLabel": "件名", "taskLabel": "タスク", "nameLabel": "名前:", + "agentPromptLabel": "プロンプト", + "agentModelLabel": "モデル", + "agentStatsBash": "コマンド", + "agentStatsRead": "ファイル読取", + "agentStatsSearch": "検索", + "agentStatsEdit": "編集", + "agentStatsOther": "その他", "field": { "file": "ファイル", "notebook": "ノートブック", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 8f2b6f7..e8c2ea7 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -1703,6 +1703,13 @@ "subjectLabel": "제목", "taskLabel": "작업", "nameLabel": "이름:", + "agentPromptLabel": "프롬프트", + "agentModelLabel": "모델", + "agentStatsBash": "명령", + "agentStatsRead": "파일 읽기", + "agentStatsSearch": "검색", + "agentStatsEdit": "편집", + "agentStatsOther": "기타", "field": { "file": "파일", "notebook": "노트북", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index dfcc557..84038ab 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -1703,6 +1703,13 @@ "subjectLabel": "Assunto", "taskLabel": "Tarefa", "nameLabel": "Nome:", + "agentPromptLabel": "Instrução", + "agentModelLabel": "Modelo", + "agentStatsBash": "Comandos", + "agentStatsRead": "Arquivos lidos", + "agentStatsSearch": "Pesquisas", + "agentStatsEdit": "Edições", + "agentStatsOther": "Outros", "field": { "file": "Arquivo", "notebook": "Caderno", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index e46938b..0fbfefe 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1703,6 +1703,13 @@ "subjectLabel": "主题", "taskLabel": "任务", "nameLabel": "名称:", + "agentPromptLabel": "提示词", + "agentModelLabel": "模型", + "agentStatsBash": "命令", + "agentStatsRead": "读取文件", + "agentStatsSearch": "搜索", + "agentStatsEdit": "编辑", + "agentStatsOther": "其他", "field": { "file": "文件", "notebook": "笔记本", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 875387e..4082095 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -1703,6 +1703,13 @@ "subjectLabel": "主題", "taskLabel": "任務", "nameLabel": "名稱:", + "agentPromptLabel": "提示詞", + "agentModelLabel": "模型", + "agentStatsBash": "命令", + "agentStatsRead": "讀取檔案", + "agentStatsSearch": "搜尋", + "agentStatsEdit": "編輯", + "agentStatsOther": "其他", "field": { "file": "檔案", "notebook": "筆記本", diff --git a/src/lib/adapters/ai-elements-adapter.ts b/src/lib/adapters/ai-elements-adapter.ts index 4bd71a0..fc06776 100644 --- a/src/lib/adapters/ai-elements-adapter.ts +++ b/src/lib/adapters/ai-elements-adapter.ts @@ -3,6 +3,7 @@ import type { ContentBlock, MessageRole, TurnUsage, + AgentExecutionStats, } from "@/lib/types" /** @@ -25,6 +26,7 @@ export type AdaptedContentPart = state: ToolCallState output?: string | null errorText?: string + agentStats?: AgentExecutionStats | null } | { type: "tool-result" @@ -662,6 +664,7 @@ export function adaptMessageTurn( errorText: matchedResult.is_error ? matchedResult.output_preview || undefined : undefined, + agentStats: matchedResult.agent_stats ?? undefined, }) } else { // Position-based matching: if this tool_use has no ID, check next block @@ -687,6 +690,7 @@ export function adaptMessageTurn( errorText: positionalResult.is_error ? positionalResult.output_preview || undefined : undefined, + agentStats: positionalResult.agent_stats ?? undefined, }) } else { // For live streaming, unmatched tools are still running. diff --git a/src/lib/types.ts b/src/lib/types.ts index 94dac9a..899c9f9 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -44,6 +44,29 @@ export interface ConversationSummary { export type MessageRole = "user" | "assistant" | "system" | "tool" +export interface AgentToolCall { + tool_name: string + input_preview?: string | null + output_preview?: string | null + is_error: boolean +} + +export interface AgentExecutionStats { + agent_type?: string | null + status?: string | null + total_duration_ms?: number | null + total_tokens?: number | null + total_tool_use_count?: number | null + read_count?: number | null + search_count?: number | null + bash_count?: number | null + edit_file_count?: number | null + lines_added?: number | null + lines_removed?: number | null + other_tool_count?: number | null + tool_calls?: AgentToolCall[] +} + export type ContentBlock = | { type: "text"; text: string } | { @@ -63,6 +86,7 @@ export type ContentBlock = tool_use_id: string | null output_preview: string | null is_error: boolean + agent_stats?: AgentExecutionStats | null } | { type: "thinking"; text: string }