Files
oh-my-pi/packages/coding-agent/src/mcp/render.ts
T
roboomp cf33b64033 fix(mcp): updated completed tool status icons
MCP tool renderers now merge call and result components so completed blocks replace the pending call header with success or error status.

MCP protocol errors now preserve top-level isError so downstream render state can use the error status consistently.

Added renderer coverage for completed MCP success and error blocks.

Fixes #1855
2026-06-04 12:14:57 +00:00

127 lines
4.4 KiB
TypeScript

/**
* TUI rendering for MCP tools.
*
* Provides structured display of MCP tool calls and results,
* showing args and output in JSON tree format similar to task tool.
*/
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
import {
formatArgsInline,
JSON_TREE_MAX_DEPTH_COLLAPSED,
JSON_TREE_MAX_DEPTH_EXPANDED,
JSON_TREE_MAX_LINES_COLLAPSED,
JSON_TREE_MAX_LINES_EXPANDED,
JSON_TREE_SCALAR_LEN_COLLAPSED,
JSON_TREE_SCALAR_LEN_EXPANDED,
renderJsonTreeLines,
} from "../tools/json-tree";
import { formatExpandHint, truncateToWidth } from "../tools/render-utils";
import { renderStatusLine } from "../tui";
import type { MCPToolDetails } from "./tool-bridge";
/**
* Render MCP tool call.
*/
export function renderMCPCall(args: Record<string, unknown>, theme: Theme, label: string): Component {
const lines: string[] = [];
lines.push(renderStatusLine({ icon: "pending", title: label }, theme));
if (args && typeof args === "object" && Object.keys(args).length > 0) {
// Show args inline preview
const preview = formatArgsInline(args, 70);
if (preview) {
lines.push(` ${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", preview)}`);
}
}
return new Text(lines.join("\n"), 0, 0);
}
/**
* Render MCP tool result.
*/
export function renderMCPResult(
result: { content: Array<{ type: string; text?: string }>; details?: MCPToolDetails; isError?: boolean },
options: RenderResultOptions,
theme: Theme,
args?: Record<string, unknown>,
): Component {
const { expanded } = options;
const lines: string[] = [];
const isError = result.isError ?? result.details?.isError ?? false;
const title = result.details ? `${result.details.serverName}/${result.details.mcpToolName}` : "MCP";
lines.push(renderStatusLine({ icon: isError ? "error" : "success", title }, theme));
// Args section (when expanded)
if (expanded && args && typeof args === "object" && Object.keys(args).length > 0) {
lines.push(`${theme.fg("dim", "Args")}`);
const maxDepth = JSON_TREE_MAX_DEPTH_EXPANDED;
const maxLines = JSON_TREE_MAX_LINES_EXPANDED;
const tree = renderJsonTreeLines(args, theme, maxDepth, maxLines, JSON_TREE_SCALAR_LEN_EXPANDED);
for (const line of tree.lines) {
lines.push(line);
}
if (tree.truncated) {
lines.push(theme.fg("dim", "…"));
}
lines.push(""); // Blank line before output
}
// Output section
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
const trimmedOutput = textContent.trimEnd();
if (!trimmedOutput) {
lines.push(theme.fg("dim", "(no output)"));
return new Text(lines.join("\n"), 0, 0);
}
// Try to parse as JSON for structured display
if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
try {
const parsed = JSON.parse(trimmedOutput);
const maxDepth = expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
const maxLines = expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
const maxScalarLen = expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
const tree = renderJsonTreeLines(parsed, theme, maxDepth, maxLines, maxScalarLen);
if (tree.lines.length > 0) {
for (const line of tree.lines) {
lines.push(line);
}
// Always show expand hint when collapsed (expanded view shows longer values and deeper nesting)
if (!expanded) {
lines.push(formatExpandHint(theme, expanded, true));
} else if (tree.truncated) {
lines.push(theme.fg("dim", "…"));
}
return new Text(lines.join("\n"), 0, 0);
}
} catch {
// Fall through to raw output
}
}
// Raw text output
const outputLines = trimmedOutput.split("\n");
const maxOutputLines = expanded ? 12 : 4;
const displayLines = outputLines.slice(0, maxOutputLines);
for (const line of displayLines) {
lines.push(theme.fg("toolOutput", truncateToWidth(line, 80)));
}
if (outputLines.length > maxOutputLines) {
const remaining = outputLines.length - maxOutputLines;
lines.push(`${theme.fg("dim", `… ${remaining} more lines`)} ${formatExpandHint(theme, expanded, true)}`);
} else if (!expanded) {
// Show expand hint when collapsed even if all lines shown (lines may be truncated)
lines.push(formatExpandHint(theme, expanded, true));
}
return new Text(lines.join("\n"), 0, 0);
}