Skip to content

Conversation

@hannesrudolph
Copy link
Collaborator

@hannesrudolph hannesrudolph commented Jan 24, 2026

Summary

Refactors terminal command output handling to preserve full output losslessly. Instead of truncating large outputs and losing information, Roo now saves complete output to disk and provides the LLM with a preview plus the ability to retrieve the full content on demand.

Closes #10941

Problem

Users experienced several pain points with the previous terminal output handling:

  • Lost output from long builds/tests: When running npm install, cargo build, test suites, or other commands that produce significant output, important error messages at the end could get truncated away
  • Confusing settings: Two separate sliders for "line limit" and "character limit" were difficult to understand and tune correctly
  • No recovery option: Once output was truncated, the LLM had no way to retrieve the lost information, often leading to missed errors or incomplete debugging

Solution

This PR implements a "persisted output" pattern:

  1. Lossless preservation: All command output is captured. When output exceeds the preview threshold, it's automatically saved to disk in the task's storage directory
  2. Smart previews: The LLM receives a configurable preview (2KB, 4KB, or 8KB) plus metadata about the full output size
  3. On-demand retrieval: A new read_command_output tool lets the LLM fetch the complete output or search for specific patterns when needed
  4. Automatic cleanup: Artifacts are cleaned up when tasks complete or when conversation history is rewound

User-Facing Changes

Settings

Before: Two confusing sliders

  • "Terminal Output Line Limit" (slider)
  • "Terminal Output Character Limit" (slider)

After: One simple dropdown

  • "Command output preview size" with options: Small (2KB), Medium (4KB), Large (8KB)

The new setting controls how much output Roo sees directly in the preview. Full output is always preserved and accessible.

How It Works

  1. You run a command (e.g., npm test)
  2. If output is small (under the preview threshold), it works exactly as before
  3. If output is large:
    • Full output is saved to a file in Roo's task storage
    • Roo sees a preview with a note about the full size
    • Roo can use read_command_output to view the full output or search for specific errors

Benefits

  • Lossless: Full output is always preserved—no more missing error messages from long builds
  • Searchable: Roo can search for specific patterns (like "error" or "failed") in large outputs
  • Simplified settings: One easy dropdown instead of two confusing sliders
  • Automatic cleanup: Artifacts are automatically cleaned up when tasks complete
  • Predictable context usage: Preview size is fixed and predictable, making context management easier

Technical Changes

  • New OutputInterceptor class: Buffers output and spills to disk when threshold exceeded
  • New read_command_output tool: Allows reading full output or searching for patterns
  • Updated ExecuteCommandTool: Uses the new interceptor pattern
  • Lifecycle management: Artifacts tied to conversation history, cleaned up on rewind/delete
  • Settings migration: Old slider settings replaced with new dropdown

Testing

  • 49 new tests covering the new functionality
  • Unit tests for OutputInterceptor (buffering, spilling, cleanup)
  • Unit tests for ReadCommandOutputTool (read, search, error cases)
  • Integration tests for the full flow
  • Manual testing of various scenarios (small output, large output, progress bars, errors)

…utput

Implements a new tool that allows the LLM to retrieve full command output
when execute_command produces output exceeding the preview threshold.

Key components:
- ReadCommandOutputTool: Reads persisted output with search/pagination
- OutputInterceptor: Intercepts and persists large command outputs to disk
- Terminal settings UI: Configuration for output interception behavior
- Type definitions for output interception settings

The tool supports:
- Reading full output beyond the truncated preview
- Search/filtering with regex patterns (like grep)
- Pagination through large outputs using offset/limit

Includes comprehensive tests for ReadCommandOutputTool and OutputInterceptor.
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. Enhancement New feature or request labels Jan 24, 2026
@roomote
Copy link
Contributor

roomote bot commented Jan 24, 2026

Oroocle Clock   See task on Roo Cloud

Re-review complete. No new issues found since 6c1bfe6; everything previously flagged remains resolved.

  • execute_command should respect terminalOutputPreviewSize
  • read_command_output native tool schema should not require optional params (search/offset/limit) under strict mode
  • Avoid allocating Buffer(offset) in read_command_output for large offsets (line-number calculation can blow up memory)
  • Bound accumulatedOutput growth in execute_command to avoid unbounded memory usage during long-running commands
Previous reviews

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

Comment on lines 211 to 214
// Use default preview size for now (terminalOutputPreviewSize setting will be added in Phase 5)
const terminalOutputPreviewSize = DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE
const providerState = await provider?.getState()
const terminalCompressProgressBar = providerState?.terminalCompressProgressBar ?? true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

terminalOutputPreviewSize is exposed in settings/types, but execute_command currently always uses DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE, so user configuration has no effect.

Suggested change
// Use default preview size for now (terminalOutputPreviewSize setting will be added in Phase 5)
const terminalOutputPreviewSize = DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE
const providerState = await provider?.getState()
const terminalCompressProgressBar = providerState?.terminalCompressProgressBar ?? true
const providerState = await provider?.getState()
const terminalOutputPreviewSize = providerState?.terminalOutputPreviewSize ?? DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE
const terminalCompressProgressBar = providerState?.terminalCompressProgressBar ?? true

Fix it with Roo Code or mention @roomote and request a fix.

