import { Box, Text } from "ink";
import { memo } from "react";
import { INTERRUPTED_BY_USER } from "../../constants";
import { clipToolReturn } from "../../tools/manager.js";
import {
formatArgsDisplay,
parsePatchInput,
parsePatchOperations,
} from "../helpers/formatArgsDisplay.js";
import {
getDisplayToolName,
isFileEditTool,
isFileWriteTool,
isMemoryTool,
isPatchTool,
isPlanTool,
isTaskTool,
isTodoTool,
} from "../helpers/toolNameMapping.js";
import { useTerminalWidth } from "../hooks/useTerminalWidth";
import { BlinkDot } from "./BlinkDot.js";
import { colors } from "./colors.js";
import {
EditRenderer,
MultiEditRenderer,
WriteRenderer,
} from "./DiffRenderer.js";
import { MarkdownDisplay } from "./MarkdownDisplay.js";
import { MemoryDiffRenderer } from "./MemoryDiffRenderer.js";
import { PlanRenderer } from "./PlanRenderer.js";
import { TodoRenderer } from "./TodoRenderer.js";
type ToolCallLine = {
kind: "tool_call";
id: string;
toolCallId?: string;
name?: string;
argsText?: string;
resultText?: string;
resultOk?: boolean;
phase: "streaming" | "ready" | "running" | "finished";
};
/**
* ToolCallMessageRich - Rich formatting version with old layout logic
* This preserves the exact wrapping and spacing logic from the old codebase
*
* Features:
* - Two-column layout for tool calls (2 chars for dot)
* - Smart wrapping that keeps function name and args together when possible
* - Blinking dots for pending/running states
* - Result shown with ⎿ prefix underneath
*/
export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => {
const columns = useTerminalWidth();
// Parse and format the tool call
const rawName = line.name ?? "?";
const argsText = line.argsText ?? "...";
// Task tool - handled by SubagentGroupDisplay, don't render here
// Exception: Cancelled/rejected Task tools should be rendered inline
// since they won't appear in SubagentGroupDisplay
if (isTaskTool(rawName)) {
const isCancelledOrRejected =
line.phase === "finished" && line.resultOk === false;
if (!isCancelledOrRejected) {
return null;
}
}
// Apply tool name remapping
let displayName = getDisplayToolName(rawName);
// For Patch tools, override display name based on patch content
// (Add → Write, Update → Update, Delete → Delete)
if (isPatchTool(rawName)) {
try {
const parsedArgs = JSON.parse(argsText);
if (parsedArgs.input) {
const patchInfo = parsePatchInput(parsedArgs.input);
if (patchInfo) {
if (patchInfo.kind === "add") displayName = "Write";
else if (patchInfo.kind === "update") displayName = "Update";
else if (patchInfo.kind === "delete") displayName = "Delete";
}
}
} catch {
// Keep default "Patch" name if parsing fails
}
}
// Format arguments for display using the old formatting logic
// Pass rawName to enable special formatting for file tools
const formatted = formatArgsDisplay(argsText, rawName);
const args = `(${formatted.display})`;
const rightWidth = Math.max(0, columns - 2); // gutter is 2 cols
// If name exceeds available width, fall back to simple wrapped rendering
const fallback = displayName.length >= rightWidth;
// Determine dot state based on phase
const getDotElement = () => {
switch (line.phase) {
case "streaming":
return ●;
case "ready":
return ;
case "running":
return ;
case "finished":
if (line.resultOk === false) {
return ●;
}
return ●;
default:
return ●;
}
};
// Format result for display
const getResultElement = () => {
if (!line.resultText) return null;
const prefix = ` ⎿ `; // Match old format: 2 spaces, glyph, 2 spaces
const prefixWidth = 5; // Total width of prefix
const contentWidth = Math.max(0, columns - prefixWidth);
// Special cases from old ToolReturnBlock (check before truncation)
if (line.resultText === "Running...") {
return (
{prefix}
Running...
);
}
if (line.resultText === INTERRUPTED_BY_USER) {
return (
{prefix}
{INTERRUPTED_BY_USER}
);
}
// Truncate the result text for display (UI only, API gets full response)
// Strip trailing newlines to avoid extra visual spacing (e.g., from bash echo)
const displayResultText = clipToolReturn(line.resultText).replace(
/\n+$/,
"",
);
// Helper to check if a value is a record
const isRecord = (v: unknown): v is Record =>
typeof v === "object" && v !== null;
// Check if this is a todo_write tool with successful result
if (
isTodoTool(rawName, displayName) &&
line.resultOk !== false &&
line.argsText
) {
try {
const parsedArgs = JSON.parse(line.argsText);
if (parsedArgs.todos && Array.isArray(parsedArgs.todos)) {
// Convert todos to safe format for TodoRenderer
// Note: Anthropic/Codex use "content", Gemini uses "description"
const safeTodos = parsedArgs.todos.map((t: unknown, i: number) => {
const rec = isRecord(t) ? t : {};
const status: "pending" | "in_progress" | "completed" =
rec.status === "completed"
? "completed"
: rec.status === "in_progress"
? "in_progress"
: "pending";
const id = typeof rec.id === "string" ? rec.id : String(i);
// Handle both "content" (Anthropic/Codex) and "description" (Gemini) fields
const content =
typeof rec.content === "string"
? rec.content
: typeof rec.description === "string"
? rec.description
: JSON.stringify(t);
const priority: "high" | "medium" | "low" | undefined =
rec.priority === "high"
? "high"
: rec.priority === "medium"
? "medium"
: rec.priority === "low"
? "low"
: undefined;
return { content, status, id, priority };
});
// Return TodoRenderer directly - it has its own prefix
return ;
}
} catch {
// If parsing fails, fall through to regular handling
}
}
// Check if this is an update_plan tool with successful result
if (
isPlanTool(rawName, displayName) &&
line.resultOk !== false &&
line.argsText
) {
try {
const parsedArgs = JSON.parse(line.argsText);
if (parsedArgs.plan && Array.isArray(parsedArgs.plan)) {
// Convert plan items to safe format for PlanRenderer
const safePlan = parsedArgs.plan.map((item: unknown) => {
const rec = isRecord(item) ? item : {};
const status: "pending" | "in_progress" | "completed" =
rec.status === "completed"
? "completed"
: rec.status === "in_progress"
? "in_progress"
: "pending";
const step =
typeof rec.step === "string" ? rec.step : JSON.stringify(item);
return { step, status };
});
const explanation =
typeof parsedArgs.explanation === "string"
? parsedArgs.explanation
: undefined;
// Return PlanRenderer directly - it has its own prefix
return ;
}
} catch {
// If parsing fails, fall through to regular handling
}
}
// Check if this is a memory tool - show diff instead of raw result
if (isMemoryTool(rawName) && line.resultOk !== false && line.argsText) {
const memoryDiff = (
);
if (memoryDiff) {
return memoryDiff;
}
// If MemoryDiffRenderer returns null, fall through to regular handling
}
// Check if this is a file edit tool - show diff instead of success message
if (isFileEditTool(rawName) && line.resultOk !== false && line.argsText) {
try {
const parsedArgs = JSON.parse(line.argsText);
const filePath = parsedArgs.file_path || "";
// Multi-edit: has edits array
if (parsedArgs.edits && Array.isArray(parsedArgs.edits)) {
const edits = parsedArgs.edits.map(
(e: { old_string?: string; new_string?: string }) => ({
old_string: e.old_string || "",
new_string: e.new_string || "",
}),
);
return (
);
}
// Single edit: has old_string/new_string
if (parsedArgs.old_string !== undefined) {
return (
);
}
} catch {
// If parsing fails, fall through to regular handling
}
}
// Check if this is a file write tool - show written content
if (isFileWriteTool(rawName) && line.resultOk !== false && line.argsText) {
try {
const parsedArgs = JSON.parse(line.argsText);
const filePath = parsedArgs.file_path || "";
const content = parsedArgs.content || "";
if (filePath && content) {
return ;
}
} catch {
// If parsing fails, fall through to regular handling
}
}
// Check if this is a patch tool - show diff/content based on operation type
if (isPatchTool(rawName) && line.resultOk !== false && line.argsText) {
try {
const parsedArgs = JSON.parse(line.argsText);
if (parsedArgs.input) {
const operations = parsePatchOperations(parsedArgs.input);
if (operations.length > 0) {
return (
{operations.map((op) => {
if (op.kind === "add") {
return (
);
}
if (op.kind === "update") {
return (
);
}
if (op.kind === "delete") {
const gutterWidth = 4;
return (
{" "}
⎿
Deleted {op.path}
);
}
return null;
})}
);
}
}
} catch {
// If parsing fails, fall through to regular handling
}
}
// Regular result handling
const isError = line.resultOk === false;
// Try to parse JSON for cleaner error display
let displayText = displayResultText;
try {
const parsed = JSON.parse(displayResultText);
if (parsed.error && typeof parsed.error === "string") {
displayText = parsed.error;
}
} catch {
// Not JSON, use raw text
}
// Format tool denial errors more user-friendly
if (isError && displayText.includes("request to call tool denied")) {
// Use [\s\S]+ to match multiline reasons
const match = displayText.match(/User reason: ([\s\S]+)$/);
const reason = match?.[1]?.trim() || "(empty)";
displayText = `User rejected the tool call with reason: ${reason}`;
}
return (
{prefix}
{isError ? (
{displayText}
) : (
)}
);
};
return (
{/* Tool call with exact wrapping logic from old codebase */}
{getDotElement()}
{fallback ? (
{isMemoryTool(rawName) ? (
<>
{displayName}
{args}
>
) : (
<>
{displayName}
{args}
>
)}
) : (
{displayName}
{args ? (
{args}
) : null}
)}
{/* Tool result (if present) */}
{getResultElement()}
);
});
ToolCallMessage.displayName = "ToolCallMessage";