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; 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, }; }); }, };