Skip to content

Commit 37300ef

Browse files
roomote[bot]roomotedaniel-lxs
authored
fix: add character limit to prevent terminal output context explosion (RooCodeInc#5777)
* fix: add character limit to prevent terminal output context explosion - Enhanced truncateOutput function to accept character limits alongside line limits - Character limits take priority over line limits to prevent context window explosion - Added terminalOutputCharacterLimit setting (default: 100,000 characters) - Updated all terminal output processing to use both limits - Added comprehensive tests for character limit functionality Fixes RooCodeInc#5775 * feat: add terminal output character limit setting to UI - Add character limit slider to Terminal Settings UI (default: 50,000) - Update ExtensionStateContext to manage character limit state - Add validation for positive character limit values - Add English translation and translations for all 17 supported languages - Connect UI to backend through proper message handling - Character limit takes precedence over line limit to prevent memory issues * fix: update test expectations for character limit edge cases - Fix multi-byte character test to account for JavaScript's string length behavior - Fix newline content test to match actual slice behavior - Tests now correctly validate the truncateOutput function's character limit handling --------- Co-authored-by: Roo Code <[email protected]> Co-authored-by: Daniel Riccio <[email protected]>
1 parent 7ddc4e6 commit 37300ef

31 files changed

+431
-15
lines changed

packages/types/src/global-settings.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ import { languagesSchema } from "./vscode.js"
2222
*/
2323
export const DEFAULT_WRITE_DELAY_MS = 1000
2424

25+
/**
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.
29+
*/
30+
export const DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT = 50_000
31+
2532
/**
2633
* GlobalSettings
2734
*/
@@ -85,6 +92,7 @@ export const globalSettingsSchema = z.object({
8592
maxReadFileLine: z.number().optional(),
8693

8794
terminalOutputLineLimit: z.number().optional(),
95+
terminalOutputCharacterLimit: z.number().optional(),
8896
terminalShellIntegrationTimeout: z.number().optional(),
8997
terminalShellIntegrationDisabled: z.boolean().optional(),
9098
terminalCommandDelay: z.number().optional(),
@@ -228,6 +236,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
228236
soundVolume: 0.5,
229237

230238
terminalOutputLineLimit: 500,
239+
terminalOutputCharacterLimit: DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
231240
terminalShellIntegrationTimeout: 30000,
232241
terminalCommandDelay: 0,
233242
terminalPowershellCounter: false,

src/core/environment/getEnvironmentDetails.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ 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"
910

1011
import { EXPERIMENT_IDS, experiments as Experiments } from "../../shared/experiments"
1112
import { formatLanguage } from "../../shared/language"
@@ -25,7 +26,11 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
2526

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

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

113118
if (newOutput) {
114-
newOutput = Terminal.compressTerminalOutput(newOutput, terminalOutputLineLimit)
119+
newOutput = Terminal.compressTerminalOutput(
120+
newOutput,
121+
terminalOutputLineLimit,
122+
terminalOutputCharacterLimit,
123+
)
115124
terminalDetails += `\n### New Output\n${newOutput}`
116125
}
117126
}
@@ -139,7 +148,11 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
139148
let output = process.getUnretrievedOutput()
140149

141150
if (output) {
142-
output = Terminal.compressTerminalOutput(output, terminalOutputLineLimit)
151+
output = Terminal.compressTerminalOutput(
152+
output,
153+
terminalOutputLineLimit,
154+
terminalOutputCharacterLimit,
155+
)
143156
terminalOutputs.push(`Command: \`${process.command}\`\n${output}`)
144157
}
145158
}

src/core/tools/executeCommandTool.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as vscode from "vscode"
44

55
import delay from "delay"
66

7-
import { CommandExecutionStatus } from "@roo-code/types"
7+
import { CommandExecutionStatus, DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT } from "@roo-code/types"
88
import { TelemetryService } from "@roo-code/telemetry"
99

1010
import { Task } from "../task/Task"
@@ -63,7 +63,11 @@ export async function executeCommandTool(
6363
const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString()
6464
const clineProvider = await cline.providerRef.deref()
6565
const clineProviderState = await clineProvider?.getState()
66-
const { terminalOutputLineLimit = 500, terminalShellIntegrationDisabled = false } = clineProviderState ?? {}
66+
const {
67+
terminalOutputLineLimit = 500,
68+
terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
69+
terminalShellIntegrationDisabled = false,
70+
} = clineProviderState ?? {}
6771

6872
// Get command execution timeout from VSCode configuration (in seconds)
6973
const commandExecutionTimeoutSeconds = vscode.workspace
@@ -87,6 +91,7 @@ export async function executeCommandTool(
8791
customCwd,
8892
terminalShellIntegrationDisabled,
8993
terminalOutputLineLimit,
94+
terminalOutputCharacterLimit,
9095
commandExecutionTimeout,
9196
}
9297

@@ -133,6 +138,7 @@ export type ExecuteCommandOptions = {
133138
customCwd?: string
134139
terminalShellIntegrationDisabled?: boolean
135140
terminalOutputLineLimit?: number
141+
terminalOutputCharacterLimit?: number
136142
commandExecutionTimeout?: number
137143
}
138144

@@ -144,6 +150,7 @@ export async function executeCommand(
144150
customCwd,
145151
terminalShellIntegrationDisabled = false,
146152
terminalOutputLineLimit = 500,
153+
terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
147154
commandExecutionTimeout = 0,
148155
}: ExecuteCommandOptions,
149156
): Promise<[boolean, ToolResponse]> {
@@ -179,7 +186,11 @@ export async function executeCommand(
179186
const callbacks: RooTerminalCallbacks = {
180187
onLine: async (lines: string, process: RooTerminalProcess) => {
181188
accumulatedOutput += lines
182-
const compressedOutput = Terminal.compressTerminalOutput(accumulatedOutput, terminalOutputLineLimit)
189+
const compressedOutput = Terminal.compressTerminalOutput(
190+
accumulatedOutput,
191+
terminalOutputLineLimit,
192+
terminalOutputCharacterLimit,
193+
)
183194
const status: CommandExecutionStatus = { executionId, status: "output", output: compressedOutput }
184195
clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
185196

@@ -198,7 +209,11 @@ export async function executeCommand(
198209
} catch (_error) {}
199210
},
200211
onCompleted: (output: string | undefined) => {
201-
result = Terminal.compressTerminalOutput(output ?? "", terminalOutputLineLimit)
212+
result = Terminal.compressTerminalOutput(
213+
output ?? "",
214+
terminalOutputLineLimit,
215+
terminalOutputCharacterLimit,
216+
)
202217
cline.say("command_output", result)
203218
completed = true
204219
},

src/core/webview/ClineProvider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
openRouterDefaultModelId,
2929
glamaDefaultModelId,
3030
ORGANIZATION_ALLOW_ALL,
31+
DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
3132
} from "@roo-code/types"
3233
import { TelemetryService } from "@roo-code/telemetry"
3334
import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud"
@@ -1393,6 +1394,7 @@ export class ClineProvider
13931394
cachedChromeHostUrl,
13941395
writeDelayMs,
13951396
terminalOutputLineLimit,
1397+
terminalOutputCharacterLimit,
13961398
terminalShellIntegrationTimeout,
13971399
terminalShellIntegrationDisabled,
13981400
terminalCommandDelay,
@@ -1493,6 +1495,7 @@ export class ClineProvider
14931495
cachedChromeHostUrl: cachedChromeHostUrl,
14941496
writeDelayMs: writeDelayMs ?? DEFAULT_WRITE_DELAY_MS,
14951497
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
1498+
terminalOutputCharacterLimit: terminalOutputCharacterLimit ?? DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
14961499
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
14971500
terminalShellIntegrationDisabled: terminalShellIntegrationDisabled ?? false,
14981501
terminalCommandDelay: terminalCommandDelay ?? 0,
@@ -1662,6 +1665,8 @@ export class ClineProvider
16621665
fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
16631666
writeDelayMs: stateValues.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS,
16641667
terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
1668+
terminalOutputCharacterLimit:
1669+
stateValues.terminalOutputCharacterLimit ?? DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
16651670
terminalShellIntegrationTimeout:
16661671
stateValues.terminalShellIntegrationTimeout ?? Terminal.defaultShellIntegrationTimeout,
16671672
terminalShellIntegrationDisabled: stateValues.terminalShellIntegrationDisabled ?? false,

src/core/webview/webviewMessageHandler.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,8 +1049,29 @@ export const webviewMessageHandler = async (
10491049
await provider.postStateToWebview()
10501050
break
10511051
case "terminalOutputLineLimit":
1052-
await updateGlobalState("terminalOutputLineLimit", message.value)
1053-
await provider.postStateToWebview()
1052+
// Validate that the line limit is a positive number
1053+
const lineLimit = message.value
1054+
if (typeof lineLimit === "number" && lineLimit > 0) {
1055+
await updateGlobalState("terminalOutputLineLimit", lineLimit)
1056+
await provider.postStateToWebview()
1057+
} else {
1058+
vscode.window.showErrorMessage(
1059+
t("common:errors.invalid_line_limit") || "Terminal output line limit must be a positive number",
1060+
)
1061+
}
1062+
break
1063+
case "terminalOutputCharacterLimit":
1064+
// Validate that the character limit is a positive number
1065+
const charLimit = message.value
1066+
if (typeof charLimit === "number" && charLimit > 0) {
1067+
await updateGlobalState("terminalOutputCharacterLimit", charLimit)
1068+
await provider.postStateToWebview()
1069+
} else {
1070+
vscode.window.showErrorMessage(
1071+
t("common:errors.invalid_character_limit") ||
1072+
"Terminal output character limit must be a positive number",
1073+
)
1074+
}
10541075
break
10551076
case "terminalShellIntegrationTimeout":
10561077
await updateGlobalState("terminalShellIntegrationTimeout", message.value)

0 commit comments

Comments
 (0)