import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
import { ToolKind, resolveToolKind, resolveToolKindByName } from "@stigmer/sdk";
/**
 * Categories whose result is foregrounded as a persistent bounded preview by
 * default — those for which the compact one-liner cannot convey what happened,
 * so the output itself carries the meaning. All other categories default to
 * `"summary"`.
 *
 * File edits and writes are previewed: a `+N -M` summary says how much changed
 * but not *what* — and the change is the decision-relevant content. Surfacing a
 * bounded diff inline (Cursor-style) is the point of a code agent. The body is
 * height-clamped with a single in-place reveal for the rest, so density stays
 * under control even in turns with many edits.
 */
const PREVIEW_CATEGORIES = new Set([
    "shell",
    "edit",
    "write",
    "mcp",
    "web-search",
    "fetch",
    "unknown",
]);
/**
 * Low-signal categories whose consecutive runs fold into a single collapsible
 * "Read 5 files" chip in the thread. These are the read-only, repetitive
 * operations whose individual rows are noise in bulk; everything else renders
 * as a persistent row. This is the *run-grouping* axis, orthogonal to
 * {@link ToolDisclosure} — see {@link isRunGroupableCategory}.
 */
const RUN_GROUPABLE_CATEGORIES = new Set([
    "read",
    "list",
    "search",
]);
/**
 * Returns the default inline {@link ToolDisclosure} for a presentation
 * category. This is the headless policy behind the thread's "keep a live
 * preview for shell/MCP/unknown, keep generic tools compact" behaviour;
 * consumers can override per {@link ToolKind} via {@link registerToolPresenter}.
 */
export function defaultDisclosureForCategory(category) {
    return PREVIEW_CATEGORIES.has(category) ? "preview" : "summary";
}
/**
 * Returns whether a presentation category participates in run-grouping — i.e.
 * whether a maximal run of consecutive calls of this category folds into one
 * collapsible chip. See {@link RUN_GROUPABLE_CATEGORIES} and `segmentToolCalls`.
 */
export function isRunGroupableCategory(category) {
    return RUN_GROUPABLE_CATEGORIES.has(category);
}
/**
 * Returns whether a category's primary argument is a file path — the file
 * tools (read / write / edit / delete). These render their path filename-first
 * via {@link FilePathLink} (full path on hover) in headers and previews, rather
 * than as a raw, often-absolute string.
 */
export function isFileCategory(category) {
    return (category === "read" ||
        category === "write" ||
        category === "edit" ||
        category === "delete");
}
// Presentation metadata per ToolKind. Classification (name -> kind) is owned by
// @stigmer/sdk; this table only describes how each kind is shown. MCP and
// unknown are resolved dynamically (label derived from the tool name).
const KIND_DISPLAY = {
    [ToolKind.FILE_READ]: { category: "read", label: "Read", primaryField: "path", fallbackFields: ["file_path", "file"] },
    [ToolKind.FILE_WRITE]: { category: "write", label: "Write", primaryField: "path", fallbackFields: ["file_path", "file", "filename"] },
    [ToolKind.FILE_EDIT]: { category: "edit", label: "Edit", primaryField: "path", fallbackFields: ["file_path", "file", "filename"] },
    [ToolKind.FILE_DELETE]: { category: "delete", label: "Delete", primaryField: "path", fallbackFields: ["file_path", "file", "filename"] },
    [ToolKind.SHELL]: { category: "shell", label: "Shell", primaryField: "command" },
    [ToolKind.SEARCH]: { category: "search", label: "Search", primaryField: "pattern", fallbackFields: ["query", "q"] },
    [ToolKind.LIST]: { category: "list", label: "List", primaryField: "path" },
    [ToolKind.FETCH]: { category: "fetch", label: "Fetch", primaryField: "url", fallbackFields: ["uri"] },
    [ToolKind.WEB_SEARCH]: { category: "web-search", label: "Web Search", primaryField: "query", fallbackFields: ["q", "search_term"] },
    [ToolKind.THINK]: { category: "think", label: "Thinking", primaryField: "thought" },
    [ToolKind.SUBAGENT]: { category: "sub-agent", label: "Sub-agent", primaryField: "description", fallbackFields: ["prompt"] },
    [ToolKind.TODO]: { category: "internal", label: "Todos", primaryField: "todos" },
};
/**
 * Returns true if the tool is an internal state-management tool that should not
 * appear in the message thread (todos are rendered through a dedicated surface).
 */
