198 lines
5.8 KiB
TypeScript
198 lines
5.8 KiB
TypeScript
import { redactToolDetail } from "../logging/redact.js";
|
|
import { shortenHomeInString } from "../utils.js";
|
|
import TOOL_DISPLAY_JSON from "./tool-display.json" with { type: "json" };
|
|
|
|
type ToolDisplayActionSpec = {
|
|
label?: string;
|
|
detailKeys?: string[];
|
|
};
|
|
|
|
type ToolDisplaySpec = {
|
|
emoji?: string;
|
|
title?: string;
|
|
label?: string;
|
|
detailKeys?: string[];
|
|
actions?: Record<string, ToolDisplayActionSpec>;
|
|
};
|
|
|
|
type ToolDisplayConfig = {
|
|
version?: number;
|
|
fallback?: ToolDisplaySpec;
|
|
tools?: Record<string, ToolDisplaySpec>;
|
|
};
|
|
|
|
export type ToolDisplay = {
|
|
name: string;
|
|
emoji: string;
|
|
title: string;
|
|
label: string;
|
|
verb?: string;
|
|
detail?: string;
|
|
};
|
|
|
|
const TOOL_DISPLAY_CONFIG = TOOL_DISPLAY_JSON as ToolDisplayConfig;
|
|
const FALLBACK = TOOL_DISPLAY_CONFIG.fallback ?? { emoji: "🧩" };
|
|
const TOOL_MAP = TOOL_DISPLAY_CONFIG.tools ?? {};
|
|
|
|
function normalizeToolName(name?: string): string {
|
|
return (name ?? "tool").trim();
|
|
}
|
|
|
|
function defaultTitle(name: string): string {
|
|
const cleaned = name.replace(/_/g, " ").trim();
|
|
if (!cleaned) return "Tool";
|
|
return cleaned
|
|
.split(/\s+/)
|
|
.map((part) =>
|
|
part.length <= 2 && part.toUpperCase() === part
|
|
? part
|
|
: `${part.at(0)?.toUpperCase() ?? ""}${part.slice(1)}`,
|
|
)
|
|
.join(" ");
|
|
}
|
|
|
|
function normalizeVerb(value?: string): string | undefined {
|
|
const trimmed = value?.trim();
|
|
if (!trimmed) return undefined;
|
|
return trimmed.replace(/_/g, " ");
|
|
}
|
|
|
|
function coerceDisplayValue(value: unknown): string | undefined {
|
|
if (value === null || value === undefined) return undefined;
|
|
if (typeof value === "string") {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return undefined;
|
|
const firstLine = trimmed.split(/\r?\n/)[0]?.trim() ?? "";
|
|
if (!firstLine) return undefined;
|
|
return firstLine.length > 160 ? `${firstLine.slice(0, 157)}…` : firstLine;
|
|
}
|
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
return String(value);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
const values = value
|
|
.map((item) => coerceDisplayValue(item))
|
|
.filter((item): item is string => Boolean(item));
|
|
if (values.length === 0) return undefined;
|
|
const preview = values.slice(0, 3).join(", ");
|
|
return values.length > 3 ? `${preview}…` : preview;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function lookupValueByPath(args: unknown, path: string): unknown {
|
|
if (!args || typeof args !== "object") return undefined;
|
|
let current: unknown = args;
|
|
for (const segment of path.split(".")) {
|
|
if (!segment) return undefined;
|
|
if (!current || typeof current !== "object") return undefined;
|
|
const record = current as Record<string, unknown>;
|
|
current = record[segment];
|
|
}
|
|
return current;
|
|
}
|
|
|
|
function resolveDetailFromKeys(
|
|
args: unknown,
|
|
keys: string[],
|
|
): string | undefined {
|
|
for (const key of keys) {
|
|
const value = lookupValueByPath(args, key);
|
|
const display = coerceDisplayValue(value);
|
|
if (display) return display;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function resolveReadDetail(args: unknown): string | undefined {
|
|
if (!args || typeof args !== "object") return undefined;
|
|
const record = args as Record<string, unknown>;
|
|
const path = typeof record.path === "string" ? record.path : undefined;
|
|
if (!path) return undefined;
|
|
const offset = typeof record.offset === "number" ? record.offset : undefined;
|
|
const limit = typeof record.limit === "number" ? record.limit : undefined;
|
|
if (offset !== undefined && limit !== undefined) {
|
|
return `${path}:${offset}-${offset + limit}`;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
function resolveWriteDetail(args: unknown): string | undefined {
|
|
if (!args || typeof args !== "object") return undefined;
|
|
const record = args as Record<string, unknown>;
|
|
const path = typeof record.path === "string" ? record.path : undefined;
|
|
return path;
|
|
}
|
|
|
|
function resolveActionSpec(
|
|
spec: ToolDisplaySpec | undefined,
|
|
action: string | undefined,
|
|
): ToolDisplayActionSpec | undefined {
|
|
if (!spec || !action) return undefined;
|
|
return spec.actions?.[action] ?? undefined;
|
|
}
|
|
|
|
export function resolveToolDisplay(params: {
|
|
name?: string;
|
|
args?: unknown;
|
|
meta?: string;
|
|
}): ToolDisplay {
|
|
const name = normalizeToolName(params.name);
|
|
const key = name.toLowerCase();
|
|
const spec = TOOL_MAP[key];
|
|
const emoji = spec?.emoji ?? FALLBACK.emoji ?? "🧩";
|
|
const title = spec?.title ?? defaultTitle(name);
|
|
const label = spec?.label ?? name;
|
|
const actionRaw =
|
|
params.args && typeof params.args === "object"
|
|
? ((params.args as Record<string, unknown>).action as string | undefined)
|
|
: undefined;
|
|
const action = typeof actionRaw === "string" ? actionRaw.trim() : undefined;
|
|
const actionSpec = resolveActionSpec(spec, action);
|
|
const verb = normalizeVerb(actionSpec?.label ?? action);
|
|
|
|
let detail: string | undefined;
|
|
if (key === "read") detail = resolveReadDetail(params.args);
|
|
if (!detail && (key === "write" || key === "edit" || key === "attach")) {
|
|
detail = resolveWriteDetail(params.args);
|
|
}
|
|
|
|
const detailKeys =
|
|
actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
|
if (!detail && detailKeys.length > 0) {
|
|
detail = resolveDetailFromKeys(params.args, detailKeys);
|
|
}
|
|
|
|
if (!detail && params.meta) {
|
|
detail = params.meta;
|
|
}
|
|
|
|
if (detail) {
|
|
detail = shortenHomeInString(detail);
|
|
}
|
|
|
|
return {
|
|
name,
|
|
emoji,
|
|
title,
|
|
label,
|
|
verb,
|
|
detail,
|
|
};
|
|
}
|
|
|
|
export function formatToolDetail(display: ToolDisplay): string | undefined {
|
|
const parts: string[] = [];
|
|
if (display.verb) parts.push(display.verb);
|
|
if (display.detail) parts.push(redactToolDetail(display.detail));
|
|
if (parts.length === 0) return undefined;
|
|
return parts.join(" · ");
|
|
}
|
|
|
|
export function formatToolSummary(display: ToolDisplay): string {
|
|
const detail = formatToolDetail(display);
|
|
return detail
|
|
? `${display.emoji} ${display.label}: ${detail}`
|
|
: `${display.emoji} ${display.label}`;
|
|
}
|