Skip to content

Comments

feat: Add sandbox isolation for agent execution#99

Merged
subsy merged 73 commits intomainfrom
feat/sandboxes
Jan 16, 2026
Merged

feat: Add sandbox isolation for agent execution#99
subsy merged 73 commits intomainfrom
feat/sandboxes

Conversation

@subsy
Copy link
Owner

@subsy subsy commented Jan 16, 2026

Summary

Add sandboxing support to isolate agent execution, protecting the host system from unintended modifications while still allowing agents to complete their tasks.

Key Features

  • Linux (bwrap): Uses bubblewrap for lightweight container-like isolation
  • macOS (sandbox-exec): Uses Apple's Seatbelt sandboxing with generated profiles
  • Auto-detection: Automatically selects the appropriate sandbox mode for the platform
  • Agent-declared requirements: Each agent declares its auth paths, binary paths, and runtime paths

Sandbox Modes

Mode Description
auto Auto-detect best available sandbox (default)
bwrap Force bubblewrap (Linux)
sandbox-exec Force sandbox-exec (macOS)
off Disable sandboxing

CLI Usage

# Run with auto-detected sandbox
ralph run --sandbox

# Force specific mode
ralph run --sandbox=bwrap

# Disable network in sandbox
ralph run --sandbox --no-network

Configuration (ralph.config.ts)

sandbox: {
  enabled: true,
  mode: 'auto',
  network: true,
  allowPaths: ['./output'],
  readOnlyPaths: ['~/.config/shared'],
}

Agent Requirements

Each agent declares what it needs to function in a sandbox:

  • authPaths (read-write): Credentials, OAuth tokens that need refresh
  • binaryPaths (read-only): Where the CLI binary is installed
  • runtimePaths (read-only): Runtime dependencies, caches

TUI Integration

  • Header shows sandbox status indicator
  • Dashboard (d) shows sandbox mode with resolved value (e.g., "auto (bwrap)")
  • Iteration detail view (o) shows full sandbox configuration

Changes

  • Add sandbox wrapper with bwrap and sandbox-exec support
  • Add sandbox detection for platform-appropriate mode selection
  • Add CLI flags: --sandbox, --no-network
  • Add sandbox config schema and defaults
  • Add getSandboxRequirements() to all agent plugins (claude, opencode, droid)
  • Mount authPaths as read-write (for OAuth token refresh)
  • Skip non-existent paths gracefully
  • Display sandbox status in TUI header, dashboard, and detail views
  • Add comprehensive documentation

Test plan

  • Typecheck passes
  • Build succeeds
  • Manual test: ralph run --sandbox with opencode agent (OAuth)
  • Manual test: ralph run --sandbox with claude agent
  • Manual test: ralph run --sandbox --no-network shows warning for network-requiring agents

Summary by CodeRabbit

  • New Features

    • Sandboxing for agent runs (Linux/macOS) with CLI flags (--sandbox, --no-sandbox, --no-network)
    • Gemini agent integration and expanded plugin/subagent tracing
    • Segment-based streaming output for richer, colourised live display
  • Improvements

    • Sandbox status shown in the UI header and detail views
    • Better JSONL/agent output parsing and more reliable streaming behaviour
  • Documentation

    • New sandbox configuration guide with platform notes and examples

✏️ Tip: You can customize this high-level summary in your review settings.

subsy added 26 commits January 15, 2026 23:39
- Add engine:warning event type for configuration warnings
- Emit warning when sandbox network disabled but agent requires network
- Handle warning in both TUI (stderr) and headless (structured logger) modes
- Update sandbox.mdx docs with network access considerations
- Add sandboxConfig to HeaderProps and RunAppProps interfaces
- Display 🔒 indicator with sandbox mode (bwrap/docker/auto)
- Show (no-net) suffix when network is disabled
- Pass sandbox config through RunAppWrapper → RunApp → Header chain
Docker sandboxing had fundamental issues: required pre-built images
with agents installed, auth/API key complexities across container
boundaries, and maintenance burden. bwrap provides simpler, more
reliable sandboxing by mounting host binaries directly.

- Remove 'docker' from SandboxMode type
- Remove 'image' field from SandboxConfig
- Remove wrapWithDocker() method from wrapper.ts
- Remove Docker detection from detect.ts
- Remove --sandbox=docker CLI flag
- Update Zod schema validation
Native macOS sandboxing using sandbox-exec with dynamically generated
Seatbelt profiles. Provides filesystem and network isolation similar
to bwrap on Linux.

- Add 'sandbox-exec' to SandboxMode type
- Add wrapWithSandboxExec() method with Seatbelt profile generation
- Update detectSandboxMode() to detect macOS and use sandbox-exec
- Add --sandbox=sandbox-exec CLI flag
- Profile includes system dirs, temp dirs, working dir, auth paths
- Network access controllable via config (same as bwrap)
Major documentation rewrite for the sandbox refactor:
- Remove all Docker sandbox references (mode, image, CLI flags)
- Add macOS sandbox-exec section with Seatbelt profile explanation
- Add Windows/WSL2 guidance section with setup steps
- Update platform requirements table (Linux/macOS/Windows)
- Update sandbox modes table (auto, bwrap, sandbox-exec, off)
- Update CLI flags to reflect available options
- Update examples to show platform-specific configurations
- Note sandbox-exec deprecation warning (expected, still works)
The normalizePaths() function was treating ~/path as a relative path,
resolving it to cwd/~/path instead of /home/user/path. This caused
bwrap to fail with "Can't find source path" errors when agent plugins
specified auth paths like ~/.opencode or ~/.claude.

Now properly expands ~/ to the user's home directory before resolving.
- ProgressDashboard (d key): Shows sandbox mode alongside agent/tracker
  (e.g., "Sandbox: bwrap" or "Sandbox: sandbox-exec (no-net)")
- IterationDetailView (o key): Adds dedicated "Sandbox Configuration"
  section showing mode, network access, and configured paths
- Helps users understand sandbox state during execution
When sandbox mode is 'auto', now displays the resolved mode in
parentheses (e.g., "auto (bwrap)" or "auto (sandbox-exec)") in:
- Progress Dashboard (d key view)
- Iteration Detail View (o key view)
- Header status bar

Detects actual sandbox mode at startup and passes through component
chain for accurate display.
Add existsSync checks for both read-write and read-only paths in
wrapWithBwrap. This prevents bwrap from failing when agent sandbox
requirements specify paths that don't exist on the system (e.g.,
~/go/bin for opencode agent when Go isn't installed).

This matches the behavior already present in sandbox-exec (macOS)
where existence is checked before adding paths to the profile.
Auth paths (like ~/.opencode, ~/.config/opencode) need write access
so agents can refresh OAuth tokens. Previously they were mounted
read-only, which broke OAuth-based authentication.

Now:
- authPaths → read-write (for token refresh)
- binaryPaths → read-only
- runtimePaths → read-only

This applies to both bwrap (Linux) and sandbox-exec (macOS).
Add getSandboxRequirements() override for Factory Droid agent with:
- authPaths: ~/.droid, ~/.config/droid, ~/.config/gcloud (for auth/tokens)
- binaryPaths: /usr/local/bin, ~/.local/bin
- requiresNetwork: true

Without this, droid would have no auth paths mounted in sandboxed mode,
causing authentication failures.
@vercel
Copy link

vercel bot commented Jan 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Review Updated (UTC)
ralph-tui Ignored Ignored Preview Jan 16, 2026 9:45pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

Walkthrough

Adds sandbox support and segment-based agent output to Ralph TUI: new sandbox types, detection, and wrappers; threads sandbox state through CLI, engine, plugins and TUI; introduces event/segment output formatting and streaming; and adds tests, docs and example utilities.

Changes

