import { c as _c } from "react/compiler-runtime"; import type { ToolResultBlockParam, ToolUseBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'; import * as React from 'react'; import { ConfigurableShortcutHint } from 'src/components/ConfigurableShortcutHint.js'; import { CtrlOToExpand, SubAgentProvider } from 'src/components/CtrlOToExpand.js'; import { Byline } from 'src/components/design-system/Byline.js'; import { KeyboardShortcutHint } from 'src/components/design-system/KeyboardShortcutHint.js'; import type { z } from 'zod/v4'; import { AgentProgressLine } from '../../components/AgentProgressLine.js'; import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'; import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage.js'; import { Markdown } from '../../components/Markdown.js'; import { Message as MessageComponent } from '../../components/Message.js'; import { MessageResponse } from '../../components/MessageResponse.js'; import { ToolUseLoader } from '../../components/ToolUseLoader.js'; import { Box, Text } from '../../ink.js'; import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js'; import { findToolByName, type Tools } from '../../Tool.js'; import type { Message, ProgressMessage } from '../../types/message.js'; import type { AgentToolProgress } from '../../types/tools.js'; import { count } from '../../utils/array.js'; import { getSearchOrReadFromContent, getSearchReadSummaryText } from '../../utils/collapseReadSearch.js'; import { getDisplayPath } from '../../utils/file.js'; import { formatDuration, formatNumber } from '../../utils/format.js'; import { buildSubagentLookups, createAssistantMessage, EMPTY_LOOKUPS } from '../../utils/messages.js'; import type { ModelAlias } from '../../utils/model/aliases.js'; import { getMainLoopModel, parseUserSpecifiedModel, renderModelName } from '../../utils/model/model.js'; import type { Theme, ThemeName } from '../../utils/theme.js'; import type { outputSchema, Progress, RemoteLaunchedOutput } from './AgentTool.js'; import { inputSchema } from './AgentTool.js'; import { getAgentColor } from './agentColorManager.js'; import { GENERAL_PURPOSE_AGENT } from './built-in/generalPurposeAgent.js'; const MAX_PROGRESS_MESSAGES_TO_SHOW = 3; /** * Guard: checks if progress data has a `message` field (agent_progress or * skill_progress). Other progress types (e.g. bash_progress forwarded from * sub-agents) lack this field and must be skipped by UI helpers. */ function hasProgressMessage(data: Progress): data is AgentToolProgress { if (!('message' in data)) { return false; } const msg = (data as AgentToolProgress).message; return msg != null && typeof msg === 'object' && 'type' in msg; } /** * Check if a progress message is a search/read/REPL operation (tool use or result). * Returns { isSearch, isRead, isREPL } if it's a collapsible operation, null otherwise. * * For tool_result messages, uses the provided `toolUseByID` map to find the * corresponding tool_use block instead of relying on `normalizedMessages`. */ function getSearchOrReadInfo(progressMessage: ProgressMessage, tools: Tools, toolUseByID: Map): { isSearch: boolean; isRead: boolean; isREPL: boolean; } | null { if (!hasProgressMessage(progressMessage.data)) { return null; } const message = progressMessage.data.message; // Check tool_use (assistant message) if (message.type === 'assistant') { return getSearchOrReadFromContent(message.message.content[0], tools); } // Check tool_result (user message) - find corresponding tool use from the map if (message.type === 'user') { const content = message.message.content[0]; if (content?.type === 'tool_result') { const toolUse = toolUseByID.get(content.tool_use_id); if (toolUse) { return getSearchOrReadFromContent(toolUse, tools); } } } return null; } type SummaryMessage = { type: 'summary'; searchCount: number; readCount: number; replCount: number; uuid: string; isActive: boolean; // true if still in progress (last message was tool_use, not tool_result) }; type ProcessedMessage = { type: 'original'; message: ProgressMessage; } | SummaryMessage; /** * Process progress messages to group consecutive search/read operations into summaries. * For ants only - returns original messages for non-ants. * @param isAgentRunning - If true, the last group is always marked as active (in progress) */ function processProgressMessages(messages: ProgressMessage[], tools: Tools, isAgentRunning: boolean): ProcessedMessage[] { // Only process for ants if (("external" as string) !== 'ant') { return messages.filter((m): m is ProgressMessage => hasProgressMessage(m.data) && m.data.message.type !== 'user').map(m => ({ type: 'original', message: m })); } const result: ProcessedMessage[] = []; let currentGroup: { searchCount: number; readCount: number; replCount: number; startUuid: string; } | null = null; function flushGroup(isActive: boolean): void { if (currentGroup && (currentGroup.searchCount > 0 || currentGroup.readCount > 0 || currentGroup.replCount > 0)) { result.push({ type: 'summary', searchCount: currentGroup.searchCount, readCount: currentGroup.readCount, replCount: currentGroup.replCount, uuid: `summary-${currentGroup.startUuid}`, isActive }); } currentGroup = null; } const agentMessages = messages.filter((m): m is ProgressMessage => hasProgressMessage(m.data)); // Build tool_use lookup incrementally as we iterate const toolUseByID = new Map(); for (const msg of agentMessages) { // Track tool_use blocks as we see them if (msg.data.message.type === 'assistant') { for (const c of msg.data.message.message.content) { if (c.type === 'tool_use') { toolUseByID.set(c.id, c as ToolUseBlockParam); } } } const info = getSearchOrReadInfo(msg, tools, toolUseByID); if (info && (info.isSearch || info.isRead || info.isREPL)) { // This is a search/read/REPL operation - add to current group if (!currentGroup) { currentGroup = { searchCount: 0, readCount: 0, replCount: 0, startUuid: msg.uuid }; } // Only count tool_result messages (not tool_use) to avoid double counting if (msg.data.message.type === 'user') { if (info.isSearch) { currentGroup.searchCount++; } else if (info.isREPL) { currentGroup.replCount++; } else if (info.isRead) { currentGroup.readCount++; } } } else { // Non-search/read/REPL message - flush current group (completed) and add this message flushGroup(false); // Skip user tool_result messages — subagent progress messages lack // toolUseResult, so UserToolSuccessMessage returns null and the // height=1 Box in renderToolUseProgressMessage shows as a blank line. if (msg.data.message.type !== 'user') { result.push({ type: 'original', message: msg }); } } } // Flush any remaining group - it's active if the agent is still running flushGroup(isAgentRunning); return result; } const ESTIMATED_LINES_PER_TOOL = 9; const TERMINAL_BUFFER_LINES = 7; type Output = z.input>; export function AgentPromptDisplay(t0) { const $ = _c(3); const { prompt, dim: t1 } = t0; t1 === undefined ? false : t1; let t2; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t2 = Prompt:; $[0] = t2; } else { t2 = $[0]; } let t3; if ($[1] !== prompt) { t3 = {t2}{prompt}; $[1] = prompt; $[2] = t3; } else { t3 = $[2]; } return t3; } export function AgentResponseDisplay(t0) { const $ = _c(5); const { content } = t0; let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t1 = Response:; $[0] = t1; } else { t1 = $[0]; } let t2; if ($[1] !== content) { t2 = content.map(_temp); $[1] = content; $[2] = t2; } else { t2 = $[2]; } let t3; if ($[3] !== t2) { t3 = {t1}{t2}; $[3] = t2; $[4] = t3; } else { t3 = $[4]; } return t3; } function _temp(block, index) { return {block.text}; } type VerboseAgentTranscriptProps = { progressMessages: ProgressMessage[]; tools: Tools; verbose: boolean; }; function VerboseAgentTranscript(t0) { const $ = _c(15); const { progressMessages, tools, verbose } = t0; let t1; if ($[0] !== progressMessages) { t1 = buildSubagentLookups(progressMessages.filter(_temp2).map(_temp3)); $[0] = progressMessages; $[1] = t1; } else { t1 = $[1]; } const { lookups: agentLookups, inProgressToolUseIDs } = t1; let t2; if ($[2] !== agentLookups || $[3] !== inProgressToolUseIDs || $[4] !== progressMessages || $[5] !== tools || $[6] !== verbose) { const filteredMessages = progressMessages.filter(_temp4); let t3; if ($[8] !== agentLookups || $[9] !== inProgressToolUseIDs || $[10] !== tools || $[11] !== verbose) { t3 = progressMessage => ; $[8] = agentLookups; $[9] = inProgressToolUseIDs; $[10] = tools; $[11] = verbose; $[12] = t3; } else { t3 = $[12]; } t2 = filteredMessages.map(t3); $[2] = agentLookups; $[3] = inProgressToolUseIDs; $[4] = progressMessages; $[5] = tools; $[6] = verbose; $[7] = t2; } else { t2 = $[7]; } let t3; if ($[13] !== t2) { t3 = <>{t2}; $[13] = t2; $[14] = t3; } else { t3 = $[14]; } return t3; } function _temp4(pm_1) { if (!hasProgressMessage(pm_1.data)) { return false; } const msg = pm_1.data.message; if (msg.type === "user" && msg.toolUseResult === undefined) { return false; } return true; } function _temp3(pm_0) { return pm_0.data; } function _temp2(pm) { return hasProgressMessage(pm.data); } export function renderToolResultMessage(data: Output, progressMessagesForMessage: ProgressMessage[], { tools, verbose, theme, isTranscriptMode = false }: { tools: Tools; verbose: boolean; theme: ThemeName; isTranscriptMode?: boolean; }): React.ReactNode { // Remote-launched agents (ant-only) use a private output type not in the // public schema. Narrow via the internal discriminant. const internal = data as Output | RemoteLaunchedOutput; if (internal.status === 'remote_launched') { return Remote agent launched{' '} · {internal.taskId} · {internal.sessionUrl} ; } if (data.status === 'async_launched') { const { prompt } = data; return Backgrounded agent {!isTranscriptMode && {' ('} {prompt && } {')'} } {isTranscriptMode && prompt && } ; } if (data.status !== 'completed') { return null; } const { agentId, totalDurationMs, totalToolUseCount, totalTokens, usage, content, prompt } = data; const result = [totalToolUseCount === 1 ? '1 tool use' : `${totalToolUseCount} tool uses`, formatNumber(totalTokens) + ' tokens', formatDuration(totalDurationMs)]; const completionMessage = `Done (${result.join(' · ')})`; const finalAssistantMessage = createAssistantMessage({ content: completionMessage, usage: { ...usage, inference_geo: null, iterations: null, speed: null } as import('@anthropic-ai/sdk/resources/beta/messages/messages.mjs').BetaUsage }); return {("external" as string) === 'ant' && [ANT-ONLY] API calls: {getDisplayPath(getDumpPromptsPath(agentId))} } {isTranscriptMode && prompt && } {isTranscriptMode ? : null} {isTranscriptMode && content && content.length > 0 && } {!isTranscriptMode && {' '} } ; } export function renderToolUseMessage({ description, prompt }: Partial<{ description: string; prompt: string; }>): React.ReactNode { if (!description || !prompt) { return null; } return description; } export function renderToolUseTag(input: Partial<{ description: string; prompt: string; subagent_type: string; model?: ModelAlias; }>): React.ReactNode { const tags: React.ReactNode[] = []; if (input.model) { const mainModel = getMainLoopModel(); const agentModel = parseUserSpecifiedModel(input.model); if (agentModel !== mainModel) { tags.push( {renderModelName(agentModel)} ); } } if (tags.length === 0) { return null; } return <>{tags}; } const INITIALIZING_TEXT = 'Initializing…'; export function renderToolUseProgressMessage(progressMessages: ProgressMessage[], { tools, verbose, terminalSize, inProgressToolCallCount, isTranscriptMode = false }: { tools: Tools; verbose: boolean; terminalSize?: { columns: number; rows: number; }; inProgressToolCallCount?: number; isTranscriptMode?: boolean; }): React.ReactNode { if (!progressMessages.length) { return {INITIALIZING_TEXT} ; } // Checks to see if we should show a super condensed progress message summary. // This prevents flickers when the terminal size is too small to render all the dynamic content const toolToolRenderLinesEstimate = (inProgressToolCallCount ?? 1) * ESTIMATED_LINES_PER_TOOL + TERMINAL_BUFFER_LINES; const shouldUseCondensedMode = !isTranscriptMode && terminalSize && terminalSize.rows && terminalSize.rows < toolToolRenderLinesEstimate; const getProgressStats = () => { const toolUseCount = count(progressMessages, msg => { if (!hasProgressMessage(msg.data)) { return false; } const message = msg.data.message; return message.message.content.some(content => content.type === 'tool_use'); }); const latestAssistant = progressMessages.findLast((msg): msg is ProgressMessage => hasProgressMessage(msg.data) && msg.data.message.type === 'assistant'); let tokens = null; if (latestAssistant?.data.message.type === 'assistant') { const usage = latestAssistant.data.message.message.usage; tokens = (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + usage.input_tokens + usage.output_tokens; } return { toolUseCount, tokens }; }; if (shouldUseCondensedMode) { const { toolUseCount, tokens } = getProgressStats(); return In progress… · {toolUseCount} tool{' '} {toolUseCount === 1 ? 'use' : 'uses'} {tokens && ` · ${formatNumber(tokens)} tokens`} ·{' '} ; } // Process messages to group consecutive search/read operations into summaries (ants only) // isAgentRunning=true since this is the progress view while the agent is still running const processedMessages = processProgressMessages(progressMessages, tools, true); // For display, take the last few processed messages const displayedMessages = isTranscriptMode ? processedMessages : processedMessages.slice(-MAX_PROGRESS_MESSAGES_TO_SHOW); // Count hidden tool uses specifically (not all messages) to match the // final "Done (N tool uses)" count. Each tool use generates multiple // progress messages (tool_use + tool_result + text), so counting all // hidden messages inflates the number shown to the user. const hiddenMessages = isTranscriptMode ? [] : processedMessages.slice(0, Math.max(0, processedMessages.length - MAX_PROGRESS_MESSAGES_TO_SHOW)); const hiddenToolUseCount = count(hiddenMessages, m => { if (m.type === 'summary') { return m.searchCount + m.readCount + m.replCount > 0; } const data = m.message.data; if (!hasProgressMessage(data)) { return false; } return data.message.message.content.some(content => content.type === 'tool_use'); }); const firstData = progressMessages[0]?.data; const prompt = firstData && hasProgressMessage(firstData) ? firstData.prompt : undefined; // After grouping, displayedMessages can be empty when the only progress so // far is an assistant tool_use for a search/read op (grouped but not yet // counted, since counts increment on tool_result). Fall back to the // initializing text so MessageResponse doesn't render a bare ⎿. if (displayedMessages.length === 0 && !(isTranscriptMode && prompt)) { return {INITIALIZING_TEXT} ; } const { lookups: subagentLookups, inProgressToolUseIDs: collapsedInProgressIDs } = buildSubagentLookups(progressMessages.filter((pm): pm is ProgressMessage => hasProgressMessage(pm.data)).map(pm => pm.data)); return {isTranscriptMode && prompt && } {displayedMessages.map(processed => { if (processed.type === 'summary') { // Render summary for grouped search/read/REPL operations using shared formatting const summaryText = getSearchReadSummaryText(processed.searchCount, processed.readCount, processed.isActive, processed.replCount); return {summaryText} ; } // Render original message without height=1 wrapper so null // content (tool not found, renderToolUseMessage returns null) // doesn't leave a blank line. Tool call headers are single-line // anyway so truncation isn't needed. return ; })} {hiddenToolUseCount > 0 && +{hiddenToolUseCount} more tool{' '} {hiddenToolUseCount === 1 ? 'use' : 'uses'} } ; } export function renderToolUseRejectedMessage(_input: { description: string; prompt: string; subagent_type: string; }, { progressMessagesForMessage, tools, verbose, isTranscriptMode }: { columns: number; messages: Message[]; style?: 'condensed'; theme: ThemeName; progressMessagesForMessage: ProgressMessage[]; tools: Tools; verbose: boolean; isTranscriptMode?: boolean; }): React.ReactNode { // Get agentId from progress messages if available (agent was running before rejection) const firstData = progressMessagesForMessage[0]?.data; const agentId = firstData && hasProgressMessage(firstData) ? firstData.agentId : undefined; return <> {("external" as string) === 'ant' && agentId && [ANT-ONLY] API calls: {getDisplayPath(getDumpPromptsPath(agentId))} } {renderToolUseProgressMessage(progressMessagesForMessage, { tools, verbose, isTranscriptMode })} ; } export function renderToolUseErrorMessage(result: ToolResultBlockParam['content'], { progressMessagesForMessage, tools, verbose, isTranscriptMode }: { progressMessagesForMessage: ProgressMessage[]; tools: Tools; verbose: boolean; isTranscriptMode?: boolean; }): React.ReactNode { return <> {renderToolUseProgressMessage(progressMessagesForMessage, { tools, verbose, isTranscriptMode })} ; } function calculateAgentStats(progressMessages: ProgressMessage[]): { toolUseCount: number; tokens: number | null; } { const toolUseCount = count(progressMessages, msg => { if (!hasProgressMessage(msg.data)) { return false; } const message = msg.data.message; return message.type === 'user' && message.message.content.some(content => content.type === 'tool_result'); }); const latestAssistant = progressMessages.findLast((msg): msg is ProgressMessage => hasProgressMessage(msg.data) && msg.data.message.type === 'assistant'); let tokens = null; if (latestAssistant?.data.message.type === 'assistant') { const usage = latestAssistant.data.message.message.usage; tokens = (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + usage.input_tokens + usage.output_tokens; } return { toolUseCount, tokens }; } export function renderGroupedAgentToolUse(toolUses: Array<{ param: ToolUseBlockParam; isResolved: boolean; isError: boolean; isInProgress: boolean; progressMessages: ProgressMessage[]; result?: { param: ToolResultBlockParam; output: Output; }; }>, options: { shouldAnimate: boolean; tools: Tools; }): React.ReactNode | null { const { shouldAnimate, tools } = options; // Calculate stats for each agent const agentStats = toolUses.map(({ param, isResolved, isError, progressMessages, result }) => { const stats = calculateAgentStats(progressMessages); const lastToolInfo = extractLastToolInfo(progressMessages, tools); const parsedInput = inputSchema().safeParse(param.input); // teammate_spawned is not part of the exported Output type (cast through unknown // for dead code elimination), so check via string comparison on the raw value const isTeammateSpawn = result?.output?.status as string === 'teammate_spawned'; // For teammate spawns, show @name with type in parens and description as status let agentType: string; let description: string | undefined; let color: keyof Theme | undefined; let descriptionColor: keyof Theme | undefined; let taskDescription: string | undefined; if (isTeammateSpawn && parsedInput.success && parsedInput.data.name) { agentType = `@${parsedInput.data.name}`; const subagentType = parsedInput.data.subagent_type; description = isCustomSubagentType(subagentType) ? subagentType : undefined; taskDescription = parsedInput.data.description; // Use the custom agent definition's color on the type, not the name descriptionColor = isCustomSubagentType(subagentType) ? getAgentColor(subagentType) as keyof Theme | undefined : undefined; } else { agentType = parsedInput.success ? userFacingName(parsedInput.data) : 'Agent'; description = parsedInput.success ? parsedInput.data.description : undefined; color = parsedInput.success ? userFacingNameBackgroundColor(parsedInput.data) : undefined; taskDescription = undefined; } // Check if this was launched as a background agent OR backgrounded mid-execution const launchedAsAsync = parsedInput.success && 'run_in_background' in parsedInput.data && parsedInput.data.run_in_background === true; const outputStatus = (result?.output as { status?: string; } | undefined)?.status; const backgroundedMidExecution = outputStatus === 'async_launched' || outputStatus === 'remote_launched'; const isAsync = launchedAsAsync || backgroundedMidExecution || isTeammateSpawn; const name = parsedInput.success ? parsedInput.data.name : undefined; return { id: param.id, agentType, description, toolUseCount: stats.toolUseCount, tokens: stats.tokens, isResolved, isError, isAsync, color, descriptionColor, lastToolInfo, taskDescription, name }; }); const anyUnresolved = toolUses.some(t => !t.isResolved); const anyError = toolUses.some(t => t.isError); const allComplete = !anyUnresolved; // Check if all agents are the same type const allSameType = agentStats.length > 0 && agentStats.every(stat => stat.agentType === agentStats[0]?.agentType); const commonType = allSameType && agentStats[0]?.agentType !== 'Agent' ? agentStats[0]?.agentType : null; // Check if all resolved agents are async (background) const allAsync = agentStats.every(stat => stat.isAsync); return {allComplete ? allAsync ? <> {toolUses.length} background agents launched{' '} : <> {toolUses.length}{' '} {commonType ? `${commonType} agents` : 'agents'} finished : <> Running {toolUses.length}{' '} {commonType ? `${commonType} agents` : 'agents'}… }{' '} {!allAsync && } {agentStats.map((stat, index) => )} ; } export function userFacingName(input: Partial<{ description: string; prompt: string; subagent_type: string; name: string; team_name: string; }> | undefined): string { if (input?.subagent_type && input.subagent_type !== GENERAL_PURPOSE_AGENT.agentType) { // Display "worker" agents as "Agent" for cleaner UI if (input.subagent_type === 'worker') { return 'Agent'; } return input.subagent_type; } return 'Agent'; } export function userFacingNameBackgroundColor(input: Partial<{ description: string; prompt: string; subagent_type: string; }> | undefined): keyof Theme | undefined { if (!input?.subagent_type) { return undefined; } // Get the color for this agent return getAgentColor(input.subagent_type) as keyof Theme | undefined; } export function extractLastToolInfo(progressMessages: ProgressMessage[], tools: Tools): string | null { // Build tool_use lookup from all progress messages (needed for reverse iteration) const toolUseByID = new Map(); for (const pm of progressMessages) { if (!hasProgressMessage(pm.data)) { continue; } if (pm.data.message.type === 'assistant') { for (const c of pm.data.message.message.content) { if (c.type === 'tool_use') { toolUseByID.set(c.id, c as ToolUseBlockParam); } } } } // Count trailing consecutive search/read operations from the end let searchCount = 0; let readCount = 0; for (let i = progressMessages.length - 1; i >= 0; i--) { const msg = progressMessages[i]!; if (!hasProgressMessage(msg.data)) { continue; } const info = getSearchOrReadInfo(msg, tools, toolUseByID); if (info && (info.isSearch || info.isRead)) { // Only count tool_result messages to avoid double counting if (msg.data.message.type === 'user') { if (info.isSearch) { searchCount++; } else if (info.isRead) { readCount++; } } } else { break; } } if (searchCount + readCount >= 2) { return getSearchReadSummaryText(searchCount, readCount, true); } // Find the last tool_result message const lastToolResult = progressMessages.findLast((msg): msg is ProgressMessage => { if (!hasProgressMessage(msg.data)) { return false; } const message = msg.data.message; return message.type === 'user' && message.message.content.some(c => c.type === 'tool_result'); }); if (lastToolResult?.data.message.type === 'user') { const toolResultBlock = lastToolResult.data.message.message.content.find(c => c.type === 'tool_result'); if (toolResultBlock?.type === 'tool_result') { // Look up the corresponding tool_use — already indexed above const toolUseBlock = toolUseByID.get(toolResultBlock.tool_use_id); if (toolUseBlock) { const tool = findToolByName(tools, toolUseBlock.name); if (!tool) { return toolUseBlock.name; // Fallback to raw name } const input = toolUseBlock.input as Record; const parsedInput = tool.inputSchema.safeParse(input); // Get user-facing tool name const userFacingToolName = tool.userFacingName(parsedInput.success ? parsedInput.data : undefined); // Try to get summary from the tool itself if (tool.getToolUseSummary) { const summary = tool.getToolUseSummary(parsedInput.success ? parsedInput.data : undefined); if (summary) { return `${userFacingToolName}: ${summary}`; } } // Default: just show user-facing tool name return userFacingToolName; } } } return null; } function isCustomSubagentType(subagentType: string | undefined): subagentType is string { return !!subagentType && subagentType !== GENERAL_PURPOSE_AGENT.agentType && subagentType !== 'worker'; }