"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
// Renders a normalized ToolResultView (from @stigmer/sdk) with a semantic view
// per result type — diffs, terminals, search lists, file content, MCP content
// blocks — instead of a raw JSON dump. The json/text variants are the graceful
// fallbacks, so unknown tools are still readable.
import { useMemo, useState } from "react";
import { cn } from "@stigmer/theme";
import { computeDiff } from "../version-history/computeDiff.js";
import { DiffViewer } from "../version-history/DiffViewer.js";
import { UnifiedDiffView } from "../version-history/UnifiedDiffView.js";
import { CollapsibleCode, CollapsiblePre, formatJson } from "./tool-rendering-primitives.js";
import { FilePathLink } from "./FilePathLink.js";
import { EmptyChangeNotice } from "./EmptyChangeNotice.js";
import { TerminalSession } from "./TerminalSession.js";
import { execIdFromStorageKey } from "./useFileChangeContent.js";
import { useArtifactDownloadUrl } from "./useArtifactDownloadUrl.js";
import { useArtifactDownload } from "./useArtifactDownload.js";
import { useToolOutputContent } from "./useToolOutputContent.js";
/**
 * Renders a {@link ToolResultView} with a type-appropriate treatment.
 *
 * This is the presentation half of the tool-result pipeline: `@stigmer/sdk`'s
 * `normalizeToolResult` turns an opaque result string into a typed view, and
 * this component renders it. Importable on its own by platform builders who
 * compose custom tool UIs.
 */
export function ResultView({ view, showFileName = true, showStats = true, className }) {
    switch (view.type) {
        case "diff":
            return _jsx(DiffResultView, { view: view, showFileName: showFileName, showStats: showStats, className: className });
        case "terminal":
            return _jsx(TerminalResultView, { view: view, className: className });
        case "search":
            return _jsx(SearchResultView, { view: view, showCount: showStats, className: className });
        case "list":
            return _jsx(ListResultView, { entries: view.entries, count: view.count, showCount: showStats, className: className });
        case "file":
            return _jsx(FileResultView, { view: view, showFileName: showFileName, className: className });
        case "contentBlocks":
            return _jsx(ContentBlocksResultView, { blocks: view.blocks, className: className });
        case "outputRef":
            return _jsx(OutputRefResultView, { view: view, className: className });
        case "text":
            return view.text ? _jsx(CollapsiblePre, { content: view.text, className: cn("text-foreground", className) }) : null;
        case "json":
            return _jsx(CollapsibleCode, { label: "Result", content: formatJson(view.value), className: className });
        case "error":
            return _jsx(ErrorResultView, { message: view.message, className: className });
        case "empty":
            return null;
    }
}
/**
 * Returns a compact one-line summary fragment for a result view, suitable for a
 * collapsed tool row (e.g. "+40 -0", "exit 1", "2 matches"). Returns null when
 * there is nothing useful to summarize.
 */