Cohort / File(s) Summary
Sandbox core
src/sandbox/types.ts, src/sandbox/detect.ts, src/sandbox/wrapper.ts, src/sandbox/index.ts
New sandbox API: types and re-exports, runtime detection (bwrap / sandbox-exec / off), command-wrapping for bwrap and macOS seatbelt profiles, path normalisation and wrapper utilities.
Config & schema
src/config/types.ts, src/config/schema.ts, src/config/index.ts, src/config/types.test.ts
New SandboxConfig/SandboxMode types and defaults; schema additions; merging of project/global sandbox config; DEFAULT_SANDBOX_CONFIG exported and tested.
CLI / command wiring
src/cli.tsx, src/commands/run.tsx
New CLI flags for sandboxing and network control; runtime sandbox detection; resolve and thread sandboxConfig and resolvedSandboxMode into RunAppWrapper/RunApp.
Engine & events
src/engine/index.ts, src/engine/types.ts
Engine now propagates sandbox settings into agent execution and emits engine:warning events (e.g., sandbox-network-conflict).
Agent plugin framework
src/plugins/agents/types.ts, src/plugins/agents/base.ts
AgentExecuteOptions gains sandbox and onStdoutSegments; AgentSandboxRequirements type added; base plugin supports getSandboxRequirements() and execution path updated to use SandboxWrapper and lifecycle/event hooks.
Agent implementations & parsers
src/plugins/agents/builtin/claude.ts, src/plugins/agents/builtin/opencode.ts, src/plugins/agents/droid/*, src/plugins/agents/droid/outputParser.ts
Plugins declare sandbox requirements and adopt JSONL/event parsing; opencode/claude/droid moved to event-based parsing and produce AgentDisplayEvent items.
Output formatting / segments
src/plugins/agents/output-formatting.ts, src/plugins/agents/output-formatting.test.ts
New event & segment model (AgentDisplayEvent, FormattedSegment), formatting helpers, ANSI stripping, and conversion to plain text or colourised segments; comprehensive tests added.
TUI: components & types
src/tui/components/Header.tsx, src/tui/components/ProgressDashboard.tsx, src/tui/components/IterationDetailView.tsx, src/tui/components/RunApp.tsx, src/tui/types.ts
Thread sandboxConfig/resolvedSandboxMode through UI; display sandbox status in header/dashboard; iteration detail shows sandbox configuration.
TUI: segment rendering & parser
src/tui/components/FormattedText.tsx, src/tui/components/ChatView.tsx, src/tui/components/PrdChatApp.tsx, src/tui/components/RightPanel.tsx, src/tui/output-parser.ts, src/tui/output-parser.test.ts
New FormattedText component; StreamingOutputParser collects FormattedSegment arrays; chat and right-panel components accept and render segments with legacy text fallback; tests added.
Chat & engine integration
src/chat/types.ts, src/chat/engine.ts
SendMessageOptions gains onSegments callback; chat engine forwards segment streams from agent execution.
Tests & CI
src/sandbox/*.test.ts, src/plugins/agents/*.test.ts, .github/workflows/ci.yml, codecov.yml, others
Extensive unit tests for sandbox detection/wrapping, output formatting and parsers; CI change to use pipefail; codecov ignore additions for TUI files.
Docs, examples & misc
website/content/docs/configuration/sandbox.mdx, website/lib/navigation.ts, README.md, .gitignore, examples/*
Sandbox documentation and navigation entry; README quick-start note; added example utility modules and updated .gitignore for coverage/.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI
  participant RunCmd as Run Command
  participant Engine
  participant Sandbox as SandboxWrapper
  participant Agent
  participant TUI

  User->>CLI: invoke ralph-tui run --sandbox[=mode] / options
  CLI->>RunCmd: parse options (sandbox, no-network)
  RunCmd->>Sandbox: detectSandboxMode() (if mode auto)
  RunCmd->>Engine: start run (include sandboxConfig, resolvedSandboxMode)
  Engine->>Agent: execute(..., { sandbox: sandboxConfig, onStdoutSegments })
  Agent->>Sandbox: if sandbox.enabled -> wrapCommand(command,args)
  Sandbox-->>Agent: WrappedCommand (bwrap or sandbox-exec or original)
  Agent->>Agent: spawn process (wrapped command)
  Agent->>Engine: emit stdout chunks and segments via onStdout/onStdoutSegments
  Engine->>TUI: forward events/warnings and streaming segments
  TUI->>TUI: StreamingOutputParser accumulates segments
  TUI->>User: render formatted segments via FormattedText
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hop and tuck each command so tight,

with bwrap shells and seatbelt light,
Agents safe, their colours beam,
Segments streaming like a dream;
A rabbit cheers—sandbox lands just right!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: Add sandbox isolation for agent execution' directly and clearly summarizes the main change: adding sandboxing capabilities to isolate agent execution. The title is specific, concise, and accurately reflects the primary objective.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Jan 16, 2026

Codecov Report

❌ Patch coverage is 53.43511% with 488 lines in your changes missing coverage. Please review.
✅ Project coverage is 40.36%. Comparing base (66c82ba) to head (25cf0d1).
⚠️ Report is 74 commits behind head on main.

Files with missing lines Patch % Lines
src/plugins/agents/base.ts 1.24% 159 Missing ⚠️
src/plugins/agents/builtin/claude.ts 4.23% 113 Missing ⚠️
src/plugins/agents/builtin/opencode.ts 5.05% 94 Missing ⚠️
src/commands/run.tsx 7.84% 47 Missing ⚠️
src/plugins/agents/output-formatting.ts 78.21% 44 Missing ⚠️
src/plugins/agents/droid/index.ts 11.76% 15 Missing ⚠️
src/config/index.ts 33.33% 8 Missing ⚠️
src/sandbox/detect.ts 82.05% 7 Missing ⚠️
src/tui/output-parser.ts 98.79% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main      #99       +/-   ##
===========================================
+ Coverage   26.68%   40.36%   +13.68%     
===========================================
  Files          69       57       -12     
  Lines       16559    12626     -3933     
===========================================
+ Hits         4418     5096      +678     
+ Misses      12141     7530     -4611     
Files with missing lines Coverage Δ
src/config/schema.ts 100.00% <100.00%> (ø)
src/config/types.ts 100.00% <100.00%> (ø)
src/engine/index.ts 65.04% <100.00%> (+0.40%) ⬆️
src/engine/types.ts 100.00% <ø> (ø)
src/plugins/agents/droid/outputParser.ts 67.86% <100.00%> (+53.31%) ⬆️
src/sandbox/index.ts 100.00% <100.00%> (ø)
src/sandbox/wrapper.ts 100.00% <100.00%> (ø)
src/tui/output-parser.ts 99.66% <98.79%> (+10.96%) ⬆️
src/sandbox/detect.ts 82.05% <82.05%> (ø)
src/config/index.ts 39.06% <33.33%> (-0.37%) ⬇️
... and 6 more

... and 17 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@README.md`:
- Around line 112-114: Update the README entry for "ralph-tui run --sandbox" to
remove references to Docker and instead document the actual supported sandbox
backends: Bubblewrap (bwrap) on Linux and sandbox-exec on macOS; explicitly
state any backend-specific requirements (e.g., bwrap must be installed and
available on PATH) and give the single command example "ralph-tui run --sandbox"
along with a short note that the implementation uses bwrap on Linux and
sandbox-exec on macOS. Ensure all Docker mentions are removed or replaced and
that the documented sandbox backends match the PR summary and actual code paths
that implement sandboxing.

In `@src/sandbox/wrapper.ts`:
- Around line 215-223: In normalizePaths update the "~" expansion logic so that
a bare "~" is expanded to the user's home directory: inside the map for each
path (in function normalizePaths) check if path === '~' then set expanded =
homedir(), else if path.startsWith('~/') replace the leading '~' with homedir(),
otherwise leave unchanged; then continue using isAbsolute(expanded) ? expanded :
resolve(cwd, expanded). This ensures both "~" and "~/..." are handled correctly
while keeping use of homedir(), resolve, and isAbsolute in the same function.
- Around line 176-208: The Seatbelt profile generation is vulnerable because
paths interpolated into lines.push strings (e.g., in the working directory
block, allowPaths loop, authPaths loop, and readOnlyPaths loop within the
wrapper generating code) are not escaped; fix by escaping special characters
(quotes, backslashes, newlines) in every path returned from normalizePaths
before embedding into `(subpath "...")` strings—add a small helper (e.g.,
escapeSeatbeltPath or similar) and call it wherever paths are pushed into lines
(including use with workDir, allowPaths, authPaths, readOnlyPaths) so generated
profile strings are syntactically safe and cannot be injection-attacked.

In `@website/content/docs/configuration/sandbox.mdx`:
- Around line 67-68: The documentation line claiming "Agent auth paths (e.g.,
`~/.claude`) are mounted read-only" is incorrect; update the phrasing to state
that authPaths are mounted read-write to match the implementation that allows
OAuth token refresh (refer to the authPaths configuration and the example
`~/.claude` reference) and ensure the text explicitly mentions read-write
mounting so it aligns with the PR summary and actual behavior.
🧹 Nitpick comments (10)
src/tui/components/Header.tsx (1)

130-148: Consider using resolvedSandboxMode when mode is 'auto'.

The HeaderProps interface includes a resolvedSandboxMode prop (as seen in src/tui/types.ts:103-104) to show what 'auto' resolved to, but this function doesn't accept or use it. When sandbox mode is 'auto', displaying "auto" may be less informative than showing the actual resolved backend (e.g., "bwrap" or "sandbox-exec").

♻️ Suggested improvement
 function getSandboxDisplay(
-  sandboxConfig: HeaderProps['sandboxConfig']
+  sandboxConfig: HeaderProps['sandboxConfig'],
+  resolvedSandboxMode?: Exclude<import('../types.js').SandboxMode, 'auto'>
 ): string | null {
   if (!sandboxConfig?.enabled) {
     return null;
   }

   const mode = sandboxConfig.mode ?? 'auto';
   if (mode === 'off') {
     return null;
   }

+  // Show resolved mode when 'auto', otherwise show configured mode
+  const displayMode = mode === 'auto' && resolvedSandboxMode ? resolvedSandboxMode : mode;
   const networkSuffix = sandboxConfig.network === false ? ' (no-net)' : '';
-  return `${mode}${networkSuffix}`;
+  return `${displayMode}${networkSuffix}`;
 }

Then update the call site:

-  const sandboxDisplay = getSandboxDisplay(sandboxConfig);
+  const sandboxDisplay = getSandboxDisplay(sandboxConfig, resolvedSandboxMode);

And add resolvedSandboxMode to the destructured props.

src/plugins/agents/types.ts (3)

104-105: Add JSDoc comment for the sandbox property.

Other properties in AgentExecuteOptions have descriptive comments. Adding documentation here would maintain consistency and clarify the property's purpose.

📝 Suggested documentation
+  /** Sandbox configuration for isolated execution */
   sandbox?: SandboxConfig;

210-215: Add JSDoc documentation for AgentSandboxRequirements.

The interface lacks documentation explaining its purpose and the semantics of each property. This would help implementers understand what paths should go where (e.g., authPaths are mounted read-write for token refresh, binaryPaths and runtimePaths are read-only).

📝 Suggested documentation
+/**
+ * Sandbox requirements declared by an agent plugin.
+ * These paths are mounted into the sandbox to allow the agent to function.
+ */
 export interface AgentSandboxRequirements {
+  /** Paths requiring read-write access (e.g., for OAuth token refresh) */
   authPaths: string[];
+  /** Paths containing binaries the agent needs (mounted read-only) */
   binaryPaths: string[];
+  /** Additional runtime paths the agent needs (mounted read-only) */
   runtimePaths: string[];
+  /** Whether the agent requires network access for API calls */
   requiresNetwork: boolean;
 }

300-301: Add JSDoc documentation for getSandboxRequirements.

Other methods in the AgentPlugin interface have descriptive JSDoc comments. Adding documentation here would maintain consistency.

📝 Suggested documentation
+  /**
+   * Get sandbox requirements for this agent.
+   * Declares paths and capabilities needed when running in a sandboxed environment.
+   * `@returns` Sandbox requirements including auth paths, binary paths, and network needs
+   */
   getSandboxRequirements(): AgentSandboxRequirements;
src/tui/components/ProgressDashboard.tsx (1)

61-64: Consider extracting shared sandbox display logic.

The mode display logic (auto (${resolvedSandboxMode})) is duplicated in IterationDetailView.tsx (lines 703-706). Consider extracting a shared helper function to ensure consistent formatting across components.

src/tui/components/IterationDetailView.tsx (1)

715-728: Consider improving path list display for readability.

When allowPaths or readOnlyPaths contain many entries, the comma-separated display could become unwieldy. Consider displaying each path on a separate line or truncating with a count indicator (e.g., "3 paths configured") with expansion on demand.

src/config/index.ts (1)

166-168: Shallow merge may not handle array properties as expected.

The spread operator performs a shallow merge. If both merged.sandbox and project.sandbox contain allowPaths or readOnlyPaths arrays, the project's arrays will completely replace the global ones rather than merging them. This is consistent with how other arrays are handled in this function (see lines 152-154), but worth noting that users cannot extend global path lists at the project level—they must re-specify all paths.

