Files
sproutclaw-data/agent/extensions/pi-mcp-adapter/tool-result-renderer.ts
shumengya 50edff80f5 feat: 导出 SproutClaw .sproutclaw 配置
包含 extensions、skills、prompts、settings、auth、models、mcp 等配置。
排除 node_modules、npm 缓存、sessions 等运行时数据。
2026-06-26 15:48:56 +08:00

162 lines
4.9 KiB
TypeScript

import type { AgentToolResult, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
import { Text } from "@earendil-works/pi-tui";
type McpToolResultDetails = Record<string, unknown> & { error?: unknown };
type McpToolContentBlock = AgentToolResult<McpToolResultDetails>["content"][number];
interface RenderTheme {
fg: (name: string, text: string) => string;
bold?: (text: string) => string;
}
export interface McpProxyToolCallInput {
tool?: string;
args?: string;
connect?: string;
describe?: string;
search?: string;
regex?: boolean;
includeSchemas?: boolean;
server?: string;
action?: string;
}
interface McpToolRenderContext {
isError: boolean;
}
export interface McpToolResultDisplay {
lines: string[];
truncated: boolean;
}
const DEFAULT_MAX_CALL_INPUT_CHARS = 1500;
function truncateText(value: string, maxChars: number): string {
if (value.length <= maxChars) return value;
return `${value.slice(0, Math.max(0, maxChars - 1))}`;
}
function formatJsonish(value: unknown, maxChars: number): string {
if (typeof value === "string") {
try {
return truncateText(JSON.stringify(JSON.parse(value), null, 2), maxChars);
} catch {
return truncateText(value, maxChars);
}
}
try {
return truncateText(JSON.stringify(value, null, 2), maxChars);
} catch {
return truncateText(String(value), maxChars);
}
}
function hasUsefulObjectContent(value: unknown): boolean {
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
}
export function formatMcpProxyToolCallLines(
args: McpProxyToolCallInput,
maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS,
): string[] {
if (args.action === "ui-messages") return [`mcp ${args.action}`];
if (args.tool) {
const target = args.server ? `${args.tool} @ ${args.server}` : args.tool;
const lines = [`mcp call ${target}`];
if (args.args) lines.push(formatJsonish(args.args, maxInputChars));
return lines;
}
if (args.connect) return [`mcp connect ${args.connect}`];
if (args.describe) return [`mcp describe ${args.describe}`];
if (args.search) {
let line = `mcp search ${args.search}`;
if (args.server) line += ` @ ${args.server}`;
if (args.regex === true) line += " (regex)";
if (args.includeSchemas === false) line += " (schemas hidden)";
return [line];
}
if (args.server) return [`mcp list ${args.server}`];
if (args.action) return [`mcp ${args.action}`];
return ["mcp status"];
}
export function formatMcpDirectToolCallLines(
displayName: string,
args: Record<string, unknown>,
maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS,
): string[] {
if (!hasUsefulObjectContent(args)) return [displayName];
return [displayName, formatJsonish(args, maxInputChars)];
}
function renderToolCallLines(lines: string[], theme: RenderTheme) {
const [title = "mcp", ...rest] = lines;
const styledTitle = theme.fg("toolTitle", theme.bold ? theme.bold(title) : title);
const styledRest = rest.map(line => theme.fg("muted", line));
return new Text([styledTitle, ...styledRest].join("\n"), 0, 0);
}
export function renderMcpProxyToolCall(args: McpProxyToolCallInput, theme: RenderTheme) {
return renderToolCallLines(formatMcpProxyToolCallLines(args), theme);
}
export function createMcpDirectToolCallRenderer(displayName: string) {
return (args: Record<string, unknown>, theme: RenderTheme) => {
return renderToolCallLines(formatMcpDirectToolCallLines(displayName, args), theme);
};
}
function blockToLines(block: McpToolContentBlock): string[] {
if (block.type === "text") {
return block.text.split("\n");
}
return [`[image: ${block.mimeType}]`];
}
export function formatMcpToolResultLines(
result: Pick<AgentToolResult<McpToolResultDetails>, "content">,
expanded: boolean,
maxCollapsedLines = 3,
): McpToolResultDisplay {
const allLines = result.content.flatMap(blockToLines);
const lines = allLines.length > 0 ? allLines : ["(empty result)"];
if (expanded || lines.length <= maxCollapsedLines) {
return { lines, truncated: false };
}
return {
lines: [...lines.slice(0, maxCollapsedLines), "…"],
truncated: true,
};
}
export function renderMcpToolResult(
result: AgentToolResult<McpToolResultDetails>,
options: ToolRenderResultOptions,
theme: RenderTheme,
context?: McpToolRenderContext,
) {
if (options.isPartial) {
return new Text(theme.fg("warning", "Running MCP tool..."), 0, 0);
}
const hasErrorDetails = Boolean(result.details.error);
const display = formatMcpToolResultLines(result, options.expanded || context?.isError === true || hasErrorDetails);
const output = display.lines
.map((line) => line === "…" ? theme.fg("muted", line) : theme.fg("toolOutput", line))
.join("\n");
const hint = display.truncated && !options.expanded
? `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
: "";
return new Text(`${output}${hint}`, 0, 0);
}