export function summarizeResultView(view) {
    switch (view.type) {
        case "diff": {
            const stats = diffStats(view);
            return stats ? `+${stats.added} -${stats.removed}` : null;
        }
        case "terminal":
            return view.exitCode !== undefined && view.exitCode !== 0 ? `exit ${view.exitCode}` : null;
        case "search":
            return searchCountLabel(view.count, view.kind);
        case "list":
            return `${view.count} ${view.count === 1 ? "item" : "items"}`;
        default:
            return null;
    }
}
function countHunks(hunks) {
    let added = 0;
    let removed = 0;
    for (const hunk of hunks) {
        for (const line of hunk.lines) {
            if (line.type === "added")
                added++;
            else if (line.type === "removed")
                removed++;
        }
    }
    return { added, removed };
}
// Used by the collapsed-row summary, where no precomputed hunks are available.
// Prefers the engine-provided counts (Cursor envelope) and only runs the diff
// algorithm for the native harness, which provides none.
function diffStats(view) {
    if (view.linesAdded !== undefined || view.linesRemoved !== undefined) {
        return { added: view.linesAdded ?? 0, removed: view.linesRemoved ?? 0 };
    }
    if (view.oldText !== undefined && view.newText !== undefined) {
        return countHunks(computeDiff(view.oldText, view.newText));
    }
    return null;
}
function DiffResultView({ view, showFileName = true, showStats = true, className, }) {
    // The native engine returns no diff, so we compute hunks from args (old/new).
    // The Cursor envelope provides a ready unified-diff string we render directly.
    const hunks = useMemo(() => {
        if (view.oldText !== undefined && view.newText !== undefined) {
            return computeDiff(view.oldText, view.newText);
        }
        return [];
    }, [view.oldText, view.newText]);
    // Derive stats from the already-computed hunks instead of re-running the diff.
    const stats = useMemo(() => {
        if (view.linesAdded !== undefined || view.linesRemoved !== undefined) {
            return { added: view.linesAdded ?? 0, removed: view.linesRemoved ?? 0 };
        }
        return hunks.length > 0 ? countHunks(hunks) : null;
    }, [view.linesAdded, view.linesRemoved, hunks]);
    const showPath = showFileName && Boolean(view.path);
    const showStatsRow = showStats && stats != null;
    // One diff family across every surface: computed hunks render through the
    // canonical, accessible DiffViewer (line numbers + --stgm-diff-* tokens); a
    // ready hunk-only patch (Cursor envelope) is parsed into the same DiffViewer
    // table by UnifiedDiffView (raw fallback only if unparseable). The header
    // (path + ± stats) is this view's own, so the DiffViewer is used without its
    // filePath header to avoid a duplicate path.
    return (_jsxs("div", { className: cn("space-y-1", className), "data-cursor-target": "file-diff", children: [(showPath || showStatsRow) && (_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [showPath && (_jsx(FilePathLink, { path: view.path, dirDisplay: "dim", className: "text-xs" })), showStatsRow && stats && (_jsxs("span", { className: "shrink-0 tabular-nums text-xs", children: [_jsxs("span", { className: "text-diff-added-fg", children: ["+", stats.added] }), " ", _jsxs("span", { className: "text-diff-removed-fg", children: ["-", stats.removed] })] }))] })), hunks.length > 0 ? (_jsx(DiffViewer, { hunks: hunks })) : view.unifiedDiff ? (_jsx(UnifiedDiffView, { patch: view.unifiedDiff })) : (_jsx(EmptyChangeNotice, { kind: "no-preview" }))] }));
}
// A shell result IS a terminal session: the command prompt line plus its
// output, rendered as one block by the shared TerminalSession (no separate
// "Output" caption or command box).
function TerminalResultView({ view, className }) {
    return (_jsx(TerminalSession, { command: view.command, stdout: view.stdout, stderr: view.stderr, exitCode: view.exitCode, className: className }));
}
/** "N files" / "N matches" — the noun follows the search subtype. */
function searchCountLabel(count, kind) {
    if (kind === "files") {
        return `${count} ${count === 1 ? "file" : "files"}`;
    }
    return `${count} ${count === 1 ? "match" : "matches"}`;
}
/**
 * Renders a search result with a subtype-aware treatment.
 *
 * - **File-name search** (`kind: "files"` — glob / file_search): the matches are
 *   paths, rendered as a clickable {@link FilePathLink} list (filename-first,
 *   full path on hover, opens on GitHub or copies locally).
 * - **Content search** (`kind: "content"` — grep): the matches are lines,
 *   grouped under their file with `line: text` rows; matches with no file
 *   association (the native grep shape) render as a flat monospace list.
 *
 * The count header uses the engine's authoritative `count` (which can exceed the
 * returned matches when `truncated`), and a "showing first N" note makes a
 * capped result honest instead of silently partial. Empty results name the
 * subtype ("No files found" vs "No matches") — the query lives in the owning row
 * header, so it is not restated here.
 */
