Skip to content

Commit e7965d9

Browse files
feat: lossless terminal output with on-demand retrieval (#10944)
1 parent a44842f commit e7965d9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+2790
-316
lines changed

packages/types/src/cloud.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,6 @@ export const organizationDefaultSettingsSchema = globalSettingsSchema
9999
maxWorkspaceFiles: true,
100100
showRooIgnoredFiles: true,
101101
terminalCommandDelay: true,
102-
terminalCompressProgressBar: true,
103-
terminalOutputLineLimit: true,
104102
terminalShellIntegrationDisabled: true,
105103
terminalShellIntegrationTimeout: true,
106104
terminalZshClearEolMark: true,
@@ -112,7 +110,6 @@ export const organizationDefaultSettingsSchema = globalSettingsSchema
112110
maxReadFileLine: z.number().int().gte(-1).optional(),
113111
maxWorkspaceFiles: z.number().int().nonnegative().optional(),
114112
terminalCommandDelay: z.number().int().nonnegative().optional(),
115-
terminalOutputLineLimit: z.number().int().nonnegative().optional(),
116113
terminalShellIntegrationTimeout: z.number().int().nonnegative().optional(),
117114
}),
118115
)

packages/types/src/global-settings.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,40 @@ import { languagesSchema } from "./vscode.js"
2323
export const DEFAULT_WRITE_DELAY_MS = 1000
2424

2525
/**
26-
* Default terminal output character limit constant.
27-
* This provides a reasonable default that aligns with typical terminal usage
28-
* while preventing context window explosions from extremely long lines.
26+
* Terminal output preview size options for persisted command output.
27+
*
28+
* Controls how much command output is kept in memory as a "preview" before
29+
* the LLM decides to retrieve more via `read_command_output`. Larger previews
30+
* mean more immediate context but consume more of the context window.
31+
*
32+
* - `small`: 5KB preview - Best for long-running commands with verbose output
33+
* - `medium`: 10KB preview - Balanced default for most use cases
34+
* - `large`: 20KB preview - Best when commands produce critical info early
35+
*
36+
* @see OutputInterceptor - Uses this setting to determine when to spill to disk
37+
* @see PersistedCommandOutput - Contains the resulting preview and artifact reference
2938
*/
30-
export const DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT = 50_000
39+
export type TerminalOutputPreviewSize = "small" | "medium" | "large"
40+
41+
/**
42+
* Byte limits for each terminal output preview size.
43+
*
44+
* Maps preview size names to their corresponding byte thresholds.
45+
* When command output exceeds these thresholds, the excess is persisted
46+
* to disk and made available via the `read_command_output` tool.
47+
*/
48+
export const TERMINAL_PREVIEW_BYTES: Record<TerminalOutputPreviewSize, number> = {
49+
small: 5 * 1024, // 5KB
50+
medium: 10 * 1024, // 10KB
51+
large: 20 * 1024, // 20KB
52+
}
53+
54+
/**
55+
* Default terminal output preview size.
56+
* The "medium" (10KB) setting provides a good balance between immediate
57+
* visibility and context window conservation for most use cases.
58+
*/
59+
export const DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE: TerminalOutputPreviewSize = "medium"
3160

