Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/terminal-output-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"roo-cline": minor
---

Add built-in terminal output filtering to reduce LLM token usage

Introduces a new `TerminalOutputFilter` module that applies command-aware output filtering before terminal output reaches the LLM context. This reduces token consumption by stripping noise (passing tests, progress bars, verbose logs) while preserving actionable information (errors, failures, summaries).

Built-in filters for common commands:

- **Test runners** (jest, vitest, mocha, pytest, cargo test, go test): Extract pass/fail summary + failure details
- **git status**: Compact file-change summary
- **git log**: One-line-per-commit format
- **Package managers** (npm, yarn, pnpm, pip): Strip progress/download noise, keep warnings + summary
- **Build tools** (tsc, cargo build, webpack, etc.): Strip progress, keep errors/warnings

New setting `terminalOutputFilterEnabled` (default: true) with toggle in Terminal Settings UI.
8 changes: 8 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ export const globalSettingsSchema = z.object({
maxTotalImageSize: z.number().optional(),

terminalOutputPreviewSize: z.enum(["small", "medium", "large"]).optional(),
/**
* Whether to enable command-aware output filtering for terminal output.
* When enabled, command output is semantically filtered before reaching the LLM,
* reducing token usage by stripping noise (passing tests, progress bars, verbose logs)
* while preserving actionable information (errors, failures, summaries).
* @default true
*/
terminalOutputFilterEnabled: z.boolean().optional(),
terminalShellIntegrationTimeout: z.number().optional(),
terminalShellIntegrationDisabled: z.boolean().optional(),
terminalCommandDelay: z.number().optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ export type ExtensionState = Pick<
| "soundVolume"
| "maxConcurrentFileReads"
| "terminalOutputPreviewSize"
| "terminalOutputFilterEnabled"
| "terminalShellIntegrationTimeout"
| "terminalShellIntegrationDisabled"
| "terminalCommandDelay"
Expand Down
30 changes: 28 additions & 2 deletions src/core/tools/ExecuteCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../..
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
import { Terminal } from "../../integrations/terminal/Terminal"
import { OutputInterceptor } from "../../integrations/terminal/OutputInterceptor"
import { filterTerminalOutput, formatFilterIndicator } from "../../integrations/terminal/TerminalOutputFilter"
import { Package } from "../../shared/package"
import { t } from "../../i18n"
import { getTaskDirectoryPath } from "../../utils/storage"
Expand Down Expand Up @@ -185,16 +186,19 @@ export async function executeCommandInTerminal(

const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode"
const provider = await task.providerRef.deref()
const providerState = await provider?.getState()

// Get global storage path for persisted output artifacts
const globalStoragePath = provider?.context?.globalStorageUri?.fsPath
let interceptor: OutputInterceptor | undefined

// Check if terminal output filtering is enabled (default: true)
const terminalOutputFilterEnabled = providerState?.terminalOutputFilterEnabled !== false

// Create OutputInterceptor if we have storage available
if (globalStoragePath) {
const taskDir = await getTaskDirectoryPath(globalStoragePath, task.taskId)
const storageDir = path.join(taskDir, "command-output")
const providerState = await provider?.getState()
const terminalOutputPreviewSize =
providerState?.terminalOutputPreviewSize ?? DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE

Expand Down Expand Up @@ -391,6 +395,17 @@ export async function executeCommandInTerminal(

// Use persisted output format when output was truncated and spilled to disk
if (persistedResult?.truncated) {
// Apply command-aware filtering to persisted preview if enabled
if (terminalOutputFilterEnabled) {
const filterResult = filterTerminalOutput(command, persistedResult.preview)
if (filterResult) {
const indicator = formatFilterIndicator(filterResult, true)
persistedResult = {
...persistedResult,
preview: filterResult.output + "\n\n" + indicator,
}
}
}
return [false, formatPersistedOutput(persistedResult, exitDetails, currentWorkingDir)]
}

Expand Down Expand Up @@ -419,9 +434,20 @@ export async function executeCommandInTerminal(
exitStatus = `Exit code: <undefined, notify user>`
}

// Apply command-aware output filtering for inline results if enabled
let outputForLlm = result
let filterIndicator = ""
if (terminalOutputFilterEnabled) {
const filterResult = filterTerminalOutput(command, result)
if (filterResult) {
outputForLlm = filterResult.output
filterIndicator = "\n" + formatFilterIndicator(filterResult, !!persistedResult?.artifactPath)
}
}

return [
false,
`Command executed in terminal within working directory '${currentWorkingDir}'. ${exitStatus}\nOutput:\n${result}`,
`Command executed in terminal within working directory '${currentWorkingDir}'. ${exitStatus}\nOutput:\n${outputForLlm}${filterIndicator}`,
]
} else {
return [
Expand Down
Loading
Loading