function SearchResultView({ view, showCount = true, className, }) {
    const { matches, count, kind, truncated } = view;
    if (count === 0 && matches.length === 0) {
        return (_jsx("p", { className: cn("text-xs text-muted-foreground", className), children: kind === "files" ? "No files found" : "No matches" }));
    }
    // The owning row header already shows the count (summarizeResultView), so when
    // suppressed the body shows only the truncation note — the one fact the header
    // does not carry. When standalone (showCount), it leads with the full count.
    const header = showCount
        ? `${searchCountLabel(count, kind)}${truncated ? ` \u00b7 showing first ${matches.length}` : ""}`
        : truncated
            ? `Showing first ${matches.length} of ${count}`
            : null;
    return (_jsxs("div", { className: cn("space-y-1", className), children: [header && _jsx("span", { className: "font-medium text-muted-foreground", children: header }), kind === "files" ? (_jsx(SearchFileList, { matches: matches })) : (_jsx(SearchContentMatches, { matches: matches }))] }));
}
/** Bounded, scrollable shell shared by both search result treatments. */
function SearchResultBox({ children }) {
    return (_jsx("div", { className: "max-h-80 overflow-auto rounded-md border border-border bg-muted-subtle text-xs", children: children }));
}
/** A clickable list of file-name search hits. */
function SearchFileList({ matches }) {
    return (_jsx(SearchResultBox, { children: matches.map((m, i) => (_jsx("div", { className: "border-b border-border-muted px-2 py-0.5 last:border-b-0", children: _jsx(FilePathLink, { path: m.file ?? m.text, dirDisplay: "dim", className: "text-xs" }) }, i))) }));
}
/** Content (grep) matches, grouped under their file when one is provided. */
function SearchContentMatches({ matches }) {
    const groups = useMemo(() => groupMatchesByFile(matches), [matches]);
    return (_jsx(SearchResultBox, { children: groups.map((group, gi) => (_jsxs("div", { className: "border-b border-border-muted last:border-b-0", children: [group.file && (_jsx("div", { className: "bg-muted-faint px-2 py-0.5", children: _jsx(FilePathLink, { path: group.file, dirDisplay: "dim", className: "text-xs" }) })), group.matches.map((m, i) => (_jsxs("div", { className: "whitespace-pre-wrap break-words px-2 py-0.5 font-mono text-foreground", children: [m.line !== undefined && (_jsxs("span", { className: "select-none text-muted-foreground-subtle", children: [m.line, ": "] })), m.text] }, i)))] }, gi))) }));
}
/**
 * Groups consecutive matches that share a file into one group, preserving order.
 * Matches with no file association fall into their own file-less group so the
 * native grep shape (line text only) still renders as a flat list.
 */
function groupMatchesByFile(matches) {
    const groups = [];
    for (const m of matches) {
        const last = groups[groups.length - 1];
        if (last && last.file === m.file) {
            last.matches.push(m);
        }
        else {
            groups.push({ file: m.file, matches: [m] });
        }
    }
    return groups;
}
function ListResultView({ entries, count, showCount = true, className, }) {
    if (count === 0) {
        return _jsx("p", { className: cn("text-xs text-muted-foreground", className), children: "Empty" });
    }
    return (_jsxs("div", { className: cn("space-y-1", className), children: [showCount && (_jsxs("span", { className: "font-medium text-muted-foreground", children: [count, " ", count === 1 ? "item" : "items"] })), _jsx("div", { className: "max-h-80 overflow-auto rounded-md border border-border bg-muted-subtle font-mono text-xs", children: entries.map((e, i) => (_jsx("div", { className: "truncate px-2 py-0.5 text-foreground", children: e }, i))) })] }));
}
function FileResultView({ view, showFileName = true, className, }) {
    if (!view.content) {
        // No body. When the filename is shown elsewhere (showFileName=false), an
        // empty write fallback degrades honestly to a neutral notice rather than a
        // bare, redundant path; otherwise the path itself is the information (a read).
        if (showFileName && view.path) {
            return (_jsx("div", { className: cn("flex items-center gap-1.5 text-xs", className), children: _jsx(FilePathLink, { path: view.path, className: "text-xs" }) }));
        }
        return showFileName ? null : (_jsx(EmptyChangeNotice, { kind: "no-preview", className: className }));
    }
    return (_jsxs("div", { className: cn("space-y-1", className), children: [showFileName && view.path && (_jsx(FilePathLink, { path: view.path, className: "text-xs" })), _jsx(CollapsibleCode, { label: view.truncated ? "Content (truncated)" : "Content", content: view.content })] }));
}
function ContentBlocksResultView({ blocks, className, }) {
    const text = useMemo(() => blocks
        .map((b) => (b.type === "text" && b.text ? b.text : `[${b.type}]`))
        .join("\n"), [blocks]);
    return _jsx(CollapsiblePre, { content: text, className: cn("text-foreground", className) });
}
function formatBytes(n) {
    if (n >= 1024 * 1024)
        return `${(n / (1024 * 1024)).toFixed(1)} MB`;
    if (n >= 1024)
        return `${(n / 1024).toFixed(1)} KB`;
    return `${n} B`;
}
/**
 * Renders a tool result whose bytes were offloaded to artifact storage. The
 * bytes are resolved on demand from the stable `storageKey` at view time — the
 * URL once baked into the persisted status expired after an hour, so it is no
 * longer trusted. Images (e.g. computer-use screenshots) render inline via a
 * freshly minted URL; other large output expands its full text in-app, with a
 * download fallback when the server truncates it.
 */