If this is the intended behaviour, consider adding a comment to clarify, similar to line 152's comment about arrays being replaced.

src/config/types.ts (1)

253-254: Consider making sandbox required on RalphConfig.

Since buildConfig in src/config/index.ts always constructs a sandbox object (lines 531-535), the property will never be undefined at runtime. Making it sandbox: SandboxConfig (required) would provide better type safety for consumers and eliminate unnecessary optional chaining in downstream code.

However, this is a minor refinement and the current optional approach is safe.

🔧 Suggested change
-  sandbox?: SandboxConfig;
+  sandbox: SandboxConfig;
src/plugins/agents/base.ts (2)

400-412: Minor: resolveSandboxConfig always returns undefined when sandbox is disabled.

The function returns undefined when sandboxConfig?.enabled is falsy, but this includes the case where sandboxConfig exists with enabled: false. This is correct behaviour, but the naming could be clearer. Consider adding a brief comment to clarify that returning undefined means "no sandboxing".


380-393: Timeout cleanup relies on completeExecution being called.

The timeout handler checks this.executions.has(executionId) before killing, which works because completeExecution removes the execution from the map. However, if the process completes normally before the timeout fires, the execution.timeoutId is cleared in completeExecution (line 486-488). This is correct.

The nested setTimeout for the SIGKILL follow-up (lines 386-390) could potentially fire after the execution is already completed if there's a race. The check on line 387 guards against this, but consider storing this inner timeout ID as well for explicit cleanup.