export function isInternalTool(toolName) {
    return resolveToolKindByName(toolName) === ToolKind.TODO;
}
/**
 * Returns true for a tool call the runner collapsed in place — a superseded
 * same-turn duplicate of an approval gate.
 *
 * When the agent emits one resource twice in a turn and both attempts are denied,
 * the runner overlays the FIRST as the WAITING_APPROVAL gate and blanks the
 * redundant twin to a content-less SKIPPED row (it cannot drop it — the backend's
 * append-only-at-identity guard forbids removing a committed tool-call id). This
 * predicate recognizes that blanked twin so the thread renders one card, not two.
 *
 * The signature is precise: a SKIPPED row with no approval flag, no result, no
 * error, and no args preview. A genuinely user-skipped tool always retains the
 * preview/args the user reviewed before skipping, so it is never blank and is
 * never hidden by this.
 */
export function isCollapsedToolCall(toolCall) {
    return (toolCall.status === ToolCallStatus.TOOL_CALL_SKIPPED &&
        !toolCall.requiresApproval &&
        !toolCall.result &&
        !toolCall.error &&
        !toolCall.argsPreview);
}
/** Maps a {@link ToolKind} to its presentation metadata. */
export function toolKindToCategoryInfo(kind, toolName) {
    const entry = KIND_DISPLAY[kind];
    if (entry) {
        return {
            category: entry.category,
            label: entry.label,
            primaryArgField: entry.primaryField,
            fallbackArgFields: entry.fallbackFields ?? [],
        };
    }
    if (kind === ToolKind.MCP) {
        return {
            category: "mcp",
            label: humanizeToolName(toolName),
            primaryArgField: "slug",
            fallbackArgFields: ["name", "org"],
        };
    }
    return {
        category: "unknown",
        label: humanizeToolName(toolName),
        primaryArgField: "",
        fallbackArgFields: [],
    };
}
/**
 * Resolves a tool's presentation category from its name + MCP slug.
 *
 * Classification is delegated to `@stigmer/sdk`'s name-based resolver (the same
 * table the runner and Go CLI use), then mapped to presentation metadata. When
 * the caller has the full {@link ToolCall}, prefer {@link resolveToolCategoryFromCall},
 * which also honours the wire `tool_kind` for forward compatibility.
 */
export function resolveToolCategory(toolName, mcpServerSlug) {
    return toolKindToCategoryInfo(resolveToolKindByName(toolName, mcpServerSlug), toolName);
}
/**
 * Like {@link resolveToolCategory} but uses the wire `tool_kind` when present,
 * falling back to name-based resolution for legacy executions.
 */
export function resolveToolCategoryFromCall(toolCall) {
    return toolKindToCategoryInfo(resolveToolKind(toolCall), toolCall.name);
}
/**
 * Resolves presentation metadata from an explicit wire {@link ToolKind} plus the
 * tool name and MCP slug. Used by surfaces that carry a denormalized `tool_kind`
 * but not a full {@link ToolCall} — notably `PendingApproval`. Falls back to
 * name-based resolution when the kind is unset (legacy executions).
 */
export function resolveToolCategoryFromKind(toolKind, toolName, mcpServerSlug) {
    const kind = toolKind !== ToolKind.UNSPECIFIED
        ? toolKind
        : resolveToolKindByName(toolName, mcpServerSlug);
    return toolKindToCategoryInfo(kind, toolName);
}
/**
 * Converts a snake_case or camelCase tool name into a
 * human-readable title.
 *
 * @example
 * humanizeToolName("apply_mcp_server") // "Apply MCP Server"
 * humanizeToolName("deleteAgent")      // "Delete Agent"
 */
