import * as os from "node:os";
import { Box, Container, getCapabilities, getImageDimensions, Image, imageFallback, Spacer, Text, truncateToWidth, } from "@hyperspaceng/neural-pods-tui";
import stripAnsi from "strip-ansi";
import { computeEditDiff } from "../../../core/tools/edit-diff.js";
import { allTools } from "../../../core/tools/index.js";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/tools/truncate.js";
import { convertToPng } from "../../../utils/image-convert.js";
import { sanitizeBinaryOutput } from "../../../utils/shell.js";
import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
import { renderDiff } from "./diff.js";
import { keyHint } from "./keybinding-hints.js";
import { truncateToVisualLines } from "./visual-truncate.js";
// Preview line limit for bash when not expanded
const BASH_PREVIEW_LINES = 5;
// During partial write tool-call streaming, re-highlight the first N lines fully
// to keep multiline tokenization mostly correct without re-highlighting the full file.
const WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50;
/**
 * Convert absolute path to tilde notation if it's in home directory
 */
function shortenPath(path) {
    if (typeof path !== "string")
        return "";
    const home = os.homedir();
    if (path.startsWith(home)) {
        return `~${path.slice(home.length)}`;
    }
    return path;
}
/**
 * Replace tabs with spaces for consistent rendering
 */
function replaceTabs(text) {
    return text.replace(/\t/g, "   ");
}
/**
 * Normalize control characters for terminal preview rendering.
 * Keep tool arguments unchanged, sanitize only display text.
 */
function normalizeDisplayText(text) {
    return text.replace(/\r/g, "");
}
/** Safely coerce value to string for display. Returns null if invalid type. */
function str(value) {
    if (typeof value === "string")
        return value;
    if (value == null)
        return "";
    return null; // Invalid type
}
/**
 * Component that renders a tool call with its result (updateable)
 */