🔧 Suggested improvement for explicit cleanup
+      let killTimeoutId: ReturnType<typeof setTimeout> | undefined;
       if (timeout > 0) {
         execution.timeoutId = setTimeout(() => {
           if (this.executions.has(executionId)) {
             proc.kill('SIGTERM');
             // Give it 5 seconds to terminate gracefully
-            setTimeout(() => {
+            killTimeoutId = setTimeout(() => {
               if (this.executions.has(executionId)) {
                 proc.kill('SIGKILL');
               }
             }, 5000);
           }
         }, timeout);
       }

Then in completeExecution, clear both timeouts.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cdf556b and 0f305dc.

📒 Files selected for processing (26)
  • .beads/issues.jsonl
  • README.md
  • src/cli.tsx
  • src/commands/run.tsx
  • src/config/index.ts
  • src/config/schema.ts
  • src/config/types.test.ts
  • src/config/types.ts
  • src/engine/index.ts
  • src/engine/types.ts
  • src/plugins/agents/base.ts
  • src/plugins/agents/builtin/claude.ts
  • src/plugins/agents/builtin/opencode.ts
  • src/plugins/agents/droid/index.ts
  • src/plugins/agents/types.ts
  • src/sandbox/detect.ts
  • src/sandbox/index.ts
  • src/sandbox/types.ts
  • src/sandbox/wrapper.ts
  • src/tui/components/Header.tsx
  • src/tui/components/IterationDetailView.tsx
  • src/tui/components/ProgressDashboard.tsx
  • src/tui/components/RunApp.tsx
  • src/tui/types.ts
  • website/content/docs/configuration/sandbox.mdx
  • website/lib/navigation.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Start all code files with a file-level JSDoc comment section explaining the file's purpose, prefixed with 'ABOUTME: '

Files:

  • src/plugins/agents/droid/index.ts
  • src/engine/types.ts
  • src/sandbox/types.ts
  • src/cli.tsx
  • src/config/schema.ts
  • src/config/types.test.ts
  • src/engine/index.ts
  • src/plugins/agents/builtin/claude.ts
  • src/tui/components/RunApp.tsx
  • src/sandbox/index.ts
  • src/plugins/agents/builtin/opencode.ts
  • src/tui/components/IterationDetailView.tsx
  • website/lib/navigation.ts
  • src/sandbox/detect.ts
  • src/tui/types.ts
  • src/plugins/agents/base.ts
  • src/config/types.ts
  • src/tui/components/ProgressDashboard.tsx
  • src/commands/run.tsx
  • src/plugins/agents/types.ts
  • src/tui/components/Header.tsx
  • src/sandbox/wrapper.ts
  • src/config/index.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Start all code files with a file-level JSDoc comment section explaining the file's purpose, prefixed with "ABOUTME: "

Files:

  • src/plugins/agents/droid/index.ts
  • src/engine/types.ts
  • src/sandbox/types.ts
  • src/cli.tsx
  • src/config/schema.ts
  • src/config/types.test.ts
  • src/engine/index.ts
  • src/plugins/agents/builtin/claude.ts
  • src/tui/components/RunApp.tsx
  • src/sandbox/index.ts
  • src/plugins/agents/builtin/opencode.ts
  • src/tui/components/IterationDetailView.tsx
  • website/lib/navigation.ts
  • src/sandbox/detect.ts
  • src/tui/types.ts
  • src/plugins/agents/base.ts
  • src/config/types.ts
  • src/tui/components/ProgressDashboard.tsx
  • src/commands/run.tsx
  • src/plugins/agents/types.ts
  • src/tui/components/Header.tsx
  • src/sandbox/wrapper.ts
  • src/config/index.ts
🧬 Code graph analysis (12)
src/config/types.test.ts (2)
src/config/types.ts (2)
  • DEFAULT_SANDBOX_CONFIG (75-81)
  • DEFAULT_CONFIG (286-295)
src/config/index.ts (2)
  • DEFAULT_SANDBOX_CONFIG (657-657)
  • DEFAULT_CONFIG (657-657)
src/tui/components/RunApp.tsx (1)
src/config/types.ts (2)
  • SandboxConfig (67-73)
  • SandboxMode (65-65)
src/tui/components/IterationDetailView.tsx (1)
src/tui/theme.ts (1)
  • colors (9-58)
src/sandbox/detect.ts (2)
src/config/types.ts (1)
  • SandboxMode (65-65)
src/sandbox/types.ts (1)
  • SandboxMode (8-8)
src/tui/types.ts (1)
src/config/types.ts (2)
  • SandboxConfig (67-73)
  • SandboxMode (65-65)
src/plugins/agents/base.ts (6)
src/plugins/agents/types.ts (2)
  • AgentSandboxRequirements (210-215)
  • AgentExecutionStatus (46-51)
src/logs/structured-logger.ts (1)
  • error (145-147)
src/plugins/agents/index.ts (1)
  • AgentExecutionStatus (15-15)
src/config/types.ts (1)
  • SandboxConfig (67-73)
src/sandbox/detect.ts (1)
  • detectSandboxMode (47-63)
src/sandbox/wrapper.ts (1)
  • SandboxWrapper (33-226)
src/tui/components/ProgressDashboard.tsx (1)
src/tui/theme.ts (1)
  • colors (9-58)
src/commands/run.tsx (2)
src/config/types.ts (2)
  • SandboxConfig (67-73)
  • SandboxMode (65-65)
src/sandbox/detect.ts (1)
  • detectSandboxMode (47-63)
src/plugins/agents/types.ts (1)
src/config/types.ts (1)
  • SandboxConfig (67-73)
src/tui/components/Header.tsx (2)
src/tui/types.ts (1)
  • HeaderProps (76-107)
src/tui/theme.ts (1)
  • colors (9-58)
src/sandbox/wrapper.ts (2)
src/config/types.ts (1)
  • SandboxConfig (67-73)
src/plugins/agents/types.ts (1)
  • AgentSandboxRequirements (210-215)
src/config/index.ts (1)
src/config/types.ts (2)
  • SandboxConfig (67-73)
  • DEFAULT_SANDBOX_CONFIG (75-81)
🪛 LanguageTool
website/content/docs/configuration/sandbox.mdx

[uncategorized] ~71-~71: Possible missing comma found.
Context: ...inues to work and is still used by many tools including Homebrew. You may see a depre...

(AI_HYDRA_LEO_MISSING_COMMA)

🔇 Additional comments (33)
.beads/issues.jsonl (1)

96-96: LGTM!

The new issue entry for tracking sandbox artifact cleanup is well-structured with appropriate metadata.

src/cli.tsx (1)

63-67: LGTM!

The sandbox CLI options are well-documented and follow the existing help text format. The options cover the key sandbox modes: auto, bwrap (Linux), sandbox-exec (macOS), and the ability to disable both sandboxing and network access.

src/sandbox/types.ts (1)

1-8: LGTM!

Clean type re-export module that provides a convenient import path for sandbox types. The ABOUTME comment is present as required, and the type-only import/export pattern is appropriate for this use case.

src/plugins/agents/droid/index.ts (1)

55-63: LGTM!

The getSandboxRequirements() override follows the established pattern from other agent plugins (Claude, OpenCode). The declared paths are sensible:

  • Auth paths cover Droid's config directories and Google Cloud credentials
  • Binary paths include common installation locations
  • Network access is correctly required for API communication
website/lib/navigation.ts (1)

53-53: LGTM!

The new Sandbox navigation item is correctly placed under the Configuration section and follows the existing pattern.

src/sandbox/index.ts (1)

1-8: LGTM!

Clean barrel export that centralises the sandbox module's public API. The ABOUTME comment correctly describes the file's purpose.

src/config/types.test.ts (2)

32-38: LGTM!

Good test coverage for the sandbox configuration defaults. The assertions correctly verify the expected default values: disabled by default, auto mode, and network enabled.


59-61: LGTM!

Correctly verifies that DEFAULT_CONFIG.sandbox includes the sandbox defaults, ensuring the configuration composition is working as expected.

website/content/docs/configuration/sandbox.mdx (1)

1-208: Well-structured and comprehensive documentation.

The sandbox documentation thoroughly covers platform requirements, installation steps, configuration options, CLI flags, and important caveats around network access. The examples are practical and the network access warning section clearly explains the implications of --no-network for cloud-based agents.

src/plugins/agents/builtin/claude.ts (1)

152-159: LGTM – sandbox requirements correctly specified.

The implementation properly defines the Claude agent's sandbox requirements:

  • Authentication paths (~/.claude, ~/.anthropic) enable OAuth token refresh
  • Binary and runtime paths provide necessary access to local tools
  • requiresNetwork: true correctly reflects Claude's cloud-based API dependency

The returned object structure matches the AgentSandboxRequirements interface exactly.

src/plugins/agents/builtin/opencode.ts (1)

168-175: LGTM!

The sandbox requirements are well-defined for the OpenCode agent. The auth paths cover both the legacy ~/.opencode and XDG-compliant ~/.config/opencode locations, binary paths include common installation directories including Go's default bin path, and requiresNetwork: true correctly reflects the agent's need for LLM API access.

src/engine/types.ts (2)

272-281: LGTM!

The EngineWarningEvent interface follows the established event pattern with a discriminant type, programmatic code for handling, and human-readable message. The literal type for code provides good type safety for the current use case.


198-198: LGTM!

The new 'engine:warning' event type is correctly added to both the EngineEventType union and the EngineEvent discriminated union.

Also applies to: 525-525

src/engine/index.ts (2)

312-324: LGTM!

The sandbox-network conflict detection is well-implemented. The conditional correctly checks for all three conditions (sandbox enabled, network explicitly disabled, agent requires network) and emits a clear warning message that helps users understand why LLM API calls might fail.


751-751: LGTM!

Passing the sandbox configuration to the agent execution enables the base plugin to wrap commands with appropriate sandbox isolation. This integrates cleanly with the AgentExecuteOptions type extension.

src/tui/components/Header.tsx (2)

15-16: LGTM!

The lock emoji 🔒 is an appropriate visual indicator for sandbox/security isolation.


244-264: LGTM!

The rendering logic for the sandbox indicator integrates cleanly with the existing header elements. The conditional separators correctly handle all display combinations, and the info colour (blue) appropriately conveys informational status.

src/tui/types.ts (1)

103-106: LGTM!

The sandbox-related props are well-typed with appropriate JSDoc documentation. Using Exclude<SandboxMode, 'auto'> for resolvedSandboxMode correctly ensures only concrete sandbox modes are passed after resolution.

src/config/schema.ts (1)

18-19: LGTM!

The Zod schemas for sandbox configuration are well-defined. The SandboxModeSchema enum values align with the SandboxMode type definition, and all fields in SandboxConfigSchema are appropriately optional to allow partial configuration.

Also applies to: 45-51

src/tui/components/ProgressDashboard.tsx (1)

44-67: LGTM!

The getSandboxDisplay helper function handles all edge cases correctly: disabled state, 'off' mode, auto-resolution display, and network suffix. The logic is clear and self-documenting.

src/sandbox/detect.ts (2)

12-45: LGTM!

The commandExists function is well-implemented with proper timeout handling, cross-platform support (using where on Windows, which elsewhere), and race condition prevention via the resolved flag. The cleanup of the timeout in both error and close handlers is correct.


47-62: LGTM!

The platform detection logic is sound. The function correctly prioritises platform-native sandboxing solutions and gracefully falls back to 'off' when no sandbox is available.

src/tui/components/IterationDetailView.tsx (1)

689-731: LGTM!

The Sandbox Configuration section is well-structured and conditionally rendered only when relevant. The display logic correctly handles auto-resolution, network status with appropriate colour coding (warning for disabled, success for enabled), and optional path lists.

src/config/index.ts (1)

531-556: LGTM!

The sandbox configuration building follows the established pattern for merging config layers, with appropriate precedence: defaults < stored config < runtime options.

src/config/types.ts (1)

65-81: LGTM!

Well-structured type definitions with sensible defaults. The enabled: false default ensures sandboxing is opt-in, and the Required<Pick<...>> type correctly constrains the default object whilst leaving path arrays optional.

src/tui/components/RunApp.tsx (2)

98-102: LGTM!

The sandbox props are correctly typed. Using Exclude<SandboxMode, 'auto'> for resolvedSandboxMode is appropriate since 'auto' is resolved to a concrete mode before being passed to the UI.


1182-1197: LGTM!

Sandbox configuration props are correctly threaded to the relevant display components (Header, ProgressDashboard, IterationDetailView) for status display.

src/plugins/agents/base.ts (2)

414-447: LGTM!

The async sandbox resolution flow is well-designed. Using resolvePromise in the catch block (rather than rejectPromise) ensures consistent behaviour—the execution handle's promise always resolves with an AgentExecutionResult, even on errors. This simplifies error handling for callers.


215-222: LGTM!

Good design: the base class provides empty sandbox requirements, allowing concrete agent implementations (claude, opencode, droid) to override with their specific needs (auth paths, binary paths, etc.).

src/commands/run.tsx (4)

185-191: Verify: --no-network implicitly enables sandboxing.

The --no-network flag sets enabled: true along with network: false. This means --no-network alone will enable sandboxing, which might surprise users who expect it to only affect network access when sandboxing is already enabled.

Is this the intended behaviour? If --no-network should only take effect when combined with --sandbox, consider:

       case '--no-network':
         options.sandbox = {
           ...options.sandbox,
-          enabled: true,
           network: false,
         };
         break;

This would require users to specify --sandbox --no-network explicitly.


77-87: LGTM!

The --sandbox=<mode> parsing correctly handles explicit mode selection for bwrap and sandbox-exec. The plain --sandbox flag defaults to 'auto' mode, and --no-sandbox disables sandboxing entirely.


952-976: LGTM!

Sandbox mode detection at startup is appropriate—the resolved mode is determined once and passed through to UI components for display. This avoids repeated detection calls during execution.


835-837: LGTM!

Engine warnings (including sandbox-related configuration warnings) are appropriately routed to stderr, ensuring they remain visible after the TUI exits.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

AI Agent added 8 commits January 16, 2026 19:11
FormattedText with <span> elements inside <text> was causing:
1. Black backgrounds on lines with colored spans
2. Character loss when text wrapped across lines

New approach:
- Split output into lines
- Render each line as a separate <text> element
- Color entire tool call lines (starting with [toolname]) with accent color
- Regular text lines use secondary foreground color

This avoids OpenTUI's span rendering issues while still providing
visual distinction for tool calls.
parseAgentOutput was only returning the last parsed part, which meant
tool calls (which appear early in the JSONL stream) were discarded and
only the final text result was shown.

Now returns all parsed parts joined together, so tool calls are visible
alongside the text output.
Droid and other agents may output tool calls in the standard Anthropic
message format where tool_use blocks are inside a content[] array,
rather than in a separate tool_calls array.

Added support for extracting tool calls from:
- content[].type === 'tool_use'
- message.content[].type === 'tool_use'
Droid emits tool calls with the tool info at the root level of the
JSONL event (e.g., {"type":"tool_call","toolName":"LS","parameters":...})
rather than in a nested object. The parser now checks if the entire
payload IS a tool call/result when type matches, in addition to
looking for nested tool_call/tool_calls fields.

This fixes tool calls not appearing in the TUI when using droid agent.
formatToolCall already adds a trailing newline, so the additional
leading newline in processAgentEvents caused double line breaks
between consecutive tool calls. Removed the redundant leading newline.
The streaming parser adds its own newline after each extracted line,
but formatters like formatToolCall already include trailing newlines.
This caused double newlines between tool calls. Now we strip trailing
newlines from extracted content before adding the parser's newline.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/plugins/agents/builtin/opencode.ts (1)

303-339: Shell escaping concern applies to Windows only.

The prompt is passed as a positional argument (line 336). On Windows, shell: true is used (line 299 in base.ts), which enables shell interpretation of special characters in arguments. If the prompt contains shell metacharacters (quotes, backticks, $, etc.), they could be misinterpreted by the shell. On Unix systems, shell: false is used, which passes arguments safely without shell interpretation.

Consider escaping the prompt or using an alternative approach (such as environment variables or stdin) if Windows compatibility with special characters is important.

🤖 Fix all issues with AI agents
In `@examples/objects.ts`:
- Around line 58-81: deepClone currently recurses infinitely on
circular/self-referential structures; add cycle tracking using a WeakMap to map
source objects to their clones inside deepClone (or an internal helper that
accepts the WeakMap). On entry, if obj is non-object return it; if seen.has(obj)
return seen.get(obj); create the appropriate empty clone (for Array create [],
for Date/RegExp handle as now, for plain objects create {}), immediately store
seen.set(obj, clone) before recursing into properties/elements, then populate
children using recursive calls that pass the same WeakMap; use the existing
function name deepClone (or an internal helper deepCloneWithMap) so callers keep
the same API.

In `@src/tui/output-parser.ts`:
- Around line 415-472: In extractReadableSegments, plain-text segment texts can
still contain ANSI sequences; update the function to strip ANSI codes before
returning any FormattedSegment.text: (1) after
formatDroidEventToSegments(droidResult.message) map over the returned segments
and replace segment.text with stripAnsi(segment.text) when this.isDroid, (2) for
the non-JSON branch that returns [{ text: trimmed }], return [{ text:
stripAnsi(trimmed) }], and (3) for assistant handling, apply stripAnsi to the
single content string and to the joined textParts output. Import or use the
existing stripAnsi utility (or add one) so every returned FormattedSegment.text
is ANSI-clean.
♻️ Duplicate comments (1)
src/plugins/agents/droid/outputParser.ts (1)

528-571: Surface tool-result failures instead of dropping them.

The code at lines 556-559 pushes a plain tool_result event regardless of whether the tool execution failed. Since processAgentEvents intentionally skips tool_result events (per the relevant code snippet from output-formatting.ts lines 339-342), any failures marked via isError or status will be invisible to the user.

Consider mapping error results to error events:

🩹 Proposed fix
   // Parse tool results (shared logic will skip these)
   if (message.toolResults && message.toolResults.length > 0) {
-    events.push({ type: 'tool_result' });
+    for (const result of message.toolResults) {
+      if (result.isError || (result.status && result.status.toLowerCase() === 'error')) {
+        const statusSuffix = result.status ? ` (status: ${result.status})` : '';
+        events.push({ type: 'error', message: `Tool execution failed${statusSuffix}` });
+      } else {
+        events.push({ type: 'tool_result' });
+      }
+    }
   }
🧹 Nitpick comments (11)
examples/validation.ts (1)

42-47: Avoid duplicate Number(s) conversion.

Number(s) is called twice on line 46. Consider storing the result in a variable to avoid redundant parsing.

♻️ Proposed refactor
 export function isNumeric(s: string): boolean {
   if (s.trim() === "") {
     return false;
   }
-  return !isNaN(Number(s)) && isFinite(Number(s));
+  const num = Number(s);
+  return !isNaN(num) && isFinite(num);
 }
examples/timers.ts (2)

28-31: Consider using any[] for the generic constraint.

The constraint (...args: unknown[]) => unknown can cause type inference issues with functions that have specific typed parameters due to contravariance. Using any[] is the standard pattern for higher-order function wrappers like debounce/throttle.

♻️ Suggested fix
-export function debounce<T extends (...args: unknown[]) => unknown>(
+export function debounce<T extends (...args: any[]) => any>(
   fn: T,
   ms: number
 ): (...args: Parameters<T>) => void {

54-66: Note: Leading-edge throttle drops intermediate calls.

This throttle implementation only executes on the leading edge — calls made during the throttle period are silently dropped with no trailing execution. This may be acceptable for your use case, but be aware that:

  • The last call during a burst won't execute if it falls within the throttle window.
  • For scroll/resize handlers, this could mean missing the final position.

If trailing execution is needed, consider adding an optional trailing parameter or documenting this behaviour explicitly.

Also, same type constraint suggestion as debounce:

♻️ Suggested fix for type constraint
-export function throttle<T extends (...args: unknown[]) => unknown>(
+export function throttle<T extends (...args: any[]) => any>(
   fn: T,
   ms: number
 ): (...args: Parameters<T>) => void {
examples/slugs.ts (1)

24-36: Filter empty segments to avoid extra spaces on malformed slugs.
This keeps output tidy for inputs with consecutive or leading/trailing hyphens.

♻️ Proposed refinement
 export function unslugify(s: string): string {
   return s
     .split('-')
+    .filter(Boolean)
     .map(word => word.charAt(0).toUpperCase() + word.slice(1))
     .join(' ');
 }
examples/dates.ts (1)

17-30: Consider UTC-normalising to avoid DST/time-of-day drift.
If callers expect calendar day differences (rather than elapsed 24‑hour blocks), the current millisecond diff can be off by one around DST or non‑midnight inputs. Normalising to UTC midnight makes the intent unambiguous.

♻️ Suggested adjustment
 export function daysBetween(date1: Date, date2: Date): number {
   const msPerDay = 24 * 60 * 60 * 1000;
-  const diffMs = Math.abs(date2.getTime() - date1.getTime());
-  return Math.floor(diffMs / msPerDay);
+  const utc1 = Date.UTC(date1.getUTCFullYear(), date1.getUTCMonth(), date1.getUTCDate());
+  const utc2 = Date.UTC(date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate());
+  return Math.abs(utc2 - utc1) / msPerDay;
 }
examples/objects.ts (1)

19-23: Prefer own-property checks in pick() to avoid inherited keys.
key in obj can include prototype properties for class instances. Using hasOwnProperty aligns with typical pick/omit expectations.

♻️ Proposed change
-  for (const key of keys) {
-    if (key in obj) {
-      result[key] = obj[key];
-    }
-  }
+  for (const key of keys) {
+    if (Object.prototype.hasOwnProperty.call(obj, key)) {
+      result[key] = obj[key];
+    }
+  }
src/tui/components/RightPanel.tsx (1)

528-546: Iteration segments are plumbed but currently unused.
If this is intentionally deferred, consider a short TODO/feature flag so it doesn’t linger unnoticed.

Also applies to: 576-579

src/tui/output-parser.ts (1)

225-313: Avoid recomputing segment length on every push.
The reduce over the full segment list each chunk can get expensive on long streams; tracking a running length is cheaper.

♻️ Possible optimisation
 export class StreamingOutputParser {
   private buffer = '';
   private parsedOutput = '';
   private parsedSegments: FormattedSegment[] = [];
+  private parsedSegmentsLength = 0;
   private lastResultText = '';
   private lastCostSummary = '';
@@
     if (newSegments.length > 0) {
       this.parsedSegments.push(...newSegments);
+      this.parsedSegmentsLength += newSegments.reduce((acc, s) => acc + s.text.length, 0);
 
-      const totalLength = this.parsedSegments.reduce((acc, s) => acc + s.text.length, 0);
-      if (totalLength > MAX_PARSED_OUTPUT_SIZE) {
+      if (this.parsedSegmentsLength > MAX_PARSED_OUTPUT_SIZE) {
         // Simple trim: keep last N segments that fit within limit
         let kept = 0;
         let keptLength = 0;
@@
         this.parsedSegments = [
           { text: '[...output trimmed...]\n', color: 'muted' },
           ...this.parsedSegments.slice(-kept),
         ];
+        this.parsedSegmentsLength = this.parsedSegments.reduce((acc, s) => acc + s.text.length, 0);
       }
     }
@@
   reset(): void {
     this.buffer = '';
     this.parsedOutput = '';
     this.parsedSegments = [];
+    this.parsedSegmentsLength = 0;
     this.lastResultText = '';
     this.lastCostSummary = '';

Also applies to: 537-537

src/plugins/agents/output-formatting.ts (2)

65-89: Consider edge case: commands with multiple semicolons.

The formatCommand function takes only the last part after splitting on ;. If the actual command contains semicolons (e.g., in a shell one-liner), this could truncate meaningful content.

This is a minor edge case — the current behaviour is reasonable for the common pattern of stripping ENV_VAR=val; actual_command.


183-185: Truncation may lose important context for edit operations.

When old_string or new_string exceed 50 characters, the diff display truncates both to 50 chars plus .... Consider showing the full strings when they're reasonably short, or providing a way to view the full diff:

-  if (input.old_string && input.new_string) {
-    parts.push(`edit: "${input.old_string.slice(0, 50)}..." → "${input.new_string.slice(0, 50)}..."`);
+  if (input.old_string && input.new_string) {
+    const oldPreview = input.old_string.length > 50 ? input.old_string.slice(0, 50) + '...' : input.old_string;
+    const newPreview = input.new_string.length > 50 ? input.new_string.slice(0, 50) + '...' : input.new_string;
+    parts.push(`edit: "${oldPreview}" → "${newPreview}"`);
   }
src/plugins/agents/builtin/claude.ts (1)

414-455: The onStdoutSegments wrapper appears to be a no-op.

Lines 425-429 set onStdoutSegments to an empty function that does nothing when isStreamingJson is true. The actual segment emission happens inside the onStdout wrapper (lines 436-440). While this works, the empty callback assignment is confusing.

Consider removing the no-op assignment for clarity:

♻️ Proposed refactor
     const parsedOptions: AgentExecuteOptions = {
       ...options,
-      // TUI-native segments callback (preferred)
-      onStdoutSegments: options?.onStdoutSegments && isStreamingJson
-        ? (/* original segments ignored - we parse from raw */) => {
-            // This callback is set up but actual segments come from wrapping onStdout below
-          }
-        : options?.onStdoutSegments,
+      // Don't pass through onStdoutSegments when streaming JSON - we emit segments from onStdout
+      onStdoutSegments: isStreamingJson ? undefined : options?.onStdoutSegments,
       // Legacy string callback or wrapper that calls both callbacks
       onStdout: isStreamingJson && (options?.onStdout || options?.onStdoutSegments)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1af7b11 and 6aec342.

📒 Files selected for processing (23)
  • examples/arrays.ts
  • examples/dates.ts
  • examples/objects.ts
  • examples/slugs.ts
  • examples/timers.ts
  • examples/validation.ts
  • src/chat/engine.ts
  • src/chat/types.ts
  • src/engine/rate-limit-detector.ts
  • src/plugins/agents/builtin/claude.ts
  • src/plugins/agents/builtin/opencode.ts
  • src/plugins/agents/droid/outputParser.ts
  • src/plugins/agents/output-formatting.ts
  • src/plugins/agents/types.ts
  • src/tui/components/ChatView.tsx
  • src/tui/components/FormattedText.tsx
  • src/tui/components/PrdChatApp.tsx
  • src/tui/components/RightPanel.tsx
  • src/tui/components/RunApp.tsx
  • src/tui/output-parser.ts
  • src/tui/types.ts
  • tests/engine/rate-limit-detector.test.ts
  • website/content/docs/getting-started/installation.mdx
✅ Files skipped from review due to trivial changes (1)
  • src/engine/rate-limit-detector.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Start all code files with a file-level JSDoc comment section explaining the file's purpose, prefixed with 'ABOUTME: '

Files:

  • tests/engine/rate-limit-detector.test.ts
  • src/plugins/agents/types.ts
  • examples/dates.ts
  • src/tui/components/FormattedText.tsx
  • examples/slugs.ts
  • examples/timers.ts
  • src/chat/types.ts
  • src/tui/components/RightPanel.tsx
  • examples/arrays.ts
  • examples/validation.ts
  • examples/objects.ts
  • src/plugins/agents/droid/outputParser.ts
  • src/tui/output-parser.ts
  • src/chat/engine.ts
  • src/tui/components/ChatView.tsx
  • src/tui/types.ts
  • src/tui/components/RunApp.tsx
  • src/plugins/agents/output-formatting.ts
  • src/plugins/agents/builtin/claude.ts
  • src/tui/components/PrdChatApp.tsx
  • src/plugins/agents/builtin/opencode.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Start all code files with a file-level JSDoc comment section explaining the file's purpose, prefixed with "ABOUTME: "

Files:

  • tests/engine/rate-limit-detector.test.ts
  • src/plugins/agents/types.ts
  • examples/dates.ts
  • src/tui/components/FormattedText.tsx
  • examples/slugs.ts
  • examples/timers.ts
  • src/chat/types.ts
  • src/tui/components/RightPanel.tsx
  • examples/arrays.ts
  • examples/validation.ts
  • examples/objects.ts
  • src/plugins/agents/droid/outputParser.ts
  • src/tui/output-parser.ts
  • src/chat/engine.ts
  • src/tui/components/ChatView.tsx
  • src/tui/types.ts
  • src/tui/components/RunApp.tsx
  • src/plugins/agents/output-formatting.ts
  • src/plugins/agents/builtin/claude.ts
  • src/tui/components/PrdChatApp.tsx
  • src/plugins/agents/builtin/opencode.ts
🧬 Code graph analysis (11)
src/plugins/agents/types.ts (3)
src/config/types.ts (1)
  • SandboxConfig (67-73)
src/plugins/agents/output-formatting.ts (1)
  • FormattedSegment (21-24)
src/tui/components/FormattedText.tsx (1)
  • FormattedSegment (12-12)
src/tui/components/FormattedText.tsx (2)
src/plugins/agents/output-formatting.ts (2)
  • SegmentColor (16-16)
  • FormattedSegment (21-24)
src/tui/theme.ts (1)
  • colors (9-58)
src/chat/types.ts (1)
src/plugins/agents/output-formatting.ts (1)
  • FormattedSegment (21-24)
src/tui/components/RightPanel.tsx (4)
src/plugins/agents/output-formatting.ts (2)
  • FormattedSegment (21-24)
  • stripAnsiCodes (415-417)
src/tui/components/FormattedText.tsx (1)
  • FormattedSegment (12-12)
src/tui/output-parser.ts (1)
  • parseAgentOutput (118-180)
src/tui/theme.ts (1)
  • colors (9-58)
src/plugins/agents/droid/outputParser.ts (2)
src/plugins/agents/output-formatting.ts (4)
  • AgentDisplayEvent (307-312)
  • processAgentEvents (319-347)
  • FormattedSegment (21-24)
  • processAgentEventsToSegments (363-391)
src/tui/components/FormattedText.tsx (1)
  • FormattedSegment (12-12)
src/tui/output-parser.ts (2)
src/plugins/agents/output-formatting.ts (2)
  • stripAnsiCodes (415-417)
  • FormattedSegment (21-24)
src/plugins/agents/droid/outputParser.ts (2)
  • parseDroidJsonlLine (616-678)
  • formatDroidEventToSegments (590-596)
src/tui/components/ChatView.tsx (3)
src/plugins/agents/output-formatting.ts (1)
  • FormattedSegment (21-24)
src/tui/components/FormattedText.tsx (2)
  • FormattedSegment (12-12)
  • FormattedText (42-66)
src/tui/theme.ts (1)
  • colors (9-58)
src/tui/components/RunApp.tsx (2)
src/config/types.ts (2)
  • SandboxConfig (67-73)
  • SandboxMode (65-65)
src/plugins/agents/output-formatting.ts (1)
  • FormattedSegment (21-24)
src/plugins/agents/builtin/claude.ts (2)
src/plugins/agents/output-formatting.ts (3)
  • AgentDisplayEvent (307-312)
  • processAgentEventsToSegments (363-391)
  • processAgentEvents (319-347)
src/plugins/agents/types.ts (3)
  • AgentFileContext (30-42)
  • AgentExecuteOptions (92-124)
  • AgentExecutionHandle (262-274)
src/tui/components/PrdChatApp.tsx (1)
src/plugins/agents/output-formatting.ts (1)
  • FormattedSegment (21-24)
src/plugins/agents/builtin/opencode.ts (2)
src/plugins/agents/output-formatting.ts (3)
  • AgentDisplayEvent (307-312)
  • processAgentEventsToSegments (363-391)
  • processAgentEvents (319-347)
src/plugins/agents/types.ts (1)
  • AgentExecuteOptions (92-124)
🪛 ast-grep (0.40.5)
examples/objects.ts

[warning] 71-71: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(obj.source, obj.flags)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🪛 Biome (2.1.2)
src/plugins/agents/droid/outputParser.ts

[error] 601-601: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 601-601: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 601-601: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 601-601: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 601-601: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)

src/plugins/agents/output-formatting.ts

[error] 413-413: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 413-413: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 413-413: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 413-413: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 413-413: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)

🔇 Additional comments (58)
tests/engine/rate-limit-detector.test.ts (1)

240-267: Solid regression coverage for false positives.

These cases are well-targeted and should prevent noisy detections in common dependency and code contexts.

website/content/docs/getting-started/installation.mdx (1)

146-146: Clear and helpful re-run guidance.
The updated command is concise and improves the reconfiguration guidance.

examples/validation.ts (3)

1-4: LGTM!

The file-level JSDoc comment follows the coding guidelines with the required ABOUTME: prefix and clearly describes the file's purpose.


13-16: LGTM!

The implementation correctly uses a simplified email regex that handles most common email formats. The regex appropriately enforces the basic structure of [email protected].


25-32: LGTM!

Using the URL constructor is the idiomatic approach for URL validation, and the protocol check correctly restricts to HTTP/HTTPS schemes.

examples/timers.ts (2)

1-4: LGTM!

File-level JSDoc with ABOUTME prefix is present as per coding guidelines.


14-16: LGTM!

Clean and correct implementation of the sleep utility.

examples/slugs.ts (2)

1-4: Good file-level ABOUTME header.
Clear purpose description and compliant with the required file header format. As per coding guidelines, ...


6-21: Slugify implementation looks solid.
Readable transformation chain with sensible normalisation steps for typical ASCII input.

examples/dates.ts (2)

6-15: Looks good: ISO formatting helper is clean and clear.


32-43: Weekend check is straightforward and readable.

examples/objects.ts (2)

1-4: ABOUTME header matches the required file-level JSDoc format.
Nice to see the file purpose documented up front.

As per coding guidelines, the file-level ABOUTME JSDoc requirement is met.


37-46: omit() implementation is clean and type-safe.
No concerns here.

examples/arrays.ts (3)

6-16: Clean, readable dedupe helper.
Nice, concise implementation with a Set.


18-28: Simple one-level flatten is clear.
No concerns with the current approach.


30-46: Chunking logic looks correct.
The loop/slice approach is clean and predictable.

src/chat/types.ts (1)

7-73: Good addition of structured streaming alongside legacy chunks.
The new onSegments callback keeps compatibility while enabling richer output.

src/tui/components/ChatView.tsx (2)

13-65: Nice, tidy prop and import additions for segment rendering.
This keeps the API clear while enabling richer output.


374-399: Segment-first rendering with legacy fallback is well handled.
Loading behaviour remains predictable with the spinner fallback.

src/plugins/agents/types.ts (3)

92-112: Execution options extension looks solid.
onStdoutSegments and sandbox config integrate cleanly with existing callbacks.


214-305: Sandbox requirements contract is clear and usable.
The explicit requirements shape should make wrappers easier to reason about.


7-9: No action required; import source is correct.

The SandboxConfig import from ../../sandbox/types.js is valid. The file src/sandbox/types.ts intentionally re-exports SandboxConfig from its canonical definition in src/config/types.ts, making it the proper public API for sandbox type imports. The relative path correctly resolves to the re-export file.

src/chat/engine.ts (1)

210-223: No action required—all agents emit stdout alongside segments.

All agent implementations (BaseAgentPlugin, DroidAgentPlugin, ClaudeAgentPlugin, and OpenCodeAgentPlugin) invoke the onStdout callback with raw output data. Even ClaudeAgentPlugin and OpenCodeAgentPlugin, which wrap onStdout to parse structured events, still call options?.onStdout() with the processed content (see claude.ts:443–445 and opencode.ts:364–367). Since onStdout accumulates into fullOutput in ChatEngine, responseContent will not be empty when segments are emitted.

Likely an incorrect or invalid review comment.

src/tui/components/FormattedText.tsx (2)

14-26: Clear semantic-to-theme colour mapping.
Nicely centralises colour choices with a sensible default.


42-64: FormattedText render path looks solid.
Early null return plus panel background handling keeps OpenTUI happy.

src/tui/types.ts (3)

9-10: Type imports align with new sandbox/segment plumbing.
Looks consistent with the new UI surface.


104-107: Header sandbox props are sensibly optional.
Good typing for resolved non-auto mode.


155-158: Right panel segment output hook is tidy.
Optional segments support keeps backwards compatibility.

src/tui/components/RightPanel.tsx (3)

13-13: Live-output ANSI stripping is a good safety net.
This keeps OpenTUI rendering clean while completed output still goes through the parser.

Also applies to: 559-575


654-675: Line-based tool highlighting reads well.
Clean and readable output for tool-prefixed lines.


692-736: Prop threading for segment output is consistent.
The TaskDetails/RightPanel pass-through stays optional and predictable.

Also applies to: 746-785

src/tui/components/PrdChatApp.tsx (3)

19-19: Streaming segments state added cleanly.
The new type import and state fit neatly alongside the legacy chunk path.

Also applies to: 210-212


348-349: Segment streaming lifecycle is handled consistently.
Reset before each run and append as callbacks arrive — nice and predictable.

Also applies to: 367-370, 382-384, 424-425, 437-440, 452-454


556-557: ChatView now receives segment streams in both phases.
Keeps review and chat paths aligned.

Also applies to: 585-586

src/tui/output-parser.ts (2)

12-16: ANSI stripping for string output is consistent and safe.
This should reduce OpenTUI artefacts across both parsed and raw paths.

Also applies to: 161-170, 179-179, 337-345


515-521: Public segment accessor is useful.
This exposes the new formatted output cleanly for the UI layer.

src/plugins/agents/droid/outputParser.ts (4)

1-12: LGTM!

The file header follows the coding guidelines with an ABOUTME prefix, and the new imports for the shared event-based formatting utilities are correctly structured.


214-239: LGTM!

The extended tool call extraction logic correctly handles:

  1. Droid's top-level tool_call format where the payload itself is the tool call
  2. Anthropic/Claude-style content[] with tool_use blocks

The guard calls.length === 0 prevents duplicate entries when tool calls are already extracted via other paths.


301-309: LGTM!

Consistent with the tool call extraction pattern - handles top-level tool_result payloads with the same guard against duplicates.


598-605: Biome lint warning is a false positive for ANSI escape sequence handling.

The static analysis tool flags control characters (\x1b, \x07) as suspicious, but these are intentional and correct for matching ANSI escape sequences. The regex properly handles:

  • CSI sequences: ESC[...letter
  • OSC sequences: ESC]...BEL
  • Charset switching: ESC(A, ESC)B, etc.

No action required — consider adding a // biome-ignore comment if the lint errors are blocking CI.

src/plugins/agents/builtin/opencode.ts (4)

1-6: LGTM!

The file header correctly follows the coding guidelines with an ABOUTME prefix describing the plugin's purpose and supported features.


32-82: LGTM!

The JSON line parser correctly handles OpenCode's event types and includes a sensible fallback for non-JSON lines that look like plugin messages (e.g., [oh-my-opencode]). The empty catch block is appropriate here since invalid JSON should be silently skipped.


195-203: LGTM!

The sandbox requirements are sensible for the OpenCode CLI:

  • Auth paths include common config locations for OAuth tokens
  • Binary paths cover standard installation locations
  • requiresNetwork: true is necessary for API calls

345-377: LGTM!

The execute override correctly wraps the callbacks to:

  1. Parse JSON events from the raw output
  2. Convert to segments for TUI-native rendering
  3. Also call the legacy string callback if provided

The conditional callback setup ensures no unnecessary processing when callbacks aren't provided.

src/plugins/agents/output-formatting.ts (5)

1-10: LGTM!

Excellent file header that clearly explains the module's architecture and responsibility boundaries. Follows coding guidelines with ABOUTME prefix.


16-24: LGTM!

Clean type definitions. SegmentColor uses semantic names that map to TUI theme colours, and FormattedSegment is a simple, focused interface.


303-312: LGTM!

The AgentDisplayEvent union type is well-designed with clear discriminated union semantics. Each event type has appropriate optional fields.


319-347: LGTM!

Both processAgentEvents (legacy string) and processAgentEventsToSegments (TUI segments) implement consistent display rules:

  • text and tool_use are displayed
  • tool_result and system are intentionally skipped

The comment documenting the intentional skip is helpful for maintainability.

Also applies to: 363-391


401-417: Biome lint warning is a false positive — control characters are intentional.

Same as in outputParser.ts, the ANSI_REGEX correctly uses control characters (\x1b, \x07) to match escape sequences. The well-documented comment explaining the matched patterns is appreciated.

src/plugins/agents/builtin/claude.ts (3)

1-6: LGTM!

File header correctly follows coding guidelines with ABOUTME prefix describing the plugin's capabilities.


154-163: LGTM!

Sandbox requirements are well-specified for the Claude CLI:

  • Auth paths cover both ~/.claude and ~/.anthropic
  • Binary paths include the symlink location and actual installation path
  • Runtime paths include ~/.bun and ~/.nvm for Node.js environments

348-396: LGTM!

The parseClaudeJsonLine method correctly handles Claude's stream-json format:

  • Extracts text and tool_use from assistant message content blocks
  • Maps user events to tool_result
  • Handles both event.error (string) and event.error.message patterns

Good defensive programming with the empty catch block for non-JSON lines.

src/tui/components/RunApp.tsx (6)

1-5: LGTM!

File header correctly follows coding guidelines with ABOUTME prefix.


34-34: LGTM!

The sandbox and segment-related imports and props are correctly added. The RunAppProps interface is properly extended with optional sandbox configuration for display purposes.

Also applies to: 40-40, 99-103, 307-308


328-328: LGTM!

The currentSegments state is properly managed:

  • Initialised as empty array (line 328)
  • Reset on iteration:started (line 498)
  • Updated on agent:output alongside currentOutput (lines 596-598)

This ensures segments stay in sync with the text output.

Also applies to: 496-498, 596-598


991-1047: LGTM!

The selectedTaskIteration useMemo correctly handles segments:

  • Live/executing iterations include currentSegments
  • Completed iterations set segments: undefined (no live data)
  • Historical data also sets segments: undefined

The dependency array properly includes currentSegments.


816-818: Good documentation of intentional behaviour.

The comment clearly explains why Ctrl+C isn't handled — terminals can't distinguish between Ctrl+C (stop) and Ctrl+Shift+C (copy). Directing users to 'q' for quit is a reasonable UX decision.


1186-1187: LGTM!

Sandbox configuration and segments are correctly threaded through to all relevant child components:

  • Header: receives sandboxConfig and resolvedSandboxMode
  • ProgressDashboard: receives sandboxConfig and resolvedSandboxMode
  • IterationDetailView: receives sandboxConfig and resolvedSandboxMode
  • RightPanel: receives iterationSegments for TUI-native rendering

Also applies to: 1200-1201, 1225-1226, 1235-1235, 1269-1269

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/plugins/agents/output-formatting.ts`:
- Around line 183-185: The edit-preview unconditionally appends "..." and slices
to 50 chars; update the legacy formatToolCall implementation to only append
"..." when the original string was actually truncated: in the formatToolCall
function adjust the parts.push call that references input.old_string and
input.new_string so you compute displayOld = input.old_string.length > 50 ?
input.old_string.slice(0,50) + '...' : input.old_string (and similarly for
displayNew) and push `edit: "${displayOld}" → "${displayNew}"`; this preserves
full strings when ≤50 chars and retains truncation only when necessary.
- Around line 280-285: The edit preview always appends "..." after slicing
input.old_string and input.new_string, which is misleading when the original
text is <=50 chars; update the logic that builds the segments (the block pushing
into segments using input.old_string and input.new_string) to only add the
truncation marker when the original string length exceeds 50 (e.g., use the
sliced value plus (input.old_string.length > 50 ? '...' : '') and likewise for
input.new_string) so the "..." appears only when truncation actually occurred.

In `@src/tui/output-parser.ts`:
- Around line 300-313: The trimming logic can fail when kept is 0 because
slice(-0) === slice(0) returns the full array; update the code that builds
this.parsedSegments so it doesn't call slice(-kept) when kept is 0. Change the
slice call to derive a safe start index (e.g. compute start = Math.max(0,
this.parsedSegments.length - kept) and use slice(start)) or conditionally use an
empty array when kept === 0; keep references to parsedSegments, kept,
keptLength, and MAX_PARSED_OUTPUT_SIZE to locate and fix the logic.
♻️ Duplicate comments (1)
src/tui/output-parser.ts (1)

417-478: ANSI codes not stripped from plain-text segments.

Plain-text segments returned at lines 437, 453, 460, and 474 can still carry ANSI sequences, which may cause TUI rendering artefacts. This was flagged in a previous review but remains unaddressed in these code paths.

🐛 Suggested fix
     // Not JSON - return as plain text segment if it's not just whitespace
     if (!trimmed.startsWith('{')) {
-      return [{ text: trimmed }];
+      return [{ text: stripAnsiCodes(trimmed) }];
     }
@@
       // Assistant message with content - show the text as plain segment
       if (event.type === 'assistant') {
         const assistantEvent = event as AssistantEvent;
         const content = assistantEvent.message?.content;
         if (typeof content === 'string' && content.trim()) {
-          return [{ text: content }];
+          return [{ text: stripAnsiCodes(content) }];
         }
         if (Array.isArray(content)) {
           const textParts = content
             .filter((c): c is { type: string; text: string } => c.type === 'text' && !!c.text)
             .map((c) => c.text);
           if (textParts.length > 0) {
-            return [{ text: textParts.join('') }];
+            return [{ text: stripAnsiCodes(textParts.join('')) }];
           }
         }
       }
@@
     } catch {
       // Not valid JSON - return as plain text if meaningful
       if (trimmed.length > 0 && !trimmed.startsWith('{')) {
-        return [{ text: trimmed }];
+        return [{ text: stripAnsiCodes(trimmed) }];
       }
       return [];
     }
🧹 Nitpick comments (1)
src/plugins/agents/output-formatting.ts (1)

65-89: Code duplication between formatCommand and cleanCommand.

Both functions share nearly identical logic for normalising commands: newline replacement, semicolon splitting, env-var stripping, and truncation. The only difference is formatCommand prepends $ .

Consider extracting the shared logic to avoid drift:

♻️ Suggested refactor
+/**
+ * Clean a command string (remove env vars, normalize whitespace, truncate).
+ */
+function cleanCommand(command: string): string {
+  let cmd = command.replace(/\n/g, ' ').trim();
+
+  if (cmd.includes(';')) {
+    const parts = cmd.split(';');
+    cmd = parts[parts.length - 1].trim();
+  }
+
+  const envVarPattern = /^(\s*\w+=[^\s]*\s+)+/;
+  if (envVarPattern.test(cmd)) {
+    cmd = cmd.replace(envVarPattern, '').trim();
+  }
+
+  if (cmd.length > 100) {
+    cmd = cmd.slice(0, 100) + '...';
+  }
+
+  return cmd;
+}
+
 export function formatCommand(command: string): string {
-  // Normalize newlines to spaces
-  let cmd = command.replace(/\n/g, ' ').trim();
-
-  // Extract actual command from env var setup
-  // Pattern: ENV_VAR=value ... ; actual_command
-  if (cmd.includes(';')) {
-    const parts = cmd.split(';');
-    cmd = parts[parts.length - 1].trim();
-  }
-
-  // Also handle inline env vars before command (VAR=val VAR2=val2 command)
-  // If the command starts with lots of VAR= patterns, try to find the actual command
-  const envVarPattern = /^(\s*\w+=[^\s]*\s+)+/;
-  if (envVarPattern.test(cmd)) {
-    cmd = cmd.replace(envVarPattern, '').trim();
-  }
-
-  // Truncate very long commands
-  if (cmd.length > 100) {
-    cmd = cmd.slice(0, 100) + '...';
-  }
-
-  return `$ ${cmd}`;
+  return `$ ${cleanCommand(command)}`;
 }

Also applies to: 197-215

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6aec342 and b413fcf.

📒 Files selected for processing (2)
  • src/plugins/agents/output-formatting.ts
  • src/tui/output-parser.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Start all code files with a file-level JSDoc comment section explaining the file's purpose, prefixed with 'ABOUTME: '

Files:

  • src/tui/output-parser.ts
  • src/plugins/agents/output-formatting.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Start all code files with a file-level JSDoc comment section explaining the file's purpose, prefixed with "ABOUTME: "

Files:

  • src/tui/output-parser.ts
  • src/plugins/agents/output-formatting.ts
🧬 Code graph analysis (1)
src/tui/output-parser.ts (2)
src/plugins/agents/output-formatting.ts (2)
  • stripAnsiCodes (415-417)
  • FormattedSegment (21-24)
src/plugins/agents/droid/outputParser.ts (2)
  • parseDroidJsonlLine (616-678)
  • formatDroidEventToSegments (590-596)
🪛 Biome (2.1.2)
src/plugins/agents/output-formatting.ts

[error] 413-413: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 413-413: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 413-413: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 413-413: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 413-413: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)

🔇 Additional comments (5)
src/plugins/agents/output-formatting.ts (2)

1-10: LGTM!

The file-level JSDoc with ABOUTME: prefix clearly explains the module's purpose and architectural role. This aligns with the coding guidelines.


401-416: Static analysis warning is a false positive — control characters are intentional.

Biome flags \x1b (the ESC character) as suspicious, but these are required to match ANSI escape sequences. The regex correctly identifies CSI, OSC, and charset-switching sequences. No action needed.

src/tui/output-parser.ts (3)

1-6: LGTM!

The file-level JSDoc with ABOUTME: prefix clearly explains the module's purpose.


266-278: Good fix for double-newline issue.

Stripping trailing newlines from extracted content before appending \n prevents the duplicated blank lines mentioned in the commit summary.


517-523: LGTM!

The getSegments() method provides a clean API for TUI components to access the accumulated colour-formatted segments.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Claude emits text and tool_use in the same message content array,
causing "Let me check...[Glob]" without a line break. Now we add
a newline before tool calls only when there's preceding content
that doesn't already end with a newline.

This fixes Claude output while preserving clean output for droid
which emits tool calls as separate events.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/plugins/agents/output-formatting.ts`:
- Around line 421-424: The ANSI_REGEX literal includes control characters that
trip Biome lint; replace the regex literal with a RegExp constructed from a
string to avoid embedded control characters. Update the ANSI_REGEX constant
(used by stripAnsiCodes) to use new RegExp('...','g') with the same pattern
escaped in a string and preserve the 'g' flag so stripAnsiCodes(str: string)
continues to work unchanged.
♻️ Duplicate comments (1)
src/plugins/agents/output-formatting.ts (1)

183-185: Only append ellipsis when truncating edit previews.

Right now "..." is added even for short strings, which is misleading.

🐛 Suggested fix
-  if (input.old_string && input.new_string) {
-    parts.push(`edit: "${input.old_string.slice(0, 50)}..." → "${input.new_string.slice(0, 50)}..."`);
-  }
+  if (input.old_string && input.new_string) {
+    const oldPreview = input.old_string.length > 50
+      ? `${input.old_string.slice(0, 50)}...`
+      : input.old_string;
+    const newPreview = input.new_string.length > 50
+      ? `${input.new_string.slice(0, 50)}...`
+      : input.new_string;
+    parts.push(`edit: "${oldPreview}" → "${newPreview}"`);
+  }
@@
-  if (input.old_string && input.new_string) {
+  if (input.old_string && input.new_string) {
+    const oldPreview = input.old_string.length > 50
+      ? input.old_string.slice(0, 50) + '...'
+      : input.old_string;
+    const newPreview = input.new_string.length > 50
+      ? input.new_string.slice(0, 50) + '...'
+      : input.new_string;
     segments.push({ text: ' edit: "', color: 'muted' });
-    segments.push({ text: input.old_string.slice(0, 50) + '...', color: 'pink' });
+    segments.push({ text: oldPreview, color: 'pink' });
     segments.push({ text: '" → "', color: 'muted' });
-    segments.push({ text: input.new_string.slice(0, 50) + '...', color: 'green' });
+    segments.push({ text: newPreview, color: 'green' });
     segments.push({ text: '"', color: 'muted' });
   }

Also applies to: 280-285

🧹 Nitpick comments (1)
src/plugins/agents/output-formatting.ts (1)

65-88: Deduplicate command normalisation to avoid drift.

formatCommand and cleanCommand repeat identical logic, which risks divergence and inconsistent truncation. Consider extracting a shared helper.

♻️ Proposed refactor
+function normalizeCommand(command: string): string {
+  let cmd = command.replace(/\n/g, ' ').trim();
+  if (cmd.includes(';')) {
+    const parts = cmd.split(';');
+    cmd = parts[parts.length - 1].trim();
+  }
+  const envVarPattern = /^(\s*\w+=[^\s]*\s+)+/;
+  if (envVarPattern.test(cmd)) {
+    cmd = cmd.replace(envVarPattern, '').trim();
+  }
+  if (cmd.length > 100) {
+    cmd = cmd.slice(0, 100) + '...';
+  }
+  return cmd;
+}
+
 export function formatCommand(command: string): string {
-  // Normalize newlines to spaces
-  let cmd = command.replace(/\n/g, ' ').trim();
-
-  // Extract actual command from env var setup
-  // Pattern: ENV_VAR=value ... ; actual_command
-  if (cmd.includes(';')) {
-    const parts = cmd.split(';');
-    cmd = parts[parts.length - 1].trim();
-  }
-
-  // Also handle inline env vars before command (VAR=val VAR2=val2 command)
-  // If the command starts with lots of VAR= patterns, try to find the actual command
-  const envVarPattern = /^(\s*\w+=[^\s]*\s+)+/;
-  if (envVarPattern.test(cmd)) {
-    cmd = cmd.replace(envVarPattern, '').trim();
-  }
-
-  // Truncate very long commands
-  if (cmd.length > 100) {
-    cmd = cmd.slice(0, 100) + '...';
-  }
-
-  return `$ ${cmd}`;
+  return `$ ${normalizeCommand(command)}`;
 }
@@
 function cleanCommand(command: string): string {
-  let cmd = command.replace(/\n/g, ' ').trim();
-
-  if (cmd.includes(';')) {
-    const parts = cmd.split(';');
-    cmd = parts[parts.length - 1].trim();
-  }
-
-  const envVarPattern = /^(\s*\w+=[^\s]*\s+)+/;
-  if (envVarPattern.test(cmd)) {
-    cmd = cmd.replace(envVarPattern, '').trim();
-  }
-
-  if (cmd.length > 100) {
-    cmd = cmd.slice(0, 100) + '...';
-  }
-
-  return cmd;
+  return normalizeCommand(command);
 }

Also applies to: 194-215

AI Agent added 3 commits January 16, 2026 19:43
The streaming parser splits on newlines. Text without a trailing
newline stays in the buffer and gets concatenated with the next chunk.
This caused "Let me check...[Glob]" on one line when text was followed
by a tool call.

Now text events always end with a newline, so the streaming parser
treats each chunk as a complete line before the next chunk arrives.
- Update output-formatting tests for new newline behavior:
  - Text now always ends with newline for streaming compatibility
  - Tool calls only get leading newline when following non-newline content
- Add droid outputParser tests (12 tests):
  - Top-level tool_call parsing
  - Top-level tool_result parsing
  - Message and completion event parsing
  - ANSI stripping
- Add streaming output-parser tests (14 tests):
  - Line buffering and newline handling
  - Trailing newline stripping to avoid doubles
  - JSONL event parsing
  - Droid-specific format support

All 943 tests pass.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/plugins/agents/output-formatting.test.ts`:
- Around line 19-28: The test 'all color values are ANSI escape codes' uses
regex literals with the control char \x1b which triggers Biome's
noControlCharactersInRegex; update the expect assertions for COLORS.blue,
purple, cyan, green, yellow, pink, and muted to use the RegExp constructor
instead of literal regexes (e.g. use new RegExp('^\\\\x1b\\\\[\\\\d+m$') in the
toMatch calls) while leaving the COLORS.reset assertion as-is; modify the
assertions in the test file referencing COLORS.* and the test name to use
RegExp(...) so the control character is represented as a string.
♻️ Duplicate comments (4)
src/plugins/agents/output-formatting.ts (3)

183-185: Avoid misleading ellipsis when edit strings are not truncated.

The edit preview always appends “…”, even when the original strings are ≤ 50 chars.

♻️ Suggested fix
-  if (input.old_string && input.new_string) {
-    parts.push(`edit: "${input.old_string.slice(0, 50)}..." → "${input.new_string.slice(0, 50)}..."`);
-  }
+  if (input.old_string && input.new_string) {
+    const oldPreview =
+      input.old_string.length > 50 ? input.old_string.slice(0, 50) + '...' : input.old_string;
+    const newPreview =
+      input.new_string.length > 50 ? input.new_string.slice(0, 50) + '...' : input.new_string;
+    parts.push(`edit: "${oldPreview}" → "${newPreview}"`);
+  }

280-285: Keep segment-based edit previews consistent with actual truncation.

Same unconditional ellipsis issue exists in the segment renderer.

♻️ Suggested fix
-  if (input.old_string && input.new_string) {
-    segments.push({ text: ' edit: "', color: 'muted' });
-    segments.push({ text: input.old_string.slice(0, 50) + '...', color: 'pink' });
-    segments.push({ text: '" → "', color: 'muted' });
-    segments.push({ text: input.new_string.slice(0, 50) + '...', color: 'green' });
-    segments.push({ text: '"', color: 'muted' });
-  }
+  if (input.old_string && input.new_string) {
+    const oldPreview =
+      input.old_string.length > 50 ? input.old_string.slice(0, 50) + '...' : input.old_string;
+    const newPreview =
+      input.new_string.length > 50 ? input.new_string.slice(0, 50) + '...' : input.new_string;
+    segments.push({ text: ' edit: "', color: 'muted' });
+    segments.push({ text: oldPreview, color: 'pink' });
+    segments.push({ text: '" → "', color: 'muted' });
+    segments.push({ text: newPreview, color: 'green' });
+    segments.push({ text: '"', color: 'muted' });
+  }

426-429: Replace ANSI regex literal to satisfy Biome control‑character lint.

Biome rejects control characters inside regex literals here.

✅ Lint-safe alternative
-const ANSI_REGEX = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]/g;
+const ANSI_REGEX = new RegExp(
+  '\\x1b\\[[0-9;?]*[a-zA-Z]|\\x1b\\][^\\x07]*\\x07|\\x1b[()][AB012]',
+  'g',
+);
src/plugins/agents/output-formatting.test.ts (1)

6-16: Replace require() with an ES import to satisfy lint.

ESLint forbids require() here (pipeline failure).

✅ Suggested fix
 import { describe, expect, test } from 'bun:test';
 import {
   COLORS,
   formatToolName,
   formatPath,
   formatCommand,
   formatError,
   formatPattern,
   formatUrl,
   formatToolCall,
+  processAgentEvents,
 } from './output-formatting.js';
 describe('processAgentEvents', () => {
-  // Import needed for these tests
-  const { processAgentEvents } = require('./output-formatting.js');
-
   test('displays text events with trailing newline', () => {

Also applies to: 269-272

AI Agent and others added 6 commits January 16, 2026 20:12
- Fix require() to ESM import in output-formatting.test.ts
- Surface tool result errors as error events in all parsers (claude, opencode, droid)
- Fix deepClone circular reference issue with WeakMap tracking
- Strip ANSI codes from FormattedSegment.text in output-parser.ts
- Only append "..." in edit preview when strings exceed 50 chars
- Fix slice(-0) bug that returned full array instead of empty
- Use RegExp constructor for ANSI_REGEX to avoid embedded control chars
- Add as const to test type literals for TypeScript inference
Tests that depend on bundled skills now gracefully skip when skills
aren't found. This handles CI environments where the skills directory
may not be accessible due to how bun test resolves import.meta.url.

The root cause is that the PR added 'set -o pipefail' to the CI workflow,
which correctly fails when bun test returns non-zero. Previously, main
was silently passing with 6 failing tests because tee's exit code (0)
was used instead of bun test's exit code.
…ions

- Add examples/math.ts with basic arithmetic functions
- Include proper TypeScript types for all parameters and return values
- Add JSDoc comments with @example for each function
- Add ABOUTME comment describing the module purpose

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
macOS and Linux have different `script` command syntax:
- Linux: script -q -c "command" /dev/null
- macOS: script -q /dev/null sh -c "command"

This fixes the "script: illegal option -- c" error when running
the droid agent on macOS.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@subsy subsy merged commit 91235aa into main Jan 16, 2026
9 checks passed
sakaman pushed a commit to sakaman/ralph-tui that referenced this pull request Feb 15, 2026
feat: Add sandbox isolation for agent execution
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant