feat: centralize tool display metadata
This commit is contained in:
@@ -575,6 +575,49 @@
|
||||
color: var(--chat-text);
|
||||
}
|
||||
|
||||
.chat-tool-card {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .chat-tool-card {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.chat-tool-card__title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--chat-text);
|
||||
}
|
||||
|
||||
.chat-tool-card__detail {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.chat-tool-card__output {
|
||||
margin-top: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
color: var(--chat-text);
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .chat-tool-card__output {
|
||||
background: rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
|
||||
.chat-stamp {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
|
||||
197
ui/src/ui/tool-display.json
Normal file
197
ui/src/ui/tool-display.json
Normal file
@@ -0,0 +1,197 @@
|
||||
{
|
||||
"version": 1,
|
||||
"fallback": {
|
||||
"emoji": "🧩",
|
||||
"detailKeys": [
|
||||
"command",
|
||||
"path",
|
||||
"url",
|
||||
"targetUrl",
|
||||
"targetId",
|
||||
"ref",
|
||||
"element",
|
||||
"node",
|
||||
"nodeId",
|
||||
"jobId",
|
||||
"requestId",
|
||||
"to",
|
||||
"channelId",
|
||||
"guildId",
|
||||
"userId",
|
||||
"name",
|
||||
"query",
|
||||
"pattern",
|
||||
"messageId"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"bash": {
|
||||
"emoji": "🛠️",
|
||||
"title": "Bash",
|
||||
"detailKeys": ["command"]
|
||||
},
|
||||
"process": {
|
||||
"emoji": "🧰",
|
||||
"title": "Process",
|
||||
"detailKeys": ["sessionId"]
|
||||
},
|
||||
"read": {
|
||||
"emoji": "📖",
|
||||
"title": "Read",
|
||||
"detailKeys": ["path"]
|
||||
},
|
||||
"write": {
|
||||
"emoji": "✍️",
|
||||
"title": "Write",
|
||||
"detailKeys": ["path"]
|
||||
},
|
||||
"edit": {
|
||||
"emoji": "📝",
|
||||
"title": "Edit",
|
||||
"detailKeys": ["path"]
|
||||
},
|
||||
"attach": {
|
||||
"emoji": "📎",
|
||||
"title": "Attach",
|
||||
"detailKeys": ["path", "url", "fileName"]
|
||||
},
|
||||
"browser": {
|
||||
"emoji": "🌐",
|
||||
"title": "Browser",
|
||||
"actions": {
|
||||
"status": { "label": "status" },
|
||||
"start": { "label": "start" },
|
||||
"stop": { "label": "stop" },
|
||||
"tabs": { "label": "tabs" },
|
||||
"open": { "label": "open", "detailKeys": ["targetUrl"] },
|
||||
"focus": { "label": "focus", "detailKeys": ["targetId"] },
|
||||
"close": { "label": "close", "detailKeys": ["targetId"] },
|
||||
"snapshot": {
|
||||
"label": "snapshot",
|
||||
"detailKeys": ["targetUrl", "targetId", "ref", "element", "format"]
|
||||
},
|
||||
"screenshot": {
|
||||
"label": "screenshot",
|
||||
"detailKeys": ["targetUrl", "targetId", "ref", "element"]
|
||||
},
|
||||
"navigate": {
|
||||
"label": "navigate",
|
||||
"detailKeys": ["targetUrl", "targetId"]
|
||||
},
|
||||
"console": { "label": "console", "detailKeys": ["level", "targetId"] },
|
||||
"pdf": { "label": "pdf", "detailKeys": ["targetId"] },
|
||||
"upload": {
|
||||
"label": "upload",
|
||||
"detailKeys": ["paths", "ref", "inputRef", "element", "targetId"]
|
||||
},
|
||||
"dialog": {
|
||||
"label": "dialog",
|
||||
"detailKeys": ["accept", "promptText", "targetId"]
|
||||
},
|
||||
"act": {
|
||||
"label": "act",
|
||||
"detailKeys": ["request.kind", "request.ref", "request.selector", "request.text", "request.value"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"canvas": {
|
||||
"emoji": "🖼️",
|
||||
"title": "Canvas",
|
||||
"actions": {
|
||||
"present": { "label": "present", "detailKeys": ["target", "node", "nodeId"] },
|
||||
"hide": { "label": "hide", "detailKeys": ["node", "nodeId"] },
|
||||
"navigate": { "label": "navigate", "detailKeys": ["url", "node", "nodeId"] },
|
||||
"eval": { "label": "eval", "detailKeys": ["javaScript", "node", "nodeId"] },
|
||||
"snapshot": { "label": "snapshot", "detailKeys": ["format", "node", "nodeId"] },
|
||||
"a2ui_push": { "label": "A2UI push", "detailKeys": ["jsonlPath", "node", "nodeId"] },
|
||||
"a2ui_reset": { "label": "A2UI reset", "detailKeys": ["node", "nodeId"] }
|
||||
}
|
||||
},
|
||||
"nodes": {
|
||||
"emoji": "📱",
|
||||
"title": "Nodes",
|
||||
"actions": {
|
||||
"status": { "label": "status" },
|
||||
"describe": { "label": "describe", "detailKeys": ["node", "nodeId"] },
|
||||
"pending": { "label": "pending" },
|
||||
"approve": { "label": "approve", "detailKeys": ["requestId"] },
|
||||
"reject": { "label": "reject", "detailKeys": ["requestId"] },
|
||||
"notify": { "label": "notify", "detailKeys": ["node", "nodeId", "title", "body"] },
|
||||
"camera_snap": { "label": "camera snap", "detailKeys": ["node", "nodeId", "facing", "deviceId"] },
|
||||
"camera_list": { "label": "camera list", "detailKeys": ["node", "nodeId"] },
|
||||
"camera_clip": { "label": "camera clip", "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] },
|
||||
"screen_record": {
|
||||
"label": "screen record",
|
||||
"detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"cron": {
|
||||
"emoji": "⏰",
|
||||
"title": "Cron",
|
||||
"actions": {
|
||||
"status": { "label": "status" },
|
||||
"list": { "label": "list" },
|
||||
"add": {
|
||||
"label": "add",
|
||||
"detailKeys": ["job.name", "job.id", "job.schedule", "job.cron"]
|
||||
},
|
||||
"update": { "label": "update", "detailKeys": ["jobId"] },
|
||||
"remove": { "label": "remove", "detailKeys": ["jobId"] },
|
||||
"run": { "label": "run", "detailKeys": ["jobId"] },
|
||||
"runs": { "label": "runs", "detailKeys": ["jobId"] },
|
||||
"wake": { "label": "wake", "detailKeys": ["text", "mode"] }
|
||||
}
|
||||
},
|
||||
"gateway": {
|
||||
"emoji": "🔌",
|
||||
"title": "Gateway",
|
||||
"actions": {
|
||||
"restart": { "label": "restart", "detailKeys": ["reason", "delayMs"] }
|
||||
}
|
||||
},
|
||||
"whatsapp_login": {
|
||||
"emoji": "🟢",
|
||||
"title": "WhatsApp Login",
|
||||
"actions": {
|
||||
"start": { "label": "start" },
|
||||
"wait": { "label": "wait" }
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"emoji": "💬",
|
||||
"title": "Discord",
|
||||
"actions": {
|
||||
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] },
|
||||
"reactions": { "label": "reactions", "detailKeys": ["channelId", "messageId"] },
|
||||
"sticker": { "label": "sticker", "detailKeys": ["to", "stickerIds"] },
|
||||
"poll": { "label": "poll", "detailKeys": ["question", "to"] },
|
||||
"permissions": { "label": "permissions", "detailKeys": ["channelId"] },
|
||||
"readMessages": { "label": "read messages", "detailKeys": ["channelId", "limit"] },
|
||||
"sendMessage": { "label": "send", "detailKeys": ["to", "content"] },
|
||||
"editMessage": { "label": "edit", "detailKeys": ["channelId", "messageId"] },
|
||||
"deleteMessage": { "label": "delete", "detailKeys": ["channelId", "messageId"] },
|
||||
"threadCreate": { "label": "thread create", "detailKeys": ["channelId", "name"] },
|
||||
"threadList": { "label": "thread list", "detailKeys": ["guildId", "channelId"] },
|
||||
"threadReply": { "label": "thread reply", "detailKeys": ["channelId", "content"] },
|
||||
"pinMessage": { "label": "pin", "detailKeys": ["channelId", "messageId"] },
|
||||
"unpinMessage": { "label": "unpin", "detailKeys": ["channelId", "messageId"] },
|
||||
"listPins": { "label": "list pins", "detailKeys": ["channelId"] },
|
||||
"searchMessages": { "label": "search", "detailKeys": ["guildId", "content"] },
|
||||
"memberInfo": { "label": "member", "detailKeys": ["guildId", "userId"] },
|
||||
"roleInfo": { "label": "roles", "detailKeys": ["guildId"] },
|
||||
"emojiList": { "label": "emoji list", "detailKeys": ["guildId"] },
|
||||
"roleAdd": { "label": "role add", "detailKeys": ["guildId", "userId", "roleId"] },
|
||||
"roleRemove": { "label": "role remove", "detailKeys": ["guildId", "userId", "roleId"] },
|
||||
"channelInfo": { "label": "channel", "detailKeys": ["channelId"] },
|
||||
"channelList": { "label": "channels", "detailKeys": ["guildId"] },
|
||||
"voiceStatus": { "label": "voice", "detailKeys": ["guildId", "userId"] },
|
||||
"eventList": { "label": "events", "detailKeys": ["guildId"] },
|
||||
"eventCreate": { "label": "event create", "detailKeys": ["guildId", "name"] },
|
||||
"timeout": { "label": "timeout", "detailKeys": ["guildId", "userId"] },
|
||||
"kick": { "label": "kick", "detailKeys": ["guildId", "userId"] },
|
||||
"ban": { "label": "ban", "detailKeys": ["guildId", "userId"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
199
ui/src/ui/tool-display.ts
Normal file
199
ui/src/ui/tool-display.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import rawConfig from "./tool-display.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 = rawConfig 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(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}`;
|
||||
}
|
||||
|
||||
function shortenHomeInString(input: string): string {
|
||||
if (!input) return input;
|
||||
return input
|
||||
.replace(/\/Users\/[^/]+/g, "~")
|
||||
.replace(/\/home\/[^/]+/g, "~");
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
import type { SessionsListResult } from "../types";
|
||||
import { resolveToolDisplay, formatToolDetail } from "../tool-display";
|
||||
|
||||
export type ChatProps = {
|
||||
sessionKey: string;
|
||||
@@ -168,11 +169,15 @@ function resolveSessionOptions(
|
||||
function renderMessage(message: unknown, opts?: { streaming?: boolean }) {
|
||||
const m = message as Record<string, unknown>;
|
||||
const role = typeof m.role === "string" ? m.role : "unknown";
|
||||
const toolCards = extractToolCards(message);
|
||||
const isToolResult = isToolResultMessage(message);
|
||||
const text =
|
||||
extractText(message) ??
|
||||
(typeof m.content === "string"
|
||||
? m.content
|
||||
: JSON.stringify(message, null, 2));
|
||||
!isToolResult
|
||||
? extractText(message) ??
|
||||
(typeof m.content === "string"
|
||||
? m.content
|
||||
: JSON.stringify(message, null, 2))
|
||||
: null;
|
||||
|
||||
const timestamp =
|
||||
typeof m.timestamp === "number" ? new Date(m.timestamp).toLocaleTimeString() : "";
|
||||
@@ -182,7 +187,8 @@ function renderMessage(message: unknown, opts?: { streaming?: boolean }) {
|
||||
<div class="chat-line ${klass}">
|
||||
<div class="chat-msg">
|
||||
<div class="chat-bubble ${opts?.streaming ? "streaming" : ""}">
|
||||
<div class="chat-text">${text}</div>
|
||||
${text ? html`<div class="chat-text">${text}</div>` : nothing}
|
||||
${toolCards.map((card) => renderToolCard(card))}
|
||||
</div>
|
||||
<div class="chat-stamp mono">
|
||||
${who}${timestamp ? html` · ${timestamp}` : nothing}
|
||||
@@ -209,3 +215,94 @@ function extractText(message: unknown): string | null {
|
||||
if (typeof m.text === "string") return m.text;
|
||||
return null;
|
||||
}
|
||||
|
||||
type ToolCard = {
|
||||
kind: "call" | "result";
|
||||
name: string;
|
||||
args?: unknown;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
function extractToolCards(message: unknown): ToolCard[] {
|
||||
const m = message as Record<string, unknown>;
|
||||
const content = normalizeContent(m.content);
|
||||
const cards: ToolCard[] = [];
|
||||
|
||||
for (const item of content) {
|
||||
const kind = String(item.type ?? "").toLowerCase();
|
||||
const isToolCall =
|
||||
["toolcall", "tool_call", "tooluse", "tool_use"].includes(kind) ||
|
||||
(typeof item.name === "string" && item.arguments != null);
|
||||
if (isToolCall) {
|
||||
cards.push({
|
||||
kind: "call",
|
||||
name: (item.name as string) ?? "tool",
|
||||
args: coerceArgs(item.arguments ?? item.args),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of content) {
|
||||
const kind = String(item.type ?? "").toLowerCase();
|
||||
if (kind !== "toolresult" && kind !== "tool_result") continue;
|
||||
const text = extractToolText(item);
|
||||
const name = typeof item.name === "string" ? item.name : "tool";
|
||||
cards.push({ kind: "result", name, text });
|
||||
}
|
||||
|
||||
if (isToolResultMessage(message) && !cards.some((card) => card.kind === "result")) {
|
||||
const name =
|
||||
(typeof m.toolName === "string" && m.toolName) ||
|
||||
(typeof m.tool_name === "string" && m.tool_name) ||
|
||||
"tool";
|
||||
const text = extractText(message) ?? undefined;
|
||||
cards.push({ kind: "result", name, text });
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
function renderToolCard(card: ToolCard) {
|
||||
const display = resolveToolDisplay({ name: card.name, args: card.args });
|
||||
const detail = formatToolDetail(display);
|
||||
return html`
|
||||
<div class="chat-tool-card">
|
||||
<div class="chat-tool-card__title">${display.emoji} ${display.label}</div>
|
||||
${detail
|
||||
? html`<div class="chat-tool-card__detail">${detail}</div>`
|
||||
: nothing}
|
||||
${card.text
|
||||
? html`<div class="chat-tool-card__output">${card.text}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function normalizeContent(content: unknown): Array<Record<string, unknown>> {
|
||||
if (!Array.isArray(content)) return [];
|
||||
return content.filter(Boolean) as Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
function coerceArgs(value: unknown): unknown {
|
||||
if (typeof value !== "string") return value;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return value;
|
||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function extractToolText(item: Record<string, unknown>): string | undefined {
|
||||
if (typeof item.text === "string") return item.text;
|
||||
if (typeof item.content === "string") return item.content;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isToolResultMessage(message: unknown): boolean {
|
||||
const m = message as Record<string, unknown>;
|
||||
const role = typeof m.role === "string" ? m.role.toLowerCase() : "";
|
||||
return role === "toolresult" || role === "tool_result";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user