export class ToolExecutionComponent extends Container {
    contentBox; // Used for custom tools and bash visual truncation
    contentText; // For built-in tools (with its own padding/bg)
    imageComponents = [];
    imageSpacers = [];
    toolName;
    args;
    expanded = false;
    showImages;
    isPartial = true;
    toolDefinition;
    ui;
    cwd;
    result;
    // Cached edit diff preview (computed when args arrive, before tool executes)
    editDiffPreview;
    editDiffArgsKey; // Track which args the preview is for
    // Cached converted images for Kitty protocol (which requires PNG), keyed by index
    convertedImages = new Map();
    // Incremental syntax highlighting cache for write tool call args
    writeHighlightCache;
    // When true, this component intentionally renders no lines
    hideComponent = false;
    bashStartedAt;
    bashElapsedInterval;
    constructor(toolName, args, options = {}, toolDefinition, ui, cwd = process.cwd()) {
        super();
        this.toolName = toolName;
        this.args = args;
        this.showImages = options.showImages ?? true;
        this.toolDefinition = toolDefinition;
        this.ui = ui;
        this.cwd = cwd;
        this.addChild(new Spacer(1));
        // Always create both - contentBox for custom tools/bash, contentText for other built-ins
        this.contentBox = new Box(1, 1, (text) => theme.bg("toolPendingBg", text));
        this.contentText = new Text("", 1, 1, (text) => theme.bg("toolPendingBg", text));
        // Use contentBox for bash (visual truncation) or custom tools with custom renderers
        // Use contentText for built-in tools (including overrides without custom renderers)
        if (toolName === "bash" || (toolDefinition && !this.shouldUseBuiltInRenderer())) {
            this.addChild(this.contentBox);
        }
        else {
            this.addChild(this.contentText);
        }
        this.updateDisplay();
    }
    /**
     * Check if we should use built-in rendering for this tool.
     * Returns true if the tool name is a built-in AND either there's no toolDefinition
     * or the toolDefinition doesn't provide custom renderers.
     */
    shouldUseBuiltInRenderer() {
        const isBuiltInName = this.toolName in allTools;
        const hasCustomRenderers = this.toolDefinition?.renderCall || this.toolDefinition?.renderResult;
        return isBuiltInName && !hasCustomRenderers;
    }
    updateArgs(args) {
        this.args = args;
        if (this.toolName === "write" && this.isPartial) {
            this.updateWriteHighlightCacheIncremental();
        }
        this.updateDisplay();
    }
    markExecutionStarted() {
        if (this.toolName !== "bash" || this.bashStartedAt !== undefined)
            return;
        this.bashStartedAt = Date.now();
        this.ensureBashElapsedTimer();
        this.updateDisplay();
        this.ui.requestRender();
    }
    ensureBashElapsedTimer() {
        if (this.toolName !== "bash" || !this.isPartial || this.bashStartedAt === undefined || this.bashElapsedInterval)
            return;
        this.bashElapsedInterval = setInterval(() => {
            this.updateDisplay();
            this.ui.requestRender();
        }, 1000);
    }
    stopBashElapsedTimer() {
        if (!this.bashElapsedInterval)
            return;
        clearInterval(this.bashElapsedInterval);
        this.bashElapsedInterval = undefined;
    }
    getBashDurationMs() {
        if (this.toolName !== "bash" || this.bashStartedAt === undefined)
            return undefined;
        return Date.now() - this.bashStartedAt;
    }
    formatDuration(ms) {
        return `${(ms / 1000).toFixed(1)}s`;
    }
    highlightSingleLine(line, lang) {
        const highlighted = highlightCode(line, lang);
        return highlighted[0] ?? "";
    }
    refreshWriteHighlightPrefix(cache) {
        const prefixCount = Math.min(WRITE_PARTIAL_FULL_HIGHLIGHT_LINES, cache.normalizedLines.length);
        if (prefixCount === 0)
            return;
        const prefixSource = cache.normalizedLines.slice(0, prefixCount).join("\n");
        const prefixHighlighted = highlightCode(prefixSource, cache.lang);
        for (let i = 0; i < prefixCount; i++) {
            cache.highlightedLines[i] =
                prefixHighlighted[i] ?? this.highlightSingleLine(cache.normalizedLines[i] ?? "", cache.lang);
        }
    }
    rebuildWriteHighlightCacheFull(rawPath, fileContent) {
        const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
        if (!lang) {
            this.writeHighlightCache = undefined;
            return;
        }
        const displayContent = normalizeDisplayText(fileContent);
        const normalized = replaceTabs(displayContent);
        this.writeHighlightCache = {
            rawPath,
            lang,
            rawContent: fileContent,
            normalizedLines: normalized.split("\n"),
            highlightedLines: highlightCode(normalized, lang),
        };
    }
    updateWriteHighlightCacheIncremental() {
        const rawPath = str(this.args?.file_path ?? this.args?.path);
        const fileContent = str(this.args?.content);
        if (rawPath === null || fileContent === null) {
            this.writeHighlightCache = undefined;
            return;
        }
        const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
        if (!lang) {
            this.writeHighlightCache = undefined;
            return;
        }
        if (!this.writeHighlightCache) {
            this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
            return;
        }
        const cache = this.writeHighlightCache;
        if (cache.lang !== lang || cache.rawPath !== rawPath) {
            this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
            return;
        }
        if (!fileContent.startsWith(cache.rawContent)) {
            this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
            return;
        }
        if (fileContent.length === cache.rawContent.length) {
            return;
        }
        const deltaRaw = fileContent.slice(cache.rawContent.length);
        const deltaDisplay = normalizeDisplayText(deltaRaw);
        const deltaNormalized = replaceTabs(deltaDisplay);
        cache.rawContent = fileContent;
        if (cache.normalizedLines.length === 0) {
            cache.normalizedLines.push("");
            cache.highlightedLines.push("");
        }
        const segments = deltaNormalized.split("\n");
        const lastIndex = cache.normalizedLines.length - 1;
        cache.normalizedLines[lastIndex] += segments[0];
        cache.highlightedLines[lastIndex] = this.highlightSingleLine(cache.normalizedLines[lastIndex], cache.lang);
        for (let i = 1; i < segments.length; i++) {
            cache.normalizedLines.push(segments[i]);
            cache.highlightedLines.push(this.highlightSingleLine(segments[i], cache.lang));
        }
        this.refreshWriteHighlightPrefix(cache);
    }
    /**
     * Signal that args are complete (tool is about to execute).
     * This triggers diff computation for edit tool.
     */
    setArgsComplete() {
        if (this.toolName === "write") {
            const rawPath = str(this.args?.file_path ?? this.args?.path);
            const fileContent = str(this.args?.content);
            if (rawPath !== null && fileContent !== null) {
                this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
            }
        }
        this.maybeComputeEditDiff();
    }
    /**
     * Compute edit diff preview when we have complete args.
     * This runs async and updates display when done.
     */
    maybeComputeEditDiff() {
        if (this.toolName !== "edit")
            return;
        const path = this.args?.path;
        const oldText = this.args?.oldText;
        const newText = this.args?.newText;
        // Need all three params to compute diff
        if (!path || oldText === undefined || newText === undefined)
            return;
        // Create a key to track which args this computation is for
        const argsKey = JSON.stringify({ path, oldText, newText });
        // Skip if we already computed for these exact args
        if (this.editDiffArgsKey === argsKey)
            return;
        this.editDiffArgsKey = argsKey;
        // Compute diff async
        computeEditDiff(path, oldText, newText, this.cwd).then((result) => {
            // Only update if args haven't changed since we started
            if (this.editDiffArgsKey === argsKey) {
                this.editDiffPreview = result;
                this.updateDisplay();
                this.ui.requestRender();
            }
        });
    }
    updateResult(result, isPartial = false) {
        this.result = result;
        this.isPartial = isPartial;
        if (this.toolName === "bash") {
            if (isPartial) {
                this.ensureBashElapsedTimer();
            }
            else {
                this.stopBashElapsedTimer();
            }
        }
        if (this.toolName === "write" && !isPartial) {
            const rawPath = str(this.args?.file_path ?? this.args?.path);
            const fileContent = str(this.args?.content);
            if (rawPath !== null && fileContent !== null) {
                this.rebuildWriteHighlightCacheFull(rawPath, fileContent);
            }
        }
        this.updateDisplay();
        // Convert non-PNG images to PNG for Kitty protocol (async)
        this.maybeConvertImagesForKitty();
    }
    /**
     * Convert non-PNG images to PNG for Kitty graphics protocol.
     * Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
     */
    maybeConvertImagesForKitty() {
        const caps = getCapabilities();
        // Only needed for Kitty protocol
        if (caps.images !== "kitty")
            return;
        if (!this.result)
            return;
        const imageBlocks = this.result.content?.filter((c) => c.type === "image") || [];
        for (let i = 0; i < imageBlocks.length; i++) {
            const img = imageBlocks[i];
            if (!img.data || !img.mimeType)
                continue;
            // Skip if already PNG or already converted
            if (img.mimeType === "image/png")
                continue;
            if (this.convertedImages.has(i))
                continue;
            // Convert async
            const index = i;
            convertToPng(img.data, img.mimeType).then((converted) => {
                if (converted) {
                    this.convertedImages.set(index, converted);
                    this.updateDisplay();
                    this.ui.requestRender();
                }
            });
        }
    }
    setExpanded(expanded) {
        this.expanded = expanded;
        this.updateDisplay();
    }
    setShowImages(show) {
        this.showImages = show;
        this.updateDisplay();
    }
    invalidate() {
        super.invalidate();
        this.updateDisplay();
    }
    render(width) {
        if (this.hideComponent) {
            return [];
        }
        return super.render(width);
    }
    updateDisplay() {
        // Set background based on state
        const bgFn = this.isPartial
            ? (text) => theme.bg("toolPendingBg", text)
            : this.result?.isError
                ? (text) => theme.bg("toolErrorBg", text)
                : (text) => theme.bg("toolSuccessBg", text);
        const useBuiltInRenderer = this.shouldUseBuiltInRenderer();
        let customRendererHasContent = false;
        this.hideComponent = false;
        // Use built-in rendering for built-in tools (or overrides without custom renderers)
        if (useBuiltInRenderer) {
            if (this.toolName === "bash") {
                // Bash uses Box with visual line truncation
                this.contentBox.setBgFn(bgFn);
                this.contentBox.clear();
                this.renderBashContent();
            }
            else {
                // Other built-in tools: use Text directly with caching
                this.contentText.setCustomBgFn(bgFn);
                this.contentText.setText(this.formatToolExecution());
            }
        }
        else if (this.toolDefinition) {
            // Custom tools use Box for flexible component rendering
            this.contentBox.setBgFn(bgFn);
            this.contentBox.clear();
            // Render call component
            if (this.toolDefinition.renderCall) {
                try {
                    const callComponent = this.toolDefinition.renderCall(this.args, theme);
                    if (callComponent !== undefined) {
                        this.contentBox.addChild(callComponent);
                        customRendererHasContent = true;
                    }
                }
                catch {
                    // Fall back to default on error
                    this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
                    customRendererHasContent = true;
                }
            }
            else {
                // No custom renderCall, show tool name
                this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
                customRendererHasContent = true;
            }
            // Render result component if we have a result
            if (this.result && this.toolDefinition.renderResult) {
                try {
                    const resultComponent = this.toolDefinition.renderResult({ content: this.result.content, details: this.result.details }, { expanded: this.expanded, isPartial: this.isPartial }, theme);
                    if (resultComponent !== undefined) {
                        this.contentBox.addChild(resultComponent);
                        customRendererHasContent = true;
                    }
                }
                catch {
                    // Fall back to showing raw output on error
                    const output = this.getTextOutput();
                    if (output) {
                        this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
                        customRendererHasContent = true;
                    }
                }
            }
            else if (this.result) {
                // Has result but no custom renderResult
                const output = this.getTextOutput();
                if (output) {
                    this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
                    customRendererHasContent = true;
                }
            }
        }
        else {
            // Unknown tool with no registered definition - show generic fallback
            this.contentText.setCustomBgFn(bgFn);
            this.contentText.setText(this.formatToolExecution());
        }
        // Handle images (same for both custom and built-in)
        for (const img of this.imageComponents) {
            this.removeChild(img);
        }
        this.imageComponents = [];
        for (const spacer of this.imageSpacers) {
            this.removeChild(spacer);
        }
        this.imageSpacers = [];
        if (this.result) {
            const imageBlocks = this.result.content?.filter((c) => c.type === "image") || [];
            const caps = getCapabilities();
            for (let i = 0; i < imageBlocks.length; i++) {
                const img = imageBlocks[i];
                if (caps.images && this.showImages && img.data && img.mimeType) {
                    // Use converted PNG for Kitty protocol if available
                    const converted = this.convertedImages.get(i);
                    const imageData = converted?.data ?? img.data;
                    const imageMimeType = converted?.mimeType ?? img.mimeType;
                    // For Kitty, skip non-PNG images that haven't been converted yet
                    if (caps.images === "kitty" && imageMimeType !== "image/png") {
                        continue;
                    }
                    const spacer = new Spacer(1);
                    this.addChild(spacer);
                    this.imageSpacers.push(spacer);
                    const imageComponent = new Image(imageData, imageMimeType, { fallbackColor: (s) => theme.fg("toolOutput", s) }, { maxWidthCells: 60 });
                    this.imageComponents.push(imageComponent);
                    this.addChild(imageComponent);
                }
            }
        }
        if (!useBuiltInRenderer && this.toolDefinition) {
            this.hideComponent = !customRendererHasContent && this.imageComponents.length === 0;
        }
    }
    /**
     * Render bash content using visual line truncation (like bash-execution.ts)
     */
    renderBashContent() {
        const command = str(this.args?.command);
        const timeout = this.args?.timeout;
        // Header
        const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
        const commandDisplay = command === null ? theme.fg("error", "[invalid arg]") : command ? command : theme.fg("toolOutput", "...");
        this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix, 0, 0));
        if (this.result) {
            const output = this.getTextOutput().trim();
            if (output) {
                // Style each line for the output
                const styledOutput = output
                    .split("\n")
                    .map((line) => theme.fg("toolOutput", line))
                    .join("\n");
                if (this.expanded) {
                    // Show all lines when expanded
                    this.contentBox.addChild(new Text(`\n${styledOutput}`, 0, 0));
                }
                else {
                    // Use visual line truncation when collapsed with width-aware caching
                    let cachedWidth;
                    let cachedLines;
                    let cachedSkipped;
                    this.contentBox.addChild({
                        render: (width) => {
                            if (cachedLines === undefined || cachedWidth !== width) {
                                const result = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);
                                cachedLines = result.visualLines;
                                cachedSkipped = result.skippedCount;
                                cachedWidth = width;
                            }
                            if (cachedSkipped && cachedSkipped > 0) {
                                const hint = theme.fg("muted", `... (${cachedSkipped} earlier lines,`) +
                                    ` ${keyHint("app.tools.expand", "to expand")})`;
                                return ["", truncateToWidth(hint, width, "..."), ...cachedLines];
                            }
                            // Add blank line for spacing (matches expanded case)
                            return ["", ...cachedLines];
                        },
                        invalidate: () => {
                            cachedWidth = undefined;
                            cachedLines = undefined;
                            cachedSkipped = undefined;
                        },
                    });
                }
            }
            // Truncation warnings
            const truncation = this.result.details?.truncation;
            const fullOutputPath = this.result.details?.fullOutputPath;
            if (truncation?.truncated || fullOutputPath) {
                const warnings = [];
                if (fullOutputPath) {
                    warnings.push(`Full output: ${fullOutputPath}`);
                }
                if (truncation?.truncated) {
                    if (truncation.truncatedBy === "lines") {
                        warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
                    }
                    else {
                        warnings.push(`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`);
                    }
                }
                this.contentBox.addChild(new Text(`\n${theme.fg("warning", `[${warnings.join(". ")}]`)}`, 0, 0));
            }
        }
        const bashDurationMs = this.getBashDurationMs();
        if (bashDurationMs !== undefined) {
            const label = this.isPartial ? "Elapsed" : "Took";
            this.contentBox.addChild(new Text(`\n${theme.fg("muted", `${label} ${this.formatDuration(bashDurationMs)}`)}`, 0, 0));
        }
    }
    getTextOutput() {
        if (!this.result)
            return "";
        const textBlocks = this.result.content?.filter((c) => c.type === "text") || [];
        const imageBlocks = this.result.content?.filter((c) => c.type === "image") || [];
        let output = textBlocks
            .map((c) => {
            // Use sanitizeBinaryOutput to handle binary data that crashes string-width
            return sanitizeBinaryOutput(stripAnsi(c.text || "")).replace(/\r/g, "");
        })
            .join("\n");
        const caps = getCapabilities();
        if (imageBlocks.length > 0 && (!caps.images || !this.showImages)) {
            const imageIndicators = imageBlocks
                .map((img) => {
                const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
                return imageFallback(img.mimeType, dims);
            })
                .join("\n");
            output = output ? `${output}\n${imageIndicators}` : imageIndicators;
        }
        return output;
    }
    formatToolExecution() {
        let text = "";
        const invalidArg = theme.fg("error", "[invalid arg]");
        if (this.toolName === "read") {
            const rawPath = str(this.args?.file_path ?? this.args?.path);
            const path = rawPath !== null ? shortenPath(rawPath) : null;
            const offset = this.args?.offset;
            const limit = this.args?.limit;
            let pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
            if (offset !== undefined || limit !== undefined) {
                const startLine = offset ?? 1;
                const endLine = limit !== undefined ? startLine + limit - 1 : "";
                pathDisplay += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
            }
            text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`;
            if (this.result) {
                const output = this.getTextOutput();
                const rawPath = str(this.args?.file_path ?? this.args?.path);
                const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
                const lines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
                const maxLines = this.expanded ? lines.length : 10;
                const displayLines = lines.slice(0, maxLines);
                const remaining = lines.length - maxLines;
                text +=
                    "\n\n" +
                        displayLines
                            .map((line) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
                            .join("\n");
                if (remaining > 0) {
                    text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
                }
                const truncation = this.result.details?.truncation;
                if (truncation?.truncated) {
                    if (truncation.firstLineExceedsLimit) {
                        text +=
                            "\n" +
                                theme.fg("warning", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`);
                    }
                    else if (truncation.truncatedBy === "lines") {
                        text +=
                            "\n" +
                                theme.fg("warning", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`);
                    }
                    else {
                        text +=
                            "\n" +
                                theme.fg("warning", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`);
                    }
                }
            }
        }
        else if (this.toolName === "write") {
            const rawPath = str(this.args?.file_path ?? this.args?.path);
            const fileContent = str(this.args?.content);
            const path = rawPath !== null ? shortenPath(rawPath) : null;
            text =
                theme.fg("toolTitle", theme.bold("write")) +
                    " " +
                    (path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."));
            if (fileContent === null) {
                text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`;
            }
            else if (fileContent) {
                const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
                let lines;
                if (lang) {
                    const cache = this.writeHighlightCache;
                    if (cache && cache.lang === lang && cache.rawPath === rawPath && cache.rawContent === fileContent) {
                        lines = cache.highlightedLines;
                    }
                    else {
                        const displayContent = normalizeDisplayText(fileContent);
                        const normalized = replaceTabs(displayContent);
                        lines = highlightCode(normalized, lang);
                        this.writeHighlightCache = {
                            rawPath,
                            lang,
                            rawContent: fileContent,
                            normalizedLines: normalized.split("\n"),
                            highlightedLines: lines,
                        };
                    }
                }
                else {
                    lines = normalizeDisplayText(fileContent).split("\n");
                    this.writeHighlightCache = undefined;
                }
                const totalLines = lines.length;
                const maxLines = this.expanded ? lines.length : 10;
                const displayLines = lines.slice(0, maxLines);
                const remaining = lines.length - maxLines;
                text +=
                    "\n\n" +
                        displayLines.map((line) => (lang ? line : theme.fg("toolOutput", replaceTabs(line)))).join("\n");
                if (remaining > 0) {
                    text +=
                        theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total,`) +
                            ` ${keyHint("app.tools.expand", "to expand")})`;
                }
            }
            // Show error if tool execution failed
            if (this.result?.isError) {
                const errorText = this.getTextOutput();
                if (errorText) {
                    text += `\n\n${theme.fg("error", errorText)}`;
                }
            }
        }
        else if (this.toolName === "edit") {
            const rawPath = str(this.args?.file_path ?? this.args?.path);
            const path = rawPath !== null ? shortenPath(rawPath) : null;
            // Build path display, appending :line if we have diff info
            let pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
            const firstChangedLine = (this.editDiffPreview && "firstChangedLine" in this.editDiffPreview
                ? this.editDiffPreview.firstChangedLine
                : undefined) ||
                (this.result && !this.result.isError ? this.result.details?.firstChangedLine : undefined);
            if (firstChangedLine) {
                pathDisplay += theme.fg("warning", `:${firstChangedLine}`);
            }
            text = `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
            if (this.result?.isError) {
                // Show error from result
                const errorText = this.getTextOutput();
                if (errorText) {
                    text += `\n\n${theme.fg("error", errorText)}`;
                }
            }
            else if (this.result?.details?.diff) {
                // Tool executed successfully - use the diff from result
                // This takes priority over editDiffPreview which may have a stale error
                // due to race condition (async preview computed after file was modified)
                text += `\n\n${renderDiff(this.result.details.diff, { filePath: rawPath ?? undefined })}`;
            }
            else if (this.editDiffPreview) {
                // Use cached diff preview (before tool executes)
                if ("error" in this.editDiffPreview) {
                    text += `\n\n${theme.fg("error", this.editDiffPreview.error)}`;
                }
                else if (this.editDiffPreview.diff) {
                    text += `\n\n${renderDiff(this.editDiffPreview.diff, { filePath: rawPath ?? undefined })}`;
                }
            }
        }
        else if (this.toolName === "ls") {
            const rawPath = str(this.args?.path);
            const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
            const limit = this.args?.limit;
            text = `${theme.fg("toolTitle", theme.bold("ls"))} ${path === null ? invalidArg : theme.fg("accent", path)}`;
            if (limit !== undefined) {
                text += theme.fg("toolOutput", ` (limit ${limit})`);
            }
            if (this.result) {
                const output = this.getTextOutput().trim();
                if (output) {
                    const lines = output.split("\n");
                    const maxLines = this.expanded ? lines.length : 20;
                    const displayLines = lines.slice(0, maxLines);
                    const remaining = lines.length - maxLines;
                    text += `\n\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
                    if (remaining > 0) {
                        text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
                    }
                }
                const entryLimit = this.result.details?.entryLimitReached;
                const truncation = this.result.details?.truncation;
                if (entryLimit || truncation?.truncated) {
                    const warnings = [];
                    if (entryLimit) {
                        warnings.push(`${entryLimit} entries limit`);
                    }
                    if (truncation?.truncated) {
                        warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
                    }
                    text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
                }
            }
        }
        else if (this.toolName === "find") {
            const pattern = str(this.args?.pattern);
            const rawPath = str(this.args?.path);
            const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
            const limit = this.args?.limit;
            text =
                theme.fg("toolTitle", theme.bold("find")) +
                    " " +
                    (pattern === null ? invalidArg : theme.fg("accent", pattern || "")) +
                    theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
            if (limit !== undefined) {
                text += theme.fg("toolOutput", ` (limit ${limit})`);
            }
            if (this.result) {
                const output = this.getTextOutput().trim();
                if (output) {
                    const lines = output.split("\n");
                    const maxLines = this.expanded ? lines.length : 20;
                    const displayLines = lines.slice(0, maxLines);
                    const remaining = lines.length - maxLines;
                    text += `\n\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
                    if (remaining > 0) {
                        text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
                    }
                }
                const resultLimit = this.result.details?.resultLimitReached;
                const truncation = this.result.details?.truncation;
                if (resultLimit || truncation?.truncated) {
                    const warnings = [];
                    if (resultLimit) {
                        warnings.push(`${resultLimit} results limit`);
                    }
                    if (truncation?.truncated) {
                        warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
                    }
                    text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
                }
            }
        }
        else if (this.toolName === "grep") {
            const pattern = str(this.args?.pattern);
            const rawPath = str(this.args?.path);
            const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
            const glob = str(this.args?.glob);
            const limit = this.args?.limit;
            text =
                theme.fg("toolTitle", theme.bold("grep")) +
                    " " +
                    (pattern === null ? invalidArg : theme.fg("accent", `/${pattern || ""}/`)) +
                    theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
            if (glob) {
                text += theme.fg("toolOutput", ` (${glob})`);
            }
            if (limit !== undefined) {
                text += theme.fg("toolOutput", ` limit ${limit}`);
            }
            if (this.result) {
                const output = this.getTextOutput().trim();
                if (output) {
                    const lines = output.split("\n");
                    const maxLines = this.expanded ? lines.length : 15;
                    const displayLines = lines.slice(0, maxLines);
                    const remaining = lines.length - maxLines;
                    text += `\n\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
                    if (remaining > 0) {
                        text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
                    }
                }
                const matchLimit = this.result.details?.matchLimitReached;
                const truncation = this.result.details?.truncation;
                const linesTruncated = this.result.details?.linesTruncated;
                if (matchLimit || truncation?.truncated || linesTruncated) {
                    const warnings = [];
                    if (matchLimit) {
                        warnings.push(`${matchLimit} matches limit`);
                    }
                    if (truncation?.truncated) {
                        warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
                    }
                    if (linesTruncated) {
                        warnings.push("some lines truncated");
                    }
                    text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
                }
            }
        }
        else {
            // Generic tool (shouldn't reach here for custom tools)
            text = theme.fg("toolTitle", theme.bold(this.toolName));
            const content = JSON.stringify(this.args, null, 2);
            text += `\n\n${content}`;
            const output = this.getTextOutput();
            if (output) {
                text += `\n${output}`;
            }
        }
        return text;
    }
}
//# sourceMappingURL=tool-execution.js.map