description: LIMIT_DESCRIPTION,
},
},
required: ["artifact_id", "search", "offset", "limit"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The native tool schema marks search/offset/limit as required, but the implementation treats them as optional. With strict=true, tool calls that omit those fields will fail schema validation before reaching the tool.

Suggested change
required: ["artifact_id", "search", "offset", "limit"],
required: ["artifact_id"],

Fix it with Roo Code or mention @roomote and request a fix.

Comment on lines 226 to 234
// Calculate line numbers based on offset
let startLineNumber = 1
if (offset > 0) {
// Count newlines before offset to determine starting line number
const prefixBuffer = Buffer.alloc(offset)
await fileHandle.read(prefixBuffer, 0, offset, 0)
const prefix = prefixBuffer.toString("utf8")
startLineNumber = (prefix.match(/\n/g) || []).length + 1
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readArtifact() allocates a Buffer of size offset to count newlines, which can be huge for large outputs and defeats the goal of keeping memory usage bounded. Consider streaming/iterating to compute the start line number, or dropping line numbers in paginated reads (or only adding them when offset is small).

Fix it with Roo Code or mention @roomote and request a fix.

@hannesrudolph hannesrudolph changed the title feat: add read_command_output tool for retrieving truncated command output feat: lossless terminal output with on-demand retrieval Jan 24, 2026
@roomote
Copy link
Contributor

roomote bot commented Jan 24, 2026

Oroocle Clock   Follow along on Roo Cloud

Flagged a few correctness and contract issues to address before merge.

  • execute_command should respect terminalOutputPreviewSize (currently hardcoded to default)
  • read_command_output native tool schema should not require optional params (search/offset/limit) under strict mode
  • Avoid allocating Buffer(offset) in read_command_output for large offsets (line-number calculation can blow up memory)

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

const taskDir = await getTaskDirectoryPath(globalStoragePath, task.taskId)
const storageDir = path.join(taskDir, "command-output")
// Use default preview size for now (terminalOutputPreviewSize setting will be added in Phase 5)
const terminalOutputPreviewSize = DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute_command currently hardcodes previewSize to DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE, so user-configured terminalOutputPreviewSize has no effect. Pull this from provider state when available so the dropdown actually controls spill threshold.

Suggested change
const terminalOutputPreviewSize = DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE
const providerState = await provider?.getState()
const terminalOutputPreviewSize =
providerState?.terminalOutputPreviewSize ?? DEFAULT_TERMINAL_OUTPUT_PREVIEW_SIZE
const terminalCompressProgressBar = providerState?.terminalCompressProgressBar ?? true

Fix it with Roo Code or mention @roomote and request a fix.

description: LIMIT_DESCRIPTION,
},
},
required: ["artifact_id", "search", "offset", "limit"],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With strict=true, marking search/offset/limit as required means tool calls that omit those fields will fail schema validation before reaching the implementation (which treats them as optional). Make only artifact_id required.

Suggested change
required: ["artifact_id", "search", "offset", "limit"],
required: ["artifact_id"],

Fix it with Roo Code or mention @roomote and request a fix.

Comment on lines 226 to 234
// Calculate line numbers based on offset
let startLineNumber = 1
if (offset > 0) {
// Count newlines before offset to determine starting line number
const prefixBuffer = Buffer.alloc(offset)
await fileHandle.read(prefixBuffer, 0, offset, 0)
const prefix = prefixBuffer.toString("utf8")
startLineNumber = (prefix.match(/\n/g) || []).length + 1
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read_command_output calculates startLineNumber by allocating Buffer(offset) and reading the entire prefix, which can be huge for large offsets and defeats the bounded-memory goal. Consider dropping line numbers when offset is large, or count newlines in fixed-size chunks up to offset.

Fix it with Roo Code or mention @roomote and request a fix.

- Read terminalOutputPreviewSize from providerState instead of hardcoded default
- Fix native tool schema to only require artifact_id (optional params no longer required)
- Fix Buffer allocation for line numbers using chunked 64KB reads to avoid memory blowup
let accumulatedOutput = ""
const callbacks: RooTerminalCallbacks = {
onLine: async (lines: string, process: RooTerminalProcess) => {
accumulatedOutput += lines
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accumulatedOutput grows without bound as output streams in, so a long-running command can still drive memory usage arbitrarily high even though the UI only needs a truncated view. Capping the in-memory accumulator (for UI status updates) to terminalOutputCharacterLimit keeps memory bounded while OutputInterceptor still preserves the full output on disk.

Suggested change
accumulatedOutput += lines
accumulatedOutput += lines
if (accumulatedOutput.length > terminalOutputCharacterLimit) {
accumulatedOutput = accumulatedOutput.slice(-terminalOutputCharacterLimit)
}

Fix it with Roo Code or mention @roomote and request a fix.

Prevent unbounded memory growth during long-running commands by trimming
the accumulated output buffer. The full output is preserved by the
OutputInterceptor; this buffer is only used for UI display.
- Remove unused barrel file (src/integrations/terminal/index.ts) to fix knip check
- Fix Windows path test in OutputInterceptor.test.ts by using path.normalize()
- Add missing translations for terminal.outputPreviewSize settings to all 17 locales
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

Terminal Output Handling Refactor: Persisted Output Pattern

2 participants