diff --git a/packages/coding-agent/src/tools/bash-interactive.ts b/packages/coding-agent/src/tools/bash-interactive.ts index fc39cb0c8..8618146fe 100644 --- a/packages/coding-agent/src/tools/bash-interactive.ts +++ b/packages/coding-agent/src/tools/bash-interactive.ts @@ -19,6 +19,7 @@ import { OutputSink, type OutputSummary } from "../session/streaming-output"; import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel"; import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "./output-meta"; import { formatStatusIcon, replaceTabs } from "./render-utils"; +import { readTerminalRows, styleTerminalRow } from "./terminal-output"; export interface BashInteractiveResult extends OutputSummary { exitCode: number | undefined; @@ -225,14 +226,10 @@ class BashInteractiveOverlayComponent implements Component { #readViewport(innerWidth: number, maxContentRows: number): string[] { this.#terminal.resize(innerWidth, maxContentRows); - const buffer = this.#terminal.buffer.active; - const viewportY = buffer.viewportY; - const visibleLines: string[] = []; - for (let i = 0; i < maxContentRows; i++) { - const line = buffer.getLine(viewportY + i)?.translateToString(true) ?? ""; - visibleLines.push(truncateToWidth(replaceTabs(sanitizeText(line)), innerWidth)); - } - return visibleLines; + const viewportY = this.#terminal.buffer.active.viewportY; + return readTerminalRows(this.#terminal, viewportY, maxContentRows).map(line => + truncateToWidth(styleTerminalRow(line, this.uiTheme.getFgAnsi("toolOutput")), innerWidth), + ); } render(width: number): readonly string[] { const safeWidth = Math.max(20, width); diff --git a/packages/coding-agent/src/tools/launch.ts b/packages/coding-agent/src/tools/launch.ts index 879c1555e..31c628bbf 100644 --- a/packages/coding-agent/src/tools/launch.ts +++ b/packages/coding-agent/src/tools/launch.ts @@ -8,11 +8,12 @@ import type { import type { ToolExample } from "@oh-my-pi/pi-ai"; import type { Component } from "@oh-my-pi/pi-tui"; import { Text } from "@oh-my-pi/pi-tui"; -import { prompt } from "@oh-my-pi/pi-utils"; +import { prompt, sanitizeText } from "@oh-my-pi/pi-utils"; import { type } from "arktype"; import type { RenderResultOptions } from "../extensibility/custom-tools/types"; import { daemonClientForProject } from "../launch/client"; import type { DaemonOperation, DaemonRpcResult, DaemonSnapshot, DaemonSpec, DaemonState } from "../launch/protocol"; +import { renderTerminalOutput } from "../launch/terminal-output"; import type { Theme, ThemeColor } from "../modes/theme/theme"; import launchDescription from "../prompts/tools/launch.md" with { type: "text" }; import { renderStatusLine } from "../tui"; @@ -32,6 +33,7 @@ import { TRUNCATE_LENGTHS, truncateToWidth, } from "./render-utils"; +import { styleTerminalRow } from "./terminal-output"; import { ToolError } from "./tool-errors"; const launchSchema = type({ @@ -92,6 +94,8 @@ export interface LaunchToolDetails { timedOut?: boolean; /** logs: daemon lifecycle state at read time. */ state?: DaemonState; + /** logs: virtual terminal rows for display; model-facing content remains sanitized text. */ + terminalRows?: string[]; /** wait: output line that satisfied the pattern. */ matched?: string; /** describe: immutable launch spec backing the command/cwd detail lines. */ @@ -243,8 +247,10 @@ function toolContent(result: DaemonRpcResult, params: LaunchParams): string { return result.daemons.length ? result.daemons.map(daemon => `- ${daemonLabel(daemon)}`).join("\n") : "No daemons."; - case "logs": - return `${result.text}${result.text && !result.text.endsWith("\n") ? "\n" : ""}[${result.name}: ${result.state}; cursor=${result.cursor}${result.timedOut ? "; follow timed out" : ""}]`; + case "logs": { + const text = sanitizeText(result.text); + return `${text}${text && !text.endsWith("\n") ? "\n" : ""}[${result.name}: ${result.state}; cursor=${result.cursor}${result.timedOut ? "; follow timed out" : ""}]`; + } case "wait": { const lines = [daemonLabel(result.daemon)]; if (result.matched) lines.push(`Matched: ${result.matched}`); @@ -270,14 +276,25 @@ function toolContent(result: DaemonRpcResult, params: LaunchParams): string { } } -function toolDetails(result: DaemonRpcResult): LaunchToolDetails { +async function toolDetails(result: DaemonRpcResult, params: LaunchParams): Promise { switch (result.op) { case "start": return { op: "start", daemon: result.daemon, timedOut: result.readyTimedOut }; case "list": return { op: "list", daemons: result.daemons }; - case "logs": - return { op: "logs", cursor: result.cursor, timedOut: result.timedOut, state: result.state }; + case "logs": { + const terminalRows = await renderTerminalOutput(result.text, { + head: params.head ?? false, + maxRows: Math.min(1_000, Math.floor(params.lines ?? 100)), + }); + return { + op: "logs", + cursor: result.cursor, + timedOut: result.timedOut, + state: result.state, + terminalRows, + }; + } case "wait": return { op: "wait", daemon: result.daemon, timedOut: result.timedOut, matched: result.matched }; case "send": @@ -372,7 +389,7 @@ export class LaunchTool implements AgentTool