diff --git a/frontend/src/app/pages/AgentChat/agentToolParsing.ts b/frontend/src/app/pages/AgentChat/agentToolParsing.ts
new file mode 100644
index 00000000..1bec7fdc
--- /dev/null
+++ b/frontend/src/app/pages/AgentChat/agentToolParsing.ts
@@ -0,0 +1,65 @@
+import { parseMcpToolName } from './mcpToolName';
+
+export function isBrowserAgentTool(name: string): boolean {
+ if (name === 'CreateBrowserAgent' || name === 'BrowserAgent' || name === 'BrowserAgents') return true;
+ const mcp = parseMcpToolName(name);
+ return mcp.isMcp && mcp.serverSlug === 'openswarm-browser-agent';
+}
+
+export function isInvokeAgentTool(name: string): boolean {
+ if (name === 'InvokeAgent') return true;
+ const mcp = parseMcpToolName(name);
+ return mcp.isMcp && mcp.serverSlug === 'openswarm-invoke-agent';
+}
+
+export function isCreateAgentTool(name: string): boolean {
+ return name === 'Agent';
+}
+
+export function parseInvokedSessionId(rawText: string): string | null {
+ const match = rawText.match(/\(forked session:\s*([a-f0-9]+)\)/);
+ return match ? match[1] : null;
+}
+
+export interface InvokeAgentParsed {
+ agentName: string;
+ sessionId: string | null;
+ cost: string | null;
+ response: string;
+}
+
+export function parseCreateAgentResult(rawText: string): string {
+ if (!rawText) return '';
+ try {
+ const parsed = JSON.parse(rawText);
+ if (typeof parsed === 'string') return parsed;
+ if (typeof parsed === 'object' && parsed !== null) {
+ if (parsed.text) return parsed.text;
+ if (parsed.content) return typeof parsed.content === 'string' ? parsed.content : JSON.stringify(parsed.content);
+ if (parsed.result) return typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
+ }
+ } catch {}
+ return rawText;
+}
+
+export function parseInvokeAgentResult(rawText: string): InvokeAgentParsed | null {
+ const headerMatch = rawText.match(
+ /\*\*Invoked Agent Result\*\*(?:\s*;\s*(.+?))?\s*\(forked session:\s*([a-f0-9]+)\)/,
+ );
+ if (!headerMatch) return null;
+
+ const agentName = headerMatch[1]?.trim() || 'Agent';
+ const sessionId = headerMatch[2];
+
+ const costMatch = rawText.match(/\*Cost:\s*\$([0-9.]+)\*/);
+ const cost = costMatch ? costMatch[1] : null;
+
+ const bodyStart = rawText.indexOf('\n\n');
+ let response = bodyStart >= 0 ? rawText.slice(bodyStart + 2).trim() : '';
+ if (response.startsWith('*Cost:')) {
+ const afterCost = response.indexOf('\n');
+ response = afterCost >= 0 ? response.slice(afterCost + 1).trim() : '';
+ }
+
+ return { agentName, sessionId, cost, response };
+}
diff --git a/frontend/src/app/pages/AgentChat/mcpToolName.ts b/frontend/src/app/pages/AgentChat/mcpToolName.ts
new file mode 100644
index 00000000..0bb911d8
--- /dev/null
+++ b/frontend/src/app/pages/AgentChat/mcpToolName.ts
@@ -0,0 +1,66 @@
+export interface McpToolInfo {
+ isMcp: boolean;
+ serverSlug: string;
+ action: string;
+ service: string;
+ displayName: string;
+}
+
+export function parseMcpToolName(rawName: string): McpToolInfo {
+ const m = rawName.match(/^mcp__([^_]+(?:-[^_]+)*)__(.+)$/);
+ if (!m) return { isMcp: false, serverSlug: '', action: '', service: '', displayName: rawName };
+ const serverSlug = m[1];
+ const action = m[2];
+ const spaced = action.replace(/_/g, ' ').toLowerCase();
+ const display = spaced.charAt(0).toUpperCase() + spaced.slice(1);
+
+ const lower = action.toLowerCase();
+ let service = '';
+ if (lower.includes('gmail') || lower.includes('email') || lower.includes('mail')) service = 'gmail';
+ else if (lower.includes('calendar') || lower.includes('event') || lower.includes('freebusy')) service = 'calendar';
+ else if (lower.includes('drive') || lower.includes('file')) service = 'drive';
+ else if (lower.includes('sheet') || lower.includes('spreadsheet')) service = 'sheets';
+ else if (lower.includes('doc') || lower.includes('paragraph')) service = 'docs';
+ else if (lower.includes('contact')) service = 'contacts';
+
+ return { isMcp: true, serverSlug, action, service, displayName: display };
+}
+
+export function getMcpInputSummary(input: any): string {
+ if (!input || typeof input !== 'object') return '';
+ const keys = Object.keys(input);
+ if (keys.length === 0) return '';
+ if (keys.length === 1) {
+ const v = input[keys[0]];
+ const s = typeof v === 'string' ? v : JSON.stringify(v);
+ return s.length > 60 ? s.slice(0, 60) + '…' : s;
+ }
+ return keys.slice(0, 3).map((k) => {
+ const v = input[k];
+ const s = typeof v === 'string' ? v : JSON.stringify(v);
+ return `${k}: ${s.length > 30 ? s.slice(0, 30) + '…' : s}`;
+ }).join(' ');
+}
+
+export function getMcpShortAction(mcpInfo: McpToolInfo): string {
+ const { action, service } = mcpInfo;
+ let short = action;
+ if (service && action.toLowerCase().startsWith(service.toLowerCase() + '_')) {
+ short = action.slice(service.length + 1);
+ }
+ const lower = short.replace(/_/g, ' ').toLowerCase();
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
+}
+
+export function getGmailHeader(msg: any, name: string): string {
+ if (msg.payload?.headers && Array.isArray(msg.payload.headers)) {
+ const h = msg.payload.headers.find(
+ (hdr: any) => (hdr.name || '').toLowerCase() === name.toLowerCase()
+ );
+ if (h) return h.value || '';
+ }
+ if (msg.headers && typeof msg.headers === 'object' && !Array.isArray(msg.headers)) {
+ return msg.headers[name] || msg.headers[name.toLowerCase()] || '';
+ }
+ return '';
+}
diff --git a/frontend/src/app/pages/AgentChat/toolBubbleChrome.tsx b/frontend/src/app/pages/AgentChat/toolBubbleChrome.tsx
new file mode 100644
index 00000000..267301c6
--- /dev/null
+++ b/frontend/src/app/pages/AgentChat/toolBubbleChrome.tsx
@@ -0,0 +1,75 @@
+import React, { useState, useEffect } from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { useClaudeTokens } from '@/shared/styles/ThemeContext';
+
+let toolCallKeyframesInjected = false;
+export function ensureToolCallKeyframes() {
+ if (toolCallKeyframesInjected) return;
+ toolCallKeyframesInjected = true;
+ const style = document.createElement('style');
+ style.setAttribute('data-tool-call-keyframes', '');
+ style.textContent = `
+@keyframes tool-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.4; }
+}
+@keyframes border-glow {
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(var(--glow-rgb), 0); }
+ 50% { box-shadow: 0 0 10px 2px rgba(var(--glow-rgb), 0.12); }
+}
+@keyframes blink-cursor {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+`;
+ document.head.appendChild(style);
+}
+
+export const ElapsedTimer: React.FC<{ startTime: string }> = ({ startTime }) => {
+ const c = useClaudeTokens();
+ const [elapsed, setElapsed] = useState(0);
+
+ useEffect(() => {
+ const start = new Date(startTime).getTime();
+ const tick = () => setElapsed(Math.floor((Date.now() - start) / 1000));
+ tick();
+ const interval = setInterval(tick, 1000);
+ return () => clearInterval(interval);
+ }, [startTime]);
+
+ const mins = Math.floor(elapsed / 60);
+ const secs = elapsed % 60;
+ const display = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
+
+ return (
+
+
+
+ {display}
+
+
+ );
+};
+
+export function formatElapsed(ms: number): string {
+ if (ms >= 60000) return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
+ if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
+ return `${ms}ms`;
+}
diff --git a/frontend/src/app/pages/AgentChat/toolColorize.tsx b/frontend/src/app/pages/AgentChat/toolColorize.tsx
new file mode 100644
index 00000000..d3efdf8d
--- /dev/null
+++ b/frontend/src/app/pages/AgentChat/toolColorize.tsx
@@ -0,0 +1,218 @@
+import React from 'react';
+import { useThemeMode } from '@/shared/styles/ThemeContext';
+import { parseMcpToolName } from './mcpToolName';
+import { isBashTool } from './toolResultParsing';
+
+export interface TermColors {
+ TERM_BG: string;
+ TERM_BORDER: string;
+ PROMPT_COLOR: string;
+ CMD_COLOR: string;
+ OUTPUT_COLOR: string;
+ PATH_COLOR: string;
+ ADD_COLOR: string;
+ DEL_COLOR: string;
+ STDERR_COLOR: string;
+ WARN_COLOR: string;
+ NUM_COLOR: string;
+ DIM_COLOR: string;
+ DIFF_HEADER_COLOR: string;
+ SCROLLBAR_THUMB: string;
+}
+
+const darkTermColors: TermColors = {
+ TERM_BG: '#131520',
+ TERM_BORDER: '#1e2030',
+ PROMPT_COLOR: '#7ec699',
+ CMD_COLOR: '#e8ecf4',
+ OUTPUT_COLOR: '#a0aab8',
+ PATH_COLOR: '#82aaff',
+ ADD_COLOR: '#7ec699',
+ DEL_COLOR: '#ff8787',
+ STDERR_COLOR: '#ff8787',
+ WARN_COLOR: '#ffcb6b',
+ NUM_COLOR: '#f78c6c',
+ DIM_COLOR: '#555b6e',
+ DIFF_HEADER_COLOR: '#c792ea',
+ SCROLLBAR_THUMB: '#2a2d3e',
+};
+
+const lightTermColors: TermColors = {
+ TERM_BG: '#f4f3ee',
+ TERM_BORDER: '#e2e0d8',
+ PROMPT_COLOR: '#2d7a3e',
+ CMD_COLOR: '#2a2a28',
+ OUTPUT_COLOR: '#555550',
+ PATH_COLOR: '#3060a8',
+ ADD_COLOR: '#2d7a3e',
+ DEL_COLOR: '#c03030',
+ STDERR_COLOR: '#c03030',
+ WARN_COLOR: '#8a6518',
+ NUM_COLOR: '#c05020',
+ DIM_COLOR: '#9e9c95',
+ DIFF_HEADER_COLOR: '#7c4daa',
+ SCROLLBAR_THUMB: '#ccc9c0',
+};
+
+export function useTermColors(): TermColors {
+ const { mode } = useThemeMode();
+ return mode === 'dark' ? darkTermColors : lightTermColors;
+}
+
+export function colorizeInput(toolName: string, text: string, tc: TermColors): React.ReactNode {
+ const n = toolName.toLowerCase();
+ const mcp = parseMcpToolName(toolName);
+
+ if (mcp.isMcp) {
+ const lines = text.split('\n');
+ return (
+ <>
+ {lines.map((line, i) => {
+ const nl = i < lines.length - 1 ? '\n' : '';
+ const colonIdx = line.indexOf(':');
+ if (colonIdx > 0 && colonIdx < 30) {
+ return (
+
+ {line.slice(0, colonIdx + 1)}
+ {line.slice(colonIdx + 1)}
+ {nl}
+
+ );
+ }
+ return {line}{nl};
+ })}
+ >
+ );
+ }
+
+ if (isBashTool(toolName)) return {text};
+
+ if (n === 'edit' || n === 'strreplace' || n === 'multiedit') {
+ const lines = text.split('\n');
+ return (
+ <>
+ {lines.map((line, i) => {
+ const nl = i < lines.length - 1 ? '\n' : '';
+ if (i === 0 && (line.startsWith('/') || line.includes('.')))
+ return {line}{nl};
+ if (line.startsWith('+ '))
+ return {line}{nl};
+ if (line.startsWith('- '))
+ return {line}{nl};
+ return {line}{nl};
+ })}
+ >
+ );
+ }
+
+ if (n === 'write') {
+ const lines = text.split('\n');
+ return (
+ <>
+ {lines.map((line, i) => {
+ const nl = i < lines.length - 1 ? '\n' : '';
+ if (i === 0 && (line.startsWith('/') || line.includes('.')))
+ return {line}{nl};
+ return {line}{nl};
+ })}
+ >
+ );
+ }
+
+ if (n === 'read' || n === 'glob' || n === 'webfetch') {
+ if (/^\//.test(text) || text.includes('/'))
+ return {text};
+ }
+
+ if (n === 'grep' || n === 'ripgrep') {
+ const lines = text.split('\n');
+ return (
+ <>
+ {lines.map((line, i) => {
+ const nl = i < lines.length - 1 ? '\n' : '';
+ if (line.startsWith('pattern:'))
+ return (
+
+ pattern:
+ {line.slice(9)}
+ {nl}
+
+ );
+ if (line.startsWith('path:'))
+ return (
+
+ path:
+ {line.slice(6)}
+ {nl}
+
+ );
+ return {line}{nl};
+ })}
+ >
+ );
+ }
+
+ return {text};
+}
+
+export function colorizeOutput(toolName: string, text: string, tc: TermColors): React.ReactNode {
+ if (!text) return (empty);
+
+ const lines = text.split('\n');
+ const n = toolName.toLowerCase();
+
+ return (
+ <>
+ {lines.map((line, i) => {
+ const nl = i < lines.length - 1 ? '\n' : '';
+ const trimmed = line.trimStart();
+
+ if (/^\/\S+/.test(trimmed))
+ return {line}{nl};
+
+ if (n === 'grep' || n === 'ripgrep') {
+ const grepMatch = line.match(/^(\S+?:\d+[:-])/);
+ if (grepMatch) {
+ return (
+
+ {grepMatch[1]}
+ {line.slice(grepMatch[1].length)}
+ {nl}
+
+ );
+ }
+ const fileHeader = line.match(/^(\S+\.\w+)$/);
+ if (fileHeader)
+ return {line}{nl};
+ }
+
+ if (line.startsWith('@@') && line.includes('@@'))
+ return {line}{nl};
+ if (line.startsWith('+'))
+ return {line}{nl};
+ if (line.startsWith('-'))
+ return {line}{nl};
+
+ if (/\b[Ee]rror\b/.test(line))
+ return {line}{nl};
+ if (/\b[Ww]arning\b/.test(line))
+ return {line}{nl};
+
+ if (n === 'read') {
+ const lineNumMatch = line.match(/^(\s*\d+\s*[|:])/);
+ if (lineNumMatch) {
+ return (
+
+ {lineNumMatch[1]}
+ {line.slice(lineNumMatch[1].length)}
+ {nl}
+
+ );
+ }
+ }
+
+ return {line}{nl};
+ })}
+ >
+ );
+}
diff --git a/frontend/src/app/pages/AgentChat/toolResultParsing.ts b/frontend/src/app/pages/AgentChat/toolResultParsing.ts
new file mode 100644
index 00000000..784d4a62
--- /dev/null
+++ b/frontend/src/app/pages/AgentChat/toolResultParsing.ts
@@ -0,0 +1,271 @@
+import { AgentMessage } from '@/shared/state/agentsSlice';
+import { prettyPath, prettyUrl, quoteQuery, bashCommandDetail } from './toolLabels';
+import { parseMcpToolName, getMcpInputSummary, getGmailHeader } from './mcpToolName';
+
+export function getToolData(call: AgentMessage) {
+ const content = typeof call.content === 'object' ? call.content : {};
+ return {
+ toolName: content.tool || 'Unknown',
+ input: content.input || {},
+ isDenied: content.approved === false,
+ toolId: content.id,
+ };
+}
+
+export function isBashTool(name: string) {
+ return name === 'Bash' || name === 'bash';
+}
+
+export function getInputSummary(toolName: string, input: any): string {
+ try {
+ const mcp = parseMcpToolName(toolName);
+ if (mcp.isMcp) return getMcpInputSummary(input);
+
+ const n = toolName.toLowerCase();
+ if (isBashTool(toolName)) {
+ return bashCommandDetail(input.command || '');
+ }
+ if (n === 'read' || n === 'write' || n === 'edit' || n === 'multiedit' || n === 'strreplace')
+ return prettyPath(input.file_path || input.path || '');
+ if (n === 'glob') return input.pattern || input.glob || input.glob_pattern || '';
+ if (n === 'grep' || n === 'ripgrep') {
+ const pat = input.pattern || input.regex || '';
+ const path = input.path || input.directory || '';
+ const q = quoteQuery(pat);
+ return path ? `${q} in ${prettyPath(path)}` : q;
+ }
+ if (n === 'websearch') return quoteQuery(input.query || input.search_term || '');
+ if (n === 'webfetch') return prettyUrl(input.url || '');
+ if (n === 'todoread' || n === 'todowrite') return '';
+ if (n === 'ls') return prettyPath(input.path || '.');
+ if (n === 'mcpactivate') return '';
+ if (n === 'mcpsearch' || n === 'outputsearch') return quoteQuery(input.query || '');
+ if (n === 'outputactivate') return input.output_id || '';
+ if (n === 'renderoutput') return input.output_id || '';
+ return '';
+ } catch {
+ return '';
+ }
+}
+
+function formatMcpInputDisplay(input: any): string {
+ if (!input || typeof input !== 'object') return String(input ?? '');
+ return Object.entries(input)
+ .map(([k, v]) => {
+ const s = typeof v === 'string' ? v : JSON.stringify(v, null, 2);
+ return `${k}: ${s}`;
+ })
+ .join('\n');
+}
+
+export function formatInputDisplay(toolName: string, input: any): string {
+ try {
+ const mcp = parseMcpToolName(toolName);
+ if (mcp.isMcp) return formatMcpInputDisplay(input);
+
+ const n = toolName.toLowerCase();
+ if (isBashTool(toolName)) return input.command || '';
+ if (n === 'read') {
+ const p = input.file_path || input.path || '';
+ const parts = [p];
+ if (input.offset) parts.push(`offset: ${input.offset}`);
+ if (input.limit) parts.push(`limit: ${input.limit}`);
+ return parts.join(' ');
+ }
+ if (n === 'write') {
+ const p = input.file_path || input.path || '';
+ const content = input.content || '';
+ const preview = content.length > 300 ? content.slice(0, 300) + '\n…' : content;
+ return `${p}\n\n${preview}`;
+ }
+ if (n === 'edit' || n === 'strreplace') {
+ const p = input.file_path || input.path || '';
+ const old = input.old_string || input.old_text || '';
+ const nw = input.new_string || input.new_text || '';
+ const lines = [p, ''];
+ if (old) {
+ const oldPreview = old.length > 200 ? old.slice(0, 200) + '…' : old;
+ lines.push(`- ${oldPreview.split('\n').join('\n- ')}`);
+ }
+ if (nw) {
+ const nwPreview = nw.length > 200 ? nw.slice(0, 200) + '…' : nw;
+ lines.push(`+ ${nwPreview.split('\n').join('\n+ ')}`);
+ }
+ return lines.join('\n');
+ }
+ if (n === 'multiedit') {
+ const p = input.file_path || input.path || '';
+ const edits = input.edits || [];
+ const lines = [p];
+ for (const e of edits.slice(0, 3)) {
+ const old = e.old_string || e.old_text || '';
+ lines.push(` - ${old.split('\n')[0].slice(0, 60)}…`);
+ }
+ if (edits.length > 3) lines.push(` … +${edits.length - 3} more edits`);
+ return lines.join('\n');
+ }
+ if (n === 'glob') return input.pattern || input.glob || input.glob_pattern || '';
+ if (n === 'grep' || n === 'ripgrep') {
+ const pat = input.pattern || input.regex || '';
+ const path = input.path || input.directory || '';
+ const parts = [`pattern: ${pat}`];
+ if (path) parts.push(`path: ${path}`);
+ if (input.include) parts.push(`include: ${input.include}`);
+ return parts.join('\n');
+ }
+ if (n === 'websearch') return input.query || input.search_term || '';
+ if (n === 'webfetch') return input.url || '';
+ } catch {}
+ if (typeof input === 'string') return input;
+ return JSON.stringify(input, null, 2);
+}
+
+export interface ParsedBashResult {
+ type: 'bash';
+ stdout: string;
+ stderr: string;
+ exitCode: number | null;
+}
+
+export interface ParsedTextResult {
+ type: 'text';
+ content: string;
+ isError?: boolean;
+}
+
+export interface ParsedMcpResult {
+ type: 'mcp';
+ service: string;
+ action: string;
+ data: Record;
+ rawText: string;
+}
+
+export type ParsedResult = ParsedBashResult | ParsedTextResult | ParsedMcpResult;
+
+export function parseToolResult(toolName: string, rawText: string): ParsedResult {
+ if (isBashTool(toolName)) {
+ try {
+ const parsed = JSON.parse(rawText);
+ if (typeof parsed === 'object' && parsed !== null && 'stdout' in parsed) {
+ const exitMatch = (parsed.stdout || '').match(/[Ee]xit code:\s*(\d+)/);
+ return {
+ type: 'bash',
+ stdout: parsed.stdout || '',
+ stderr: parsed.stderr || '',
+ exitCode: exitMatch ? parseInt(exitMatch[1], 10) : null,
+ };
+ }
+ } catch {}
+ }
+
+ const mcp = parseMcpToolName(toolName);
+ if (mcp.isMcp) {
+ try {
+ let parsed = JSON.parse(rawText);
+
+ if (Array.isArray(parsed) && parsed.some((b: any) => b?.type === 'text' && typeof b?.text === 'string')) {
+ const textContent = parsed
+ .filter((b: any) => b?.type === 'text')
+ .map((b: any) => b.text)
+ .join('\n');
+ try {
+ parsed = JSON.parse(textContent);
+ } catch {
+ return { type: 'mcp', service: mcp.service, action: mcp.action, data: {}, rawText: textContent };
+ }
+ }
+
+ if (typeof parsed === 'object' && parsed !== null) {
+ return { type: 'mcp', service: mcp.service, action: mcp.action, data: parsed, rawText };
+ }
+ } catch {}
+ return { type: 'mcp', service: mcp.service, action: mcp.action, data: {}, rawText };
+ }
+
+ try {
+ const parsed = JSON.parse(rawText);
+ if (typeof parsed === 'object' && parsed !== null) {
+ if ('stdout' in parsed) {
+ return { type: 'text', content: parsed.stdout || '' };
+ }
+ if ('content' in parsed && typeof parsed.content === 'string') {
+ return { type: 'text', content: parsed.content, isError: !!parsed.is_error };
+ }
+ if ('result' in parsed && typeof parsed.result === 'string') {
+ return { type: 'text', content: parsed.result };
+ }
+ if ('output' in parsed && typeof parsed.output === 'string') {
+ return { type: 'text', content: parsed.output };
+ }
+ const n = toolName.toLowerCase();
+ if (n === 'glob' && Array.isArray(parsed)) {
+ return { type: 'text', content: parsed.join('\n') };
+ }
+ }
+ } catch {}
+
+ return { type: 'text', content: rawText };
+}
+
+export function getResultSummary(toolName: string, rawText: string): string {
+ const parsed = parseToolResult(toolName, rawText);
+
+ if (parsed.type === 'bash') {
+ const lines = parsed.stdout.split('\n').filter((l) => l.trim()).length;
+ if (parsed.exitCode !== null && parsed.exitCode !== 0) return `exit ${parsed.exitCode}`;
+ if (parsed.stderr && !parsed.stdout) return 'stderr';
+ return `${lines} line${lines !== 1 ? 's' : ''}`;
+ }
+
+ if (parsed.type === 'mcp') {
+ const d = parsed.data;
+ if (parsed.service === 'gmail') {
+ const subj = d.subject || getGmailHeader(d, 'Subject');
+ if (subj) return subj;
+ if (Array.isArray(d.messages)) return `${d.messages.length} email${d.messages.length !== 1 ? 's' : ''}`;
+ if (d.id || d.messageId) return 'sent';
+ }
+ if (parsed.service === 'calendar') {
+ if (d.summary) return d.summary.slice(0, 40);
+ if (Array.isArray(d.items)) return `${d.items.length} event${d.items.length !== 1 ? 's' : ''}`;
+ }
+ if (parsed.service === 'drive') {
+ if (d.name) return d.name;
+ if (Array.isArray(d.files)) return `${d.files.length} file${d.files.length !== 1 ? 's' : ''}`;
+ }
+ if (d.error || d.is_error) return 'error';
+ return '';
+ }
+
+ const text = parsed.content;
+ const lines = text.split('\n');
+ const lineCount = lines.length;
+ const n = toolName.toLowerCase();
+
+ try {
+ if (n === 'glob') {
+ const fileCount = lines.filter((l) => l.trim()).length;
+ return `${fileCount} file${fileCount !== 1 ? 's' : ''}`;
+ }
+ if (n === 'grep' || n === 'ripgrep') {
+ const matchCount = lines.filter((l) => l.trim()).length;
+ return `${matchCount} match${matchCount !== 1 ? 'es' : ''}`;
+ }
+ if (n === 'read') return `${lineCount} lines`;
+ if (n === 'write') return '';
+ if (n === 'edit' || n === 'multiedit' || n === 'strreplace') return '';
+ if (n === 'websearch') return 'results';
+ if (n === 'webfetch') return `${lineCount} lines`;
+ if (parsed.isError) return 'error';
+ } catch {}
+
+ return `${lineCount} line${lineCount !== 1 ? 's' : ''}`;
+}
+
+export function getPromptPrefix(toolName: string): string {
+ if (isBashTool(toolName)) return '$ ';
+ const mcp = parseMcpToolName(toolName);
+ if (mcp.isMcp) return `❯ ${mcp.displayName} `;
+ return `❯ ${toolName} `;
+}