function OutputRefResultView({ view, className }) {
    if (view.isImage) {
        return _jsx(OutputRefImage, { storageKey: view.storageKey, className: className });
    }
    return _jsx(OutputRefText, { view: view, className: className });
}
/** Offloaded image output, rendered from an always-fresh presigned URL. */
function OutputRefImage({ storageKey, className }) {
    const executionId = useMemo(() => execIdFromStorageKey(storageKey), [storageKey]);
    const { url, error } = useArtifactDownloadUrl(executionId, storageKey);
    if (error) {
        return _jsx("p", { className: cn("text-xs text-destructive", className), children: "Couldn't load image output." });
    }
    if (!url) {
        return (_jsx("div", { className: cn("h-40 w-64 animate-pulse rounded-md border border-border bg-muted", className), "aria-busy": "true", "aria-label": "Loading image output" }));
    }
    return (_jsx("div", { className: cn("space-y-1", className), children: _jsx("a", { href: url, target: "_blank", rel: "noreferrer", className: "inline-block focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx("img", { src: url, alt: "Tool output screenshot", loading: "lazy", className: "max-h-96 w-auto rounded-md border border-border" }) }) }));
}
/**
 * Offloaded text output. Shows a preview head with a "View full output" toggle
 * that lazily fetches the full content in-app (CORS-safe). If the server
 * truncates it, offers a full download via a freshly minted URL.
 */
function OutputRefText({ view, className }) {
    const [expanded, setExpanded] = useState(false);
    const executionId = useMemo(() => execIdFromStorageKey(view.storageKey), [view.storageKey]);
    const { content, isLoading, isTruncated, error } = useToolOutputContent({ storageKey: view.storageKey, contentHash: view.contentHash }, expanded);
    const { download, isDownloading } = useArtifactDownload(executionId);
    const sizeSuffix = view.sizeBytes ? ` (${formatBytes(view.sizeBytes)})` : "";
    return (_jsx("div", { className: cn("space-y-1", className), children: !expanded ? (_jsxs(_Fragment, { children: [view.preview && (_jsx(CollapsiblePre, { content: view.preview, className: "text-foreground" })), _jsxs("button", { type: "button", onClick: () => setExpanded(true), className: "inline-block text-xs font-medium text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: ["View full output", sizeSuffix] })] })) : isLoading ? (_jsx("p", { className: "text-xs text-muted-foreground", children: "Loading full output\u2026" })) : error ? (_jsx("p", { className: "text-xs text-destructive", children: "Couldn't load full output. Try again." })) : content !== null ? (_jsxs("div", { className: "space-y-1", children: [_jsx(CollapsiblePre, { content: content, className: "text-foreground" }), isTruncated && (_jsx("button", { type: "button", onClick: () => download(view.storageKey), disabled: isDownloading, className: "inline-block text-xs font-medium text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50", children: isDownloading ? "Preparing download…" : `Output truncated — download full file${sizeSuffix}` }))] })) : null }));
}
function ErrorResultView({ message, className }) {
    return (_jsxs("div", { className: cn("space-y-1", className), children: [_jsx("span", { className: "font-medium text-destructive", children: "Error" }), _jsx("pre", { className: "whitespace-pre-wrap break-words rounded-md border border-destructive/20 bg-destructive-subtle p-2 font-mono text-destructive", children: message })] }));
}
//# sourceMappingURL=ResultView.js.map