openclaw/ui/src/ui/tool-display.ts
2026-03-02 19:57:33 +00:00

160 lines
4.5 KiB
TypeScript

import SHARED_TOOL_DISPLAY_JSON from "../../../apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json" with { type: "json" };
import {
defaultTitle,
formatToolDetailText,
normalizeToolName,
resolveToolVerbAndDetailForArgs,
type ToolDisplaySpec as ToolDisplaySpecBase,
} from "../../../src/agents/tool-display-common.js";
import type { IconName } from "./icons.ts";
type ToolDisplaySpec = ToolDisplaySpecBase & {
icon?: string;
};
type SharedToolDisplaySpec = ToolDisplaySpecBase & {
emoji?: string;
};
type SharedToolDisplayConfig = {
version?: number;
fallback?: SharedToolDisplaySpec;
tools?: Record<string, SharedToolDisplaySpec>;
};
export type ToolDisplay = {
name: string;
icon: IconName;
title: string;
label: string;
verb?: string;
detail?: string;
};
const EMOJI_ICON_MAP: Record<string, IconName> = {
"🧩": "puzzle",
"🛠️": "wrench",
"🧰": "wrench",
"📖": "fileText",
"✍️": "edit",
"📝": "penLine",
"📎": "paperclip",
"🌐": "globe",
"📺": "monitor",
"🧾": "fileText",
"🔐": "settings",
"💻": "monitor",
"🔌": "plug",
"💬": "messageSquare",
};
const SLACK_SPEC: ToolDisplaySpec = {
icon: "messageSquare",
title: "Slack",
actions: {
react: { label: "react", detailKeys: ["channelId", "messageId", "emoji"] },
reactions: { label: "reactions", detailKeys: ["channelId", "messageId"] },
sendMessage: { label: "send", detailKeys: ["to", "content"] },
editMessage: { label: "edit", detailKeys: ["channelId", "messageId"] },
deleteMessage: { label: "delete", detailKeys: ["channelId", "messageId"] },
readMessages: { label: "read messages", detailKeys: ["channelId", "limit"] },
pinMessage: { label: "pin", detailKeys: ["channelId", "messageId"] },
unpinMessage: { label: "unpin", detailKeys: ["channelId", "messageId"] },
listPins: { label: "list pins", detailKeys: ["channelId"] },
memberInfo: { label: "member", detailKeys: ["userId"] },
emojiList: { label: "emoji list" },
},
};
function iconForEmoji(emoji?: string): IconName {
if (!emoji) {
return "puzzle";
}
return EMOJI_ICON_MAP[emoji] ?? "puzzle";
}
function convertSpec(spec?: SharedToolDisplaySpec): ToolDisplaySpec {
return {
icon: iconForEmoji(spec?.emoji),
title: spec?.title,
label: spec?.label,
detailKeys: spec?.detailKeys,
actions: spec?.actions,
};
}
const SHARED_TOOL_DISPLAY_CONFIG = SHARED_TOOL_DISPLAY_JSON as SharedToolDisplayConfig;
const FALLBACK = convertSpec(SHARED_TOOL_DISPLAY_CONFIG.fallback ?? { emoji: "🧩" });
const TOOL_MAP: Record<string, ToolDisplaySpec> = Object.fromEntries(
Object.entries(SHARED_TOOL_DISPLAY_CONFIG.tools ?? {}).map(([key, spec]) => [
key,
convertSpec(spec),
]),
);
TOOL_MAP.slack = SLACK_SPEC;
function shortenHomeInString(input: string): string {
if (!input) {
return input;
}
// Browser-safe home shortening: avoid importing Node-only helpers (keeps Vite builds working in Docker/CI).
const patterns = [
{ re: /^\/Users\/[^/]+(\/|$)/, replacement: "~$1" }, // macOS
{ re: /^\/home\/[^/]+(\/|$)/, replacement: "~$1" }, // Linux
{ re: /^C:\\Users\\[^\\]+(\\|$)/i, replacement: "~$1" }, // Windows
] as const;
for (const pattern of patterns) {
if (pattern.re.test(input)) {
return input.replace(pattern.re, pattern.replacement);
}
}
return input;
}
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 icon = (spec?.icon ?? FALLBACK.icon ?? "puzzle") as IconName;
const title = spec?.title ?? defaultTitle(name);
const label = spec?.label ?? title;
let { verb, detail } = resolveToolVerbAndDetailForArgs({
toolKey: key,
args: params.args,
meta: params.meta,
spec,
fallbackDetailKeys: FALLBACK.detailKeys,
detailMode: "first",
detailCoerce: { includeFalse: true, includeZero: true },
});
if (detail) {
detail = shortenHomeInString(detail);
}
return {
name,
icon,
title,
label,
verb,
detail,
};
}
export function formatToolDetail(display: ToolDisplay): string | undefined {
return formatToolDetailText(display.detail, { prefixWithWith: true });
}
export function formatToolSummary(display: ToolDisplay): string {
const detail = formatToolDetail(display);
return detail ? `${display.label}: ${detail}` : display.label;
}