Files
nanobot/webui/src/components/thread/activity/trace-activity-model.ts
T

237 lines
7.2 KiB
TypeScript

import type { GenericToolStatus } from "./generic-tool-model";
import { safeActivityDetail, summarizeShellCommand } from "./activity-text";
import { presentWebSearchAction } from "./web-search-model";
import { displayWebHost, formatCompactWebUrl, parseSafeActivityHttpUrl } from "./web-url";
export interface TraceDescription {
kind: "search" | "tool" | "done" | "trace";
label: string;
detail: string;
icon?: "clock";
url?: string;
host?: string;
}
export function describeTraceLine(
line: string,
status: GenericToolStatus,
result?: unknown,
): TraceDescription {
const trimmed = line.trim();
const functionMatch = /^([a-zA-Z0-9_.-]+)\((.*)\)$/.exec(trimmed);
const name = (functionMatch?.[1] ?? "").toLowerCase().split(".").pop() || "";
const args = functionMatch?.[2] ?? "";
const parsedUrl = traceUrlFromArgs(args, trimmed);
const webDetail = parsedUrl ? formatCompactWebUrl(parsedUrl) : "";
const plainWebReadTrace =
!!parsedUrl && /\b(fetch(?:ing|ed)?|read(?:ing)?|opened?|opening)\b/i.test(trimmed);
if (/search/i.test(name)) {
const query = traceFieldFromArgs(args, ["query", "q", "text"]) || args || trimmed;
return {
kind: "search",
label: presentWebSearchAction(query, status),
detail: "",
};
}
if (/fetch|read|open/i.test(name) || plainWebReadTrace) {
const rawTarget = traceFieldFromArgs(args, ["path", "file_path", "url"]) || args || trimmed;
const pageTitle = parsedUrl ? webPageTitle(result) : "";
return {
kind: "tool",
label: pageTitle || statusCopy(status, "Reading", "Read", "Could not read"),
detail: webDetail || (/^https?:\/\//i.test(rawTarget.trim())
? "Private address"
: safeActivityDetail(rawTarget)),
url: parsedUrl?.href,
host: parsedUrl ? displayWebHost(parsedUrl.hostname) : undefined,
};
}
if (isShellTraceName(name)) return describeShellTrace(args, trimmed, status);
if (name === "write_file") {
return describeFileMutationTrace(args, status, "Writing file", "Wrote file", "Could not write file");
}
if (name === "edit_file" || name === "apply_patch") {
return describeFileMutationTrace(args, status, "Editing file", "Edited file", "Could not edit file");
}
if (name) {
const action = humanizeTraceToolName(name);
return {
kind: "tool",
label: statusCopy(
status,
`Running ${action}`,
`Completed ${action}`,
`Could not complete ${action}`,
),
detail: "",
};
}
if (/done|complete|success/i.test(trimmed)) {
return { kind: "done", label: "Completed step", detail: safeActivityDetail(trimmed) };
}
return {
kind: status === "done" ? "done" : "trace",
label: statusCopy(status, "Working", "Completed step", "Step failed"),
detail: safeActivityDetail(trimmed),
};
}
function webPageTitle(result: unknown): string {
if (result && typeof result === "object" && !Array.isArray(result)) {
const title = (result as Record<string, unknown>).title;
if (typeof title === "string") return safeActivityDetail(title);
}
if (typeof result !== "string") return "";
const heading = result.match(/^#\s+(.+)$/m)?.[1]?.trim();
return heading ? safeActivityDetail(heading) : "";
}
function describeShellTrace(
args: string,
fallback: string,
status: GenericToolStatus,
): TraceDescription {
const command = shellCommandFromArgs(args) || fallback;
if (/^(?:\/(?:usr\/)?bin\/)?date(?:\s|$)/i.test(command.trim())) {
return {
kind: "tool",
label: statusCopy(
status,
"Checking current time",
"Checked current time",
"Could not check current time",
),
detail: "",
icon: "clock",
};
}
return {
kind: "tool",
label: statusCopy(status, "Running command", "Ran command", "Command failed"),
detail: summarizeShellCommand(command),
};
}
function describeFileMutationTrace(
args: string,
status: GenericToolStatus,
running: string,
done: string,
failed: string,
): TraceDescription {
const path = traceFieldFromArgs(args, ["path", "file_path"]);
return {
kind: "tool",
label: statusCopy(status, running, done, failed),
detail: path ? safeActivityDetail(path) : "",
};
}
function statusCopy(
status: GenericToolStatus,
running: string,
done: string,
failed: string,
): string {
return status === "running" ? running : status === "error" ? failed : done;
}
function traceFieldFromArgs(args: string, keys: string[]): string {
const compactArgs = args.trim();
if (!compactArgs) return "";
try {
const parsed = JSON.parse(compactArgs) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "";
const record = parsed as Record<string, unknown>;
for (const key of keys) {
const value = record[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
} catch {
return "";
}
return "";
}
function isShellTraceName(name: string): boolean {
return [
"exec",
"exec_command",
"execute_command",
"run_command",
"run_shell",
"shell",
"terminal",
"bash",
"sh",
].includes(name.toLowerCase().split(".").pop() || name.toLowerCase());
}
function shellCommandFromArgs(args: string): string {
const compactArgs = args.trim();
if (!compactArgs) return "";
try {
const parsed = JSON.parse(compactArgs) as unknown;
if (typeof parsed === "string") return parsed;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "";
const record = parsed as Record<string, unknown>;
for (const key of ["command", "cmd", "script", "input"]) {
const value = record[key];
if (typeof value === "string" && value.trim()) return value;
}
} catch {
return compactArgs.replace(/^["']|["']$/g, "");
}
return "";
}
function humanizeTraceToolName(name: string): string {
const words = name
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/[._-]+/g, " ")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "tool action";
}
function traceUrlFromArgs(args: string, fallback: string): URL | null {
const candidates: string[] = [];
const compactArgs = args.trim();
if (compactArgs) {
try {
collectUrlCandidates(JSON.parse(compactArgs), candidates);
} catch {
candidates.push(compactArgs.replace(/^["']|["']$/g, ""));
}
}
candidates.push(fallback);
for (const candidate of candidates) {
const url = parseSafeActivityHttpUrl(candidate);
if (url) return url;
const embedded = candidate.match(/https?:\/\/[^\s"'<>),]+/i)?.[0];
if (embedded) {
const embeddedUrl = parseSafeActivityHttpUrl(embedded);
if (embeddedUrl) return embeddedUrl;
}
}
return null;
}
function collectUrlCandidates(value: unknown, candidates: string[]) {
if (typeof value === "string") {
candidates.push(value);
return;
}
if (!value || typeof value !== "object") return;
if (Array.isArray(value)) {
for (const item of value.slice(0, 6)) collectUrlCandidates(item, candidates);
return;
}
const record = value as Record<string, unknown>;
for (const key of ["url", "uri", "href", "link"]) {
if (typeof record[key] === "string") candidates.push(record[key]);
}
}