3261
/**
3362
* Minimum checkpoint timeout in seconds.
@@ -147,8 +176,7 @@ export const globalSettingsSchema = z.object({
147176
maxImageFileSize: z.number().optional(),
148177
maxTotalImageSize: z.number().optional(),
149178

150-
terminalOutputLineLimit: z.number().optional(),
151-
terminalOutputCharacterLimit: z.number().optional(),
179+
terminalOutputPreviewSize: z.enum(["small", "medium", "large"]).optional(),
152180
terminalShellIntegrationTimeout: z.number().optional(),
153181
terminalShellIntegrationDisabled: z.boolean().optional(),
154182
terminalCommandDelay: z.number().optional(),
@@ -157,7 +185,6 @@ export const globalSettingsSchema = z.object({
157185
terminalZshOhMy: z.boolean().optional(),
158186
terminalZshP10k: z.boolean().optional(),
159187
terminalZdotdir: z.boolean().optional(),
160-
terminalCompressProgressBar: z.boolean().optional(),
161188

162189
diagnosticsEnabled: z.boolean().optional(),
163190

@@ -338,16 +365,13 @@ export const EVALS_SETTINGS: RooCodeSettings = {
338365
soundEnabled: false,
339366
soundVolume: 0.5,
340367

341-
terminalOutputLineLimit: 500,
342-
terminalOutputCharacterLimit: DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
343368
terminalShellIntegrationTimeout: 30000,
344369
terminalCommandDelay: 0,
345370
terminalPowershellCounter: false,
346371
terminalZshOhMy: true,
347372
terminalZshClearEolMark: true,
348373
terminalZshP10k: false,
349374
terminalZdotdir: true,
350-
terminalCompressProgressBar: true,
351375
terminalShellIntegrationDisabled: true,
352376

353377
diagnosticsEnabled: true,

packages/types/src/message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export const clineSays = [
182182
"codebase_search_result",
183183
"user_edit_todos",
184184
"too_many_tools_warning",
185+
"tool",
185186
] as const
186187

187188
export const clineSaySchema = z.enum(clineSays)

packages/types/src/terminal.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,69 @@ export const commandExecutionStatusSchema = z.discriminatedUnion("status", [
3232
])
3333

3434
export type CommandExecutionStatus = z.infer<typeof commandExecutionStatusSchema>
35+
36+
/**
37+
* PersistedCommandOutput
38+
*
39+
* Represents the result of a terminal command execution that may have been
40+
* truncated and persisted to disk.
41+
*
42+
* When command output exceeds the configured preview threshold, the full
43+
* output is saved to a disk artifact file. The LLM receives this structure
44+
* which contains:
45+
* - A preview of the output (for immediate display in context)
46+
* - Metadata about the full output (size, truncation status)
47+
* - A path to the artifact file for later retrieval via `read_command_output`
48+
*
49+
* ## Usage in execute_command Response
50+
*
51+
* The response format depends on whether truncation occurred:
52+
*
53+
* **Not truncated** (output fits in preview):
54+
* ```json
55+
* {
56+
* "preview": "full output here...",
57+
* "totalBytes": 1234,
58+
* "artifactPath": null,
59+
* "truncated": false
60+
* }
61+
* ```
62+
*
63+
* **Truncated** (output exceeded threshold):
64+
* ```json
65+
* {
66+
* "preview": "first 4KB of output...",
67+
* "totalBytes": 1048576,
68+
* "artifactPath": "/path/to/tasks/123/command-output/cmd-1706119234567.txt",
69+
* "truncated": true
70+
* }
71+
* ```
72+
*
73+
* @see OutputInterceptor - Creates these results during command execution
74+
* @see ReadCommandOutputTool - Retrieves full content from artifact files
75+
*/
76+
export interface PersistedCommandOutput {
77+
/**
78+
* Preview of the command output, truncated to the preview threshold.
79+
* Always contains the beginning of the output, even if truncated.
80+
*/
81+
preview: string
82+
83+
/**
84+
* Total size of the command output in bytes.
85+
* Useful for determining if additional reads are needed.
86+
*/
87+
totalBytes: number
88+
89+
/**
90+
* Absolute path to the artifact file containing full output.
91+
* `null` if output wasn't truncated (no artifact was created).
92+
*/
93+
artifactPath: string | null
94+
95+
/**
96+
* Whether the output was truncated (exceeded preview threshold).
97+
* When `true`, use `read_command_output` to retrieve full content.
98+
*/
99+
truncated: boolean
100+
}

