diff --git a/packages/coding-agent/.changes/eng-5344-terminal-output-sanitize.md b/packages/coding-agent/.changes/eng-5344-terminal-output-sanitize.md new file mode 100644 index 0000000000..5d4aac42c4 --- /dev/null +++ b/packages/coding-agent/.changes/eng-5344-terminal-output-sanitize.md @@ -0,0 +1 @@ +- Fixed assistant text, thinking, tool output and link targets being able to send terminal control sequences (clipboard writes, screen clears, title changes) to the terminal ([ENG-5344](https://linear.app/primeintellect/issue/ENG-5344)). diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 9b72ffcc24..6cb9b396df 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -1,6 +1,6 @@ import { existsSync } from "node:fs"; import type { AgentTool } from "@earendil-works/pi-agent-core"; -import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui"; +import { Container, sanitizeTerminalText, Text, truncateToWidth } from "@earendil-works/pi-tui"; import { type Static, Type } from "typebox"; import { expandCollapseHint } from "../../modes/interactive/components/keybinding-hints.js"; import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js"; @@ -175,7 +175,8 @@ function formatDuration(ms: number): string { } function formatBashCall(args: { command?: string; timeout?: number } | undefined): string { - const command = str(args?.command); + const rawCommand = str(args?.command); + const command = rawCommand === null ? null : sanitizeTerminalText(rawCommand); const timeout = args?.timeout as number | undefined; const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : ""; let commandDisplay: string; diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index df71f893f4..1a57ef52ef 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -1,5 +1,13 @@ import type { AgentTool } from "@earendil-works/pi-agent-core"; -import { Box, type Component, Container, Spacer, Text, wrapTextWithAnsi } from "@earendil-works/pi-tui"; +import { + Box, + type Component, + Container, + Spacer, + sanitizeTerminalText, + Text, + wrapTextWithAnsi, +} from "@earendil-works/pi-tui"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises"; import { type Static, Type } from "typebox"; @@ -201,7 +209,7 @@ function formatEditCall( ): string { const invalidArg = invalidArgText(theme); const rawPath = str(args?.file_path ?? args?.path); - const path = rawPath !== null ? shortenPath(rawPath) : null; + const path = rawPath !== null ? sanitizeTerminalText(shortenPath(rawPath)) : null; const pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."); return `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`; } @@ -224,7 +232,7 @@ function formatEditResult( if (!errorText || errorText === previewError) { return undefined; } - return theme.fg("error", errorText); + return theme.fg("error", sanitizeTerminalText(errorText)); } const resultDiff = result.details?.diff; diff --git a/packages/coding-agent/src/core/tools/render-utils.ts b/packages/coding-agent/src/core/tools/render-utils.ts index 19893aa50e..31487c5365 100644 --- a/packages/coding-agent/src/core/tools/render-utils.ts +++ b/packages/coding-agent/src/core/tools/render-utils.ts @@ -1,6 +1,6 @@ import * as os from "node:os"; import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; -import { getImageDimensions, imageFallback } from "@earendil-works/pi-tui"; +import { getImageDimensions, imageFallback, sanitizeTerminalText } from "@earendil-works/pi-tui"; import stripAnsi from "strip-ansi"; import { sanitizeBinaryOutput } from "../../utils/shell.js"; @@ -38,7 +38,9 @@ export function getTextOutput( const textBlocks = result.content.filter((c) => c.type === "text"); const imageBlocks = result.content.filter((c) => c.type === "image"); - let output = textBlocks.map((c) => sanitizeBinaryOutput(stripAnsi(c.text || "")).replace(/\r/g, "")).join("\n"); + let output = textBlocks + .map((c) => sanitizeTerminalText(sanitizeBinaryOutput(stripAnsi(c.text || "")).replace(/\r/g, ""))) + .join("\n"); const includeImageDimensions = options.includeImageDimensions ?? true; if (imageBlocks.length > 0 && !showImages) { diff --git a/packages/coding-agent/src/modes/interactive/components/agent-message.ts b/packages/coding-agent/src/modes/interactive/components/agent-message.ts index cf6845a495..61d6f0f991 100644 --- a/packages/coding-agent/src/modes/interactive/components/agent-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/agent-message.ts @@ -3,6 +3,7 @@ import { Container, type MarkdownTheme, Spacer, + sanitizeTerminalText, Text, truncateToWidth, visibleWidth, @@ -13,7 +14,7 @@ import { getMarkdownTheme, theme } from "../theme/theme.js"; import { expandCollapseHint } from "./keybinding-hints.js"; function collapseText(text: string): string { - return text.replace(/\s+/g, " ").trim(); + return sanitizeTerminalText(text).replace(/\s+/g, " ").trim(); } /** `◆