105 lines
3.9 KiB
TypeScript
105 lines
3.9 KiB
TypeScript
import { safeActivityDetail } from "./activity-text";
|
|
import { formatCompactWebUrl, parseSafeActivityHttpUrl } from "./web-url";
|
|
|
|
export type McpActivityStatus = "running" | "done" | "error";
|
|
|
|
export interface McpActivityDescription {
|
|
action: string;
|
|
target?: string;
|
|
}
|
|
|
|
export function describeMcpActivity(
|
|
toolName: string,
|
|
args: unknown,
|
|
status: McpActivityStatus,
|
|
): McpActivityDescription {
|
|
const name = toolName.toLowerCase();
|
|
|
|
if (matches(name, "navigate", "goto", "open_url", "visit")) {
|
|
return describe(status, "Opening", "Opened", "Could not open", value(args, ["url"]));
|
|
}
|
|
if (matches(name, "click", "tap")) {
|
|
return describe(status, "Clicking", "Clicked", "Could not click", elementTarget(args));
|
|
}
|
|
if (matches(name, "type", "fill", "enter_text", "insert_text")) {
|
|
const target = value(args, ["element", "selector", "ref", "name"]);
|
|
return describe(status, "Entering text", "Entered text", "Could not enter text", target && `in ${target}`);
|
|
}
|
|
if (matches(name, "press_key", "keypress")) {
|
|
return describe(status, "Pressing", "Pressed", "Could not press", value(args, ["key"]));
|
|
}
|
|
if (matches(name, "hover")) {
|
|
return describe(status, "Hovering over", "Hovered over", "Could not hover over", elementTarget(args));
|
|
}
|
|
if (matches(name, "select", "select_option")) {
|
|
return describe(status, "Selecting", "Selected", "Could not select", elementTarget(args));
|
|
}
|
|
if (matches(name, "snapshot", "inspect", "get_page_content", "page_content")) {
|
|
return describe(status, "Inspecting page", "Inspected page", "Could not inspect page");
|
|
}
|
|
if (matches(name, "screenshot", "capture_screenshot")) {
|
|
return describe(status, "Capturing screenshot", "Captured screenshot", "Could not capture screenshot");
|
|
}
|
|
if (matches(name, "wait", "wait_for")) {
|
|
return describe(status, "Waiting for page", "Waited for page", "Page did not become ready");
|
|
}
|
|
if (matches(name, "search", "web_search")) {
|
|
return describe(status, "Searching", "Searched", "Could not search", value(args, ["query", "q"]));
|
|
}
|
|
|
|
const action = humanizeToolName(toolName);
|
|
if (status === "running") return { action: `Running ${action}` };
|
|
if (status === "error") return { action: `${action} failed` };
|
|
return { action: `${action} completed` };
|
|
}
|
|
|
|
function describe(
|
|
status: McpActivityStatus,
|
|
running: string,
|
|
done: string,
|
|
failed: string,
|
|
target?: string,
|
|
): McpActivityDescription {
|
|
return {
|
|
action: status === "running" ? running : status === "error" ? failed : done,
|
|
target: target ? compactUrl(target) : undefined,
|
|
};
|
|
}
|
|
|
|
function matches(name: string, ...actions: string[]): boolean {
|
|
return actions.some((action) => name === action || name.endsWith(`_${action}`));
|
|
}
|
|
|
|
function elementTarget(args: unknown): string | undefined {
|
|
return value(args, ["element", "selector", "ref", "name", "text"]);
|
|
}
|
|
|
|
function value(args: unknown, keys: string[]): string | undefined {
|
|
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
const record = args as Record<string, unknown>;
|
|
for (const key of keys) {
|
|
const candidate = record[key];
|
|
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
|
|
if (typeof candidate === "number" || typeof candidate === "boolean") return String(candidate);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function compactUrl(value: string): string {
|
|
const url = parseSafeActivityHttpUrl(value);
|
|
if (url) return formatCompactWebUrl(url);
|
|
if (/^https?:\/\//i.test(value.trim())) return "Private address";
|
|
return safeActivityDetail(value, 80);
|
|
}
|
|
|
|
function humanizeToolName(value: string): string {
|
|
const words = value
|
|
.replace(/^(?:browser|page|playwright)[_.-]+/i, "")
|
|
.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 call";
|
|
}
|