export function humanizeToolName(name) {
    return name
        .replace(/([a-z])([A-Z])/g, "$1 $2")
        .replace(/[_-]+/g, " ")
        .replace(/\b[a-z]/g, (c) => c.toUpperCase())
        .replace(/\bMcp\b/gi, "MCP")
        .replace(/\bApi\b/gi, "API")
        .replace(/\bId\b/gi, "ID")
        .replace(/\bUrl\b/gi, "URL")
        .replace(/\bIam\b/gi, "IAM");
}
function extractArgValue(args, primary, fallbacks) {
    if (!args)
        return null;
    const tryField = (field) => {
        const val = args[field];
        if (typeof val === "string" && val.length > 0)
            return val;
        return null;
    };
    if (primary) {
        const v = tryField(primary);
        if (v)
            return v;
    }
    for (const fb of fallbacks) {
        const v = tryField(fb);
        if (v)
            return v;
    }
    return null;
}
/**
 * Extracts the most relevant argument value from a tool call
 * based on its category (command for shell tools, path for file
 * tools, slug for MCP tools, etc.).
 *
 * Returns `null` when the tool has no recognised arguments.
 */
export function extractPrimaryArg(toolCall) {
    const info = resolveToolCategoryFromCall(toolCall);
    const result = extractArgValue(toolCall.args, info.primaryArgField, info.fallbackArgFields);
    if (result)
        return result;
    if ((info.category === "unknown" || info.category === "mcp") && toolCall.args) {
        const keys = Object.keys(toolCall.args);
        if (keys.length > 0) {
            const val = toolCall.args[keys[0]];
            if (typeof val === "string")
                return val;
        }
    }
    return null;
}
/**
 * Extracts the primary argument value from a JSON string
 * (typically `PendingApproval.argsPreview`). Returns `null` if
 * parsing fails or the expected field is not found.
 *
 * When the caller has the denormalized wire `tool_kind` (as `PendingApproval`
 * does), pass it so resolution honors the same kind that drives the label/icon —
 * a file tool whose NAME the classifier doesn't recognize still resolves to its
 * `path` field. This mirrors {@link extractPrimaryArg}, which uses the wire kind
 * via `resolveToolCategoryFromCall`. Falls back to name-based resolution when
 * the kind is unset (legacy executions).
 */
export function extractPrimaryArgFromPreview(toolName, argsPreview, mcpServerSlug, toolKind) {
    if (!argsPreview)
        return null;
    try {
        const parsed = JSON.parse(argsPreview);
        if (typeof parsed !== "object" || parsed === null)
            return null;
        const info = toolKind !== undefined
            ? resolveToolCategoryFromKind(toolKind, toolName, mcpServerSlug)
            : resolveToolCategory(toolName, mcpServerSlug);
        return extractArgValue(parsed, info.primaryArgField, info.fallbackArgFields);
    }
    catch {
        return null;
    }
}
const WRITE_CONTENT_FIELDS = [
    "contents",
    "content",
    "file_content",
    "new_text",
    "new_string",
    "replacement",
];
/**
 * Extracts the file content body from a JSON `argsPreview` string
 * for write/edit tool categories. Scans the same field names used
 * by the post-execution {@link ToolCallDetail} renderer so that
 * the approval preview matches the completed tool call display.
 *
 * Returns `null` if parsing fails or no content field is found.
 */
export function extractWriteContentFromPreview(argsPreview) {
    if (!argsPreview)
        return null;
    try {
        const parsed = JSON.parse(argsPreview);
        if (typeof parsed !== "object" || parsed === null)
            return null;
        for (const field of WRITE_CONTENT_FIELDS) {
            const val = parsed[field];
            if (typeof val === "string" && val.length > 0)
                return val;
        }
        return null;
    }
    catch {
        return null;
    }
}
//# sourceMappingURL=tool-categories.js.map