packages/types/src/tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type ToolGroup = z.infer<typeof toolGroupsSchema>
1717
export const toolNames = [
1818
"execute_command",
1919
"read_file",
20+
"read_command_output",
2021
"write_to_file",
2122
"apply_diff",
2223
"search_and_replace",

packages/types/src/vscode-extension-host.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,7 @@ export type ExtensionState = Pick<
302302
| "soundEnabled"
303303
| "soundVolume"
304304
| "maxConcurrentFileReads"
305-
| "terminalOutputLineLimit"
306-
| "terminalOutputCharacterLimit"
305+
| "terminalOutputPreviewSize"
307306
| "terminalShellIntegrationTimeout"
308307
| "terminalShellIntegrationDisabled"
309308
| "terminalCommandDelay"
@@ -312,7 +311,6 @@ export type ExtensionState = Pick<
312311
| "terminalZshOhMy"
313312
| "terminalZshP10k"
314313
| "terminalZdotdir"
315-
| "terminalCompressProgressBar"
316314
| "diagnosticsEnabled"
317315
| "language"
318316
| "modeApiConfigs"
@@ -780,6 +778,7 @@ export interface ClineSayTool {
780778
| "newFileCreated"
781779
| "codebaseSearch"
782780
| "readFile"
781+
| "readCommandOutput"
783782
| "fetchInstructions"
784783
| "listFilesTopLevel"
785784
| "listFilesRecursive"
@@ -792,6 +791,12 @@ export interface ClineSayTool {
792791
| "runSlashCommand"
793792
| "updateTodoList"
794793
path?: string
794+
// For readCommandOutput
795+
readStart?: number
796+
readEnd?: number
797+
totalBytes?: number
798+
searchPattern?: string
799+
matchCount?: number
795800
diff?: string
796801
content?: string
797802
// Unified diff statistics computed by the extension

pnpm-lock.yaml

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/assistant-message/NativeToolCallParser.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,17 @@ export class NativeToolCallParser {
790790
}
791791
break
792792

793+
case "read_command_output":
794+
if (args.artifact_id !== undefined) {
795+
nativeArgs = {
796+
artifact_id: args.artifact_id,
797+
search: args.search,
798+
offset: args.offset,
799+
limit: args.limit,
800+
} as NativeArgsFor<TName>
801+
}
802+
break
803+
793804
case "write_to_file":
794805
if (args.path !== undefined && args.content !== undefined) {
795806
nativeArgs = {

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Task } from "../task/Task"
1717
import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
1818
import { listFilesTool } from "../tools/ListFilesTool"
1919
import { readFileTool } from "../tools/ReadFileTool"
20+
import { readCommandOutputTool } from "../tools/ReadCommandOutputTool"
2021
import { writeToFileTool } from "../tools/WriteToFileTool"
2122
import { searchAndReplaceTool } from "../tools/SearchAndReplaceTool"
2223
import { searchReplaceTool } from "../tools/SearchReplaceTool"
@@ -402,8 +403,10 @@ export async function presentAssistantMessage(cline: Task) {
402403
return `[${block.name}]`
403404
case "switch_mode":
404405
return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
405-
case "codebase_search": // Add case for the new tool
406+
case "codebase_search":
406407
return `[${block.name} for '${block.params.query}']`
408+
case "read_command_output":
409+
return `[${block.name} for '${block.params.artifact_id}']`
407410
case "update_todo_list":
408411
return `[${block.name}]`
409412
case "new_task": {
@@ -846,6 +849,13 @@ export async function presentAssistantMessage(cline: Task) {
846849
pushToolResult,
847850
})
848851
break
852+
case "read_command_output":
853+
await readCommandOutputTool.handle(cline, block as ToolUse<"read_command_output">, {
854+
askApproval,
855+
handleError,
856+
pushToolResult,
857+
})
858+
break
849859
case "use_mcp_tool":
850860
await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
851861
askApproval,
@@ -1088,6 +1098,7 @@ function containsXmlToolMarkup(text: string): boolean {
10881098
"generate_image",
10891099
"list_files",
10901100
"new_task",
1101+
"read_command_output",
10911102
"read_file",
10921103
"search_and_replace",
10931104
"search_files",

src/core/environment/getEnvironmentDetails.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import pWaitFor from "p-wait-for"
66
import delay from "delay"
77

88
import type { ExperimentId } from "@roo-code/types"
9-
import { DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT } from "@roo-code/types"
109

1110
import { formatLanguage } from "../../shared/language"
1211
import { defaultModeSlug, getFullModeDetails } from "../../shared/modes"
@@ -26,11 +25,7 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
2625

2726
const clineProvider = cline.providerRef.deref()
2827
const state = await clineProvider?.getState()
29-
const {
30-
terminalOutputLineLimit = 500,
31-
terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
32-
maxWorkspaceFiles = 200,
33-
} = state ?? {}
28+
const { maxWorkspaceFiles = 200 } = state ?? {}
3429

3530
// It could be useful for cline to know if the user went from one or no
3631
// file to another between messages, so we always include this context.
@@ -112,11 +107,7 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
112107
let newOutput = TerminalRegistry.getUnretrievedOutput(busyTerminal.id)
113108

114109
if (newOutput) {
115-
newOutput = Terminal.compressTerminalOutput(
116-
newOutput,
117-
terminalOutputLineLimit,
118-
terminalOutputCharacterLimit,
119-
)
110+
newOutput = Terminal.compressTerminalOutput(newOutput)
120111
terminalDetails += `\n### New Output\n${newOutput}`
121112
}
122113
}
@@ -144,11 +135,7 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
144135
let output = process.getUnretrievedOutput()
145136

146137
if (output) {
147-
output = Terminal.compressTerminalOutput(
148-
output,
149-
terminalOutputLineLimit,
150-
terminalOutputCharacterLimit,
151-
)
138+
output = Terminal.compressTerminalOutput(output)
152139
terminalOutputs.push(`Command: \`${process.command}\`\n${output}`)
153140
}
154141
}

0 commit comments

Comments
 (0)