Files
oh-my-pi/packages/coding-agent/src/tools/computer-renderer.ts
T
2026-07-24 01:40:04 +00:00

109 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Component } from "@oh-my-pi/pi-tui";
import { Text } from "@oh-my-pi/pi-tui";
import { sanitizeText } from "@oh-my-pi/pi-utils";
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
import type { Theme } from "../modes/theme/theme";
import { framedBlock, renderStatusLine } from "../tui";
import type { ComputerToolDetails } from "./computer";
import { replaceTabs, truncateToWidth } from "./render-utils";
interface ComputerRenderArgs {
actions?: Array<{ type?: unknown }>;
}
interface ComputerRenderResult {
content: Array<{ type: string; text?: string }>;
details?: unknown;
isError?: boolean;
}
function clean(value: unknown, width = 100): string {
const text = typeof value === "string" ? value : JSON.stringify(value) || String(value);
return truncateToWidth(replaceTabs(sanitizeText(text)).replace(/[\r\n]+/g, " "), width);
}
function isComputerToolDetails(value: unknown): value is ComputerToolDetails {
if (!value || typeof value !== "object") return false;
const details = value as Partial<ComputerToolDetails>;
return (
Array.isArray(details.actions) &&
Array.isArray(details.displays) &&
typeof details.width === "number" &&
typeof details.height === "number"
);
}
function actionDescription(args: ComputerRenderArgs | undefined): string | undefined {
if (!Array.isArray(args?.actions) || args.actions.length === 0) return undefined;
return clean(args.actions.map(action => (typeof action?.type === "string" ? action.type : "action")).join(" → "));
}
function resultDescription(details: ComputerToolDetails): string {
return clean(`${details.actions.join(" → ")} · ${details.width}×${details.height}`, 120);
}
function errorDescription(result: ComputerRenderResult, args: ComputerRenderArgs | undefined): string | undefined {
const text = result.content.find(item => item.type === "text" && typeof item.text === "string")?.text;
return text ? clean(text, 120) : actionDescription(args);
}
export const computerToolRenderer = {
mergeCallAndResult: true,
renderCall(args: ComputerRenderArgs, _options: RenderResultOptions, theme: Theme): Component {
return new Text(
renderStatusLine({ icon: "pending", title: "Computer", description: actionDescription(args) }, theme),
0,
0,
);
},
renderResult(
result: ComputerRenderResult,
options: RenderResultOptions,
theme: Theme,
args?: ComputerRenderArgs,
): Component {
const details = isComputerToolDetails(result.details) ? result.details : undefined;
const header = renderStatusLine(
result.isError
? { icon: "error", title: "Computer", description: errorDescription(result, args) }
: { icon: "success", title: "Computer", description: details ? resultDescription(details) : undefined },
theme,
);
if (!details) return new Text(header, 0, 0);
return framedBlock(theme, width => {
const body: string[] = [
theme.fg(
"dim",
`backend ${clean(details.backend)}${details.displayServer ? ` · server ${clean(details.displayServer)}` : ""} · capture ${clean(details.capturePermission)} · input ${clean(details.inputPermission)} · ${details.displays.length} display(s)`,
),
];
const displayLimit = options.expanded ? details.displays.length : Math.min(details.displays.length, 3);
for (const display of details.displays.slice(0, displayLimit)) {
body.push(
theme.fg(
"toolOutput",
clean(
`${display.id}${display.name ? ` ${display.name}` : ""}: logical ${display.x},${display.y} ${display.width}×${display.height}; pixels ${display.pixelX},${display.pixelY} ${display.pixelWidth}×${display.pixelHeight}; scale ${display.scale}${display.isPrimary ? "; primary" : ""}`,
160,
),
),
);
}
if (displayLimit < details.displays.length) {
body.push(theme.fg("dim", `… ${details.displays.length - displayLimit} more display(s)`));
}
if (details.capabilities) {
body.push(theme.fg("dim", `capabilities ${clean(details.capabilities, 160)}`));
}
return {
header,
sections: [{ lines: body }],
state: result.isError ? "error" : "success",
borderColor: result.isError ? "error" : "borderMuted",
applyBg: false,
width,
};
});
},
};