Skip to content

Add shell input to the composer - #163

Merged
soorya-u merged 5 commits into
mainfrom
shell-input-composer-161
Aug 9, 2026
Merged

soorya-u merged 5 commits into
mainfrom
shell-input-composer-161

Conversation

@soorya-u

@soorya-u soorya-u commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Typing ! as the first character in a thread's composer arms shell input: border turns red, placeholder explains the mode, and submitting runs the command as a worker-side subprocess (Bun.spawn) instead of a chat message.
  • Output streams live to watching peers and persists as a new shell_execution conversation entry (command, stdout/stderr-tagged lines, exit code, start/complete times), rendered as an accordion row with a live status dot, inline stop control, and chevron.
  • No approval gate — the user typing and submitting the command is the authorization (see docs/adr/0024-shell-input-has-no-approval-gate.md).
  • Shell input runs concurrently with an agent turn in the same thread. ChatChunkSchema.turnId is now optional with a new shellExecutionId field, so a shell execution's chunks are never mistaken for an open turn by code that gates/queues chat turns or builds synthetic Turn records.
  • executeShellInput is fire-and-forget ({ shellExecutionId } returned immediately); output arrives async over the existing chat-chunk broadcast, reusing the per-thread seq/sub stamping already in queue/bus.ts.
  • cancelShellExecution is a dedicated RPC with its own per-row stop control, distinct from the composer's turn-scoped Stop button, since a turn and a shell execution can run at the same time.
  • CYRUS_SHELL_INPUT_TIMEOUT_MS (default 60s) bounds how long a spawned command may run before it's killed and reported as timed out.
  • Backspacing the composer to empty exits shell input with no dedicated key handler: shell input is armed reactively (plainText.startsWith("!")), so deleting the ! already flips it off.

Closes #161.

Test plan

  • bun run check:types — clean across all 17 packages
  • bun run test:unit — 154 vitest + 130 bun:test, 0 failures
  • New unit tests added: fold.ts (shell execution folding incl. the "no fake turn" invariant), thread-feed.ts (feed ordering), bus.ts (live fan-out + no replay-buffer leak), conversation-cache.ts (line merging/pruning), shell.ts's pumpLines (UTF-8 boundary handling)
  • /code-review run against ed64428...HEAD — spec axis clean, standards-axis findings (naming collision, misplaced shared component, complexity, try/catch vs Result) all fixed
  • Manual verification in a running app (not done in this sandboxed session — no live worker/controller pairing available)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added shell command execution directly from the chat composer using !-prefixed commands.
    • Streamed terminal output appears in the conversation with expandable results and status indicators.
    • Added controls to stop running commands and feedback for failures, timeouts, and cancellations.
    • Shell commands execute immediately without an approval prompt.
  • Documentation

    • Added guidance defining shell execution and shell input behavior.

Typing `!` as the first character in a thread's composer arms shell
input: border turns red, placeholder explains the mode, and submitting
runs the typed command as a worker-side subprocess instead of a chat
message. Output streams live to watching peers and persists as a new
`shell_execution` conversation entry (command, stdout/stderr-tagged
lines, exit code, start/complete times) rendered via the validated
"Variant A" design from the earlier prototype (accordion header with a
live status dot, inline stop control, and chevron; right-aligned to
read as user-authored).

Key design points carried over from the grilling session:
- No approval gate — the user typing and submitting the command is the
  authorization (see docs/adr/0024-shell-input-has-no-approval-gate.md).
- Shell input runs concurrently with an agent turn in the same thread;
  ChatChunkSchema's turnId is now optional with a new shellExecutionId
  field, so a shell execution's chunks are never mistaken for an open
  turn by code that gates/queues real chat turns (use-thread-turns.ts)
  or builds synthetic Turn records (fold.ts).
- executeShellInput is fire-and-forget (returns { shellExecutionId }
  immediately); output arrives async over the existing chat-chunk
  broadcast, reusing the per-thread seq/sub stamping in queue/bus.ts.
- cancelShellExecution is a dedicated RPC with its own per-row stop
  control, distinct from the composer's turn-scoped Stop button, since
  a turn and a shell execution can be running at the same time.
- CYRUS_SHELL_INPUT_TIMEOUT_MS (default 60s) bounds how long a spawned
  command may run before it's killed and reported as timed out.

Backspacing the composer back to empty exits shell input with no
dedicated key handler needed: shell input is armed reactively
(`plainText.startsWith("!")`), so deleting the `!` already flips it
off.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cyrus Ready Ready Preview Aug 9, 2026 8:52am

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds thread-scoped shell input and execution. It defines shell events and APIs, streams process output, stores shell executions separately from turns, adds cancellation and timeout handling, and renders terminal output in the chat feed.

Changes

Shell execution

Layer / File(s) Summary
Shell execution contracts
shared/schemas/..., shared/connections/..., shared/errors/..., CONTEXT.md, docs/adr/...
Defines shell event schemas, controller endpoints, execution statuses, view records, error types, and shell-input behavior.
Controller execution and event delivery
apps/cli/src/handlers/..., apps/cli/src/shell/..., apps/cli/src/queue/..., apps/cli/src/lib/env.ts
Starts sh -c executions, streams stdout and stderr, handles cancellation and timeouts, persists completion events, and publishes live chunks.
Conversation folding, caching, and feed ordering
shared/hooks/src/conversation/..., shared/utils/src/conversations/...
Correlates events by shellExecutionId, accumulates output, folds standalone executions, and inserts them into feed order.
Composer shell input integration
apps/web/src/hooks/chat/use-composer-editor.ts, apps/web/src/components/chat/composer/..., apps/web/src/components/chat/main/...
Arms shell mode for !-prefixed input, updates composer state, executes commands for the active thread, and restores failed commands.
Shell execution feed rendering
apps/web/src/components/chat/feed/..., apps/web/src/components/ui/terminal-output.tsx, apps/web/src/hooks/chat/use-shell-failure-toast.ts, apps/web/src/index.css, apps/web/package.json
Renders expandable terminal output with status indicators, cancellation controls, animations, failure toasts, and terminal styling.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Composer
  participant ThreadWorkspace
  participant Controller
  participant ShellProcess
  participant ConversationFeed
  User->>Composer: Enter !command
  Composer->>ThreadWorkspace: executeShellInput(command)
  ThreadWorkspace->>Controller: executeShellInput(threadId, command)
  Controller->>ShellProcess: spawn sh -c
  ShellProcess-->>Controller: stdout/stderr lines
  Controller-->>ConversationFeed: publish shell start, line, and end events
  ConversationFeed-->>User: render shell execution row
  User->>ConversationFeed: cancel running execution
  ConversationFeed->>Controller: cancelShellExecution(threadId, shellExecutionId)
  Controller->>ShellProcess: terminate process
Loading

Possibly related PRs

  • soorya-u/cyrus#6: Introduced the controller and RTC communication extended by these shell endpoints.
  • soorya-u/cyrus#23: Also modifies event delivery, conversation caching, controller contracts, and composer flow for streamed execution events.
  • soorya-u/cyrus#140: Refactored conversation folding, caching, feed ordering, and event sequencing extended here for shell executions.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The implementation covers shell mode, placeholder changes, RPC execution, timeout, cancellation, and disarming, but the required red border is not confirmed and appears terminal-themed. Set the composer border to red whenever shell mode is armed, and add a focused test or clear styling assertion for issue #161.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding shell input support to the composer.
Out of Scope Changes check ✅ Passed The changes support shell input, execution, streaming, cancellation, persistence, rendering, and related documentation without unrelated code changes.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch shell-input-composer-161

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (1)
apps/web/src/components/chat/feed/shell-execution-row.tsx (1)

23-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the cancelled status color with the summary line.

Line 23 treats every non-exited status as failed, so a cancelled execution shows a red dot. shellSummaryLine renders the same status with text-muted-foreground (lines 55-59). The two indicators disagree for a user-initiated stop.

🎨 Proposed change
-	const failed = execution.status !== "exited" || execution.exitCode !== 0;
+	if (execution.status === "cancelled") {
+		return <span className="size-2 shrink-0 rounded-full bg-muted-foreground" />;
+	}
+	const failed = execution.status !== "exited" || execution.exitCode !== 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/chat/feed/shell-execution-row.tsx` around lines 23 -
30, Update the failed-status logic in the shell execution row so cancelled
executions use the same neutral styling as shellSummaryLine, while preserving
red for actual failures and green for successful exits. Adjust the class
selection around the failed indicator without changing unrelated execution
statuses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/src/handlers/controller/shell.ts`:
- Around line 97-103: Update executeShellInput’s Bun.spawn launch path to use
the supported shell executable and argument conventions for the current worker
platform instead of always invoking "sh". Preserve cwd, stdout, and stderr
handling, reject unsupported worker OS values with the existing error result,
and add coverage for command launching and shell output on every supported
platform.
- Around line 118-153: Update the shell execution cancellation path around kill
and subprocess.exited to terminate the entire spawned process tree using the
supported strategy for each platform, rather than only calling
subprocess.kill(). Preserve pipe draining and await the tree’s exit before
finish persists shell_execution_end. Add an integration test using a cancellable
background child command that verifies both child termination and persistence of
the terminal event.

In `@apps/web/src/components/chat/feed/shell-execution-row.tsx`:
- Around line 96-103: Add disclosure semantics to both accordion toggle buttons
in the shell execution row, including the controls near the command and output
sections: set aria-expanded from the open state and aria-controls to the shared
outputId. Assign the matching outputId to the TerminalOutput wrapper so each
button references the expanded output container.
- Around line 108-119: Update the stop button handler in the running-state
branch of the shell execution row to await and inspect the Result returned by
cancelShellExecution. On failure, surface the contained error through the
component’s existing toast or notification mechanism while leaving the execution
state and button enabled so the user can retry.

In `@apps/web/src/components/chat/feed/terminal-output.tsx`:
- Around line 128-151: Keep the rendered tree stable in the wrappedChildren and
content paths by rendering ItemIndexContext.Provider around every child and
SequenceContext.Provider around the <pre> unconditionally. Pass the existing
null-resolving contextValue when sequence is disabled, and remove the
conditional branches that return unwrapped children or bare content.

In `@apps/web/src/hooks/chat/use-composer-editor.ts`:
- Around line 249-253: Move the shellInputArmed branch in the composer execution
flow before the hasAgents and hasAgentSelected guards. Ensure executeShellInput
can run with only threadId and command, without requiring an agent selection,
while preserving the existing command and onExecuteShell validation.
- Around line 207-214: Update restorePlainText to also persist the restored
command by calling setDraft with the same text content after restoring the
editor state. Preserve the existing originatingThreadId guard and local state
updates.

In `@shared/utils/src/conversations/fold.ts`:
- Around line 278-289: Update the shell execution start handling around the
state.shellExecutions.set call to merge start metadata into an existing record
instead of replacing it. Preserve existing lines, status, exitCode, and
completedAt when a fallback record from applyShellExecutionLine already exists,
while using the start event’s metadata for the execution identity and start
fields; add a test covering output arriving before shell_execution_start.

---

Nitpick comments:
In `@apps/web/src/components/chat/feed/shell-execution-row.tsx`:
- Around line 23-30: Update the failed-status logic in the shell execution row
so cancelled executions use the same neutral styling as shellSummaryLine, while
preserving red for actual failures and green for successful exits. Adjust the
class selection around the failed indicator without changing unrelated execution
statuses.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b96ab93-392f-417c-8b10-4ad3759550d0

📥 Commits

Reviewing files that changed from the base of the PR and between ed64428 and bdfda67.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • CONTEXT.md
  • apps/cli/src/handlers/controller/index.ts
  • apps/cli/src/handlers/controller/shell.test.ts
  • apps/cli/src/handlers/controller/shell.ts
  • apps/cli/src/lib/env.ts
  • apps/cli/src/queue/bus.test.ts
  • apps/cli/src/queue/bus.ts
  • apps/web/package.json
  • apps/web/src/components/chat/composer/index.tsx
  • apps/web/src/components/chat/feed/feed-entry-view.tsx
  • apps/web/src/components/chat/feed/shell-execution-row.tsx
  • apps/web/src/components/chat/feed/terminal-output.tsx
  • apps/web/src/components/chat/main/draft-workspace.tsx
  • apps/web/src/components/chat/main/thread-workspace.tsx
  • apps/web/src/hooks/chat/use-composer-editor.ts
  • apps/web/src/types/composer.ts
  • docs/adr/0024-shell-input-has-no-approval-gate.md
  • shared/connections/src/contracts/controller.ts
  • shared/hooks/src/conversation/conversation-cache.test.ts
  • shared/hooks/src/conversation/conversation-cache.ts
  • shared/hooks/src/conversation/use-shell-execution.ts
  • shared/hooks/src/conversation/use-thread-conversation.ts
  • shared/hooks/src/conversation/use-thread-turns.ts
  • shared/schemas/src/rtc/chat.ts
  • shared/schemas/src/view/index.ts
  • shared/utils/src/conversations/fold.test.ts
  • shared/utils/src/conversations/fold.ts
  • shared/utils/src/conversations/thread-feed.test.ts
  • shared/utils/src/conversations/thread-feed.ts

Comment thread apps/cli/src/handlers/controller/shell.ts Outdated
Comment thread apps/cli/src/handlers/controller/shell.ts Outdated
Comment thread apps/web/src/components/chat/feed/shell-execution-row.tsx
Comment thread apps/web/src/components/chat/feed/shell-execution-row.tsx
Comment thread apps/web/src/components/ui/terminal-output.tsx
Comment thread apps/web/src/hooks/chat/use-composer-editor.ts Outdated
Comment thread apps/web/src/hooks/chat/use-composer-editor.ts
Comment thread shared/utils/src/conversations/fold.ts
- New --terminal / --terminal-foreground CSS variables (#4af262): the
  composer border and primary send button now use terminal green while
  shell input is armed, instead of red (which read as an error state).
- Typing `!` as the first character now arms shell input and consumes
  the `!` itself rather than leaving it visible — the box shows just
  the command. Since shell input is no longer derivable from plainText
  once armed, it's real state now, with Backspace-on-empty-composer as
  the explicit disarm trigger (new KEY_BACKSPACE_COMMAND handling in
  composer-prompt-editor.tsx).
- ShellExecutionRow: dropped the "Process exited with code 0" summary
  line (redundant with the green status dot; nonzero-exit/timeout/
  cancelled/spawn_error summaries still show, since the dot alone can't
  say *why*), the $ icon is terminal green, and the inline stop control
  is red with a brighter hover state.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (2)
apps/web/src/hooks/chat/use-composer-editor.ts (2)

218-231: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle rejected submission actions.

If action() rejects, onFailure and setSending(false) do not run. The composer stays disabled after its input and draft were cleared.

Wrap the await in try/catch/finally. Restore the input for both an error Result and a rejected promise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/hooks/chat/use-composer-editor.ts` around lines 218 - 231,
Update runSubmission so rejected action() promises are caught, invoking
onFailure with originatingThreadId and restoring the cleared composer input;
also handle error Results the same way. Use finally to always call
setSending(false), including when action() rejects.

349-363: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Disarm shell mode when the input becomes empty.

After the final character is deleted, next is empty but shellInputArmed remains true. Text typed afterward is submitted as a shell command instead of a chat message.

Set shellInputArmed to false when shell input transitions to empty. Re-arm it in restorePlainText when a shell submission fails.

Proposed fix
 const restorePlainText = useCallback(
   (text: string, originatingThreadId: string) => {
     if (originatingThreadId !== threadIdRef.current) return;
+    setShellInputArmed(true);
     editorRef.current?.setPlainText(text);
     setPlainText(text);
     setHasContent(true);
   },
   []
 );

 function handlePlainTextChange(next: string) {
+  if (shellInputArmed && next === "") {
+    setShellInputArmed(false);
+  }
+
   if (onExecuteShell && !shellInputArmed && next.startsWith("!")) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/hooks/chat/use-composer-editor.ts` around lines 349 - 363,
Update handlePlainTextChange so an armed shell input is disarmed when next
becomes empty, while preserving the existing “!” arming behavior. Also update
restorePlainText to re-arm shell input after a failed shell submission, ensuring
subsequent typing is handled as shell input only in that failure-recovery path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/chat/composer/primary-action.tsx`:
- Around line 65-70: Update the busy-path action control in the component
containing shellInputArmed so an armed shell composer retains shell-specific
styling and the appropriate shell action label while an agent turn is active.
Mirror the conditional styling currently used by the idle control and update the
busy-path accessible label/text, preserving the existing non-shell busy
behavior.

In `@apps/web/src/index.css`:
- Around line 237-239: Define a darker terminal-border token alongside
--terminal in the theme variables, and update the composer’s border-terminal
usage to reference it. Keep --terminal unchanged for the filled primary action
and retain --terminal-foreground for its text.

---

Outside diff comments:
In `@apps/web/src/hooks/chat/use-composer-editor.ts`:
- Around line 218-231: Update runSubmission so rejected action() promises are
caught, invoking onFailure with originatingThreadId and restoring the cleared
composer input; also handle error Results the same way. Use finally to always
call setSending(false), including when action() rejects.
- Around line 349-363: Update handlePlainTextChange so an armed shell input is
disarmed when next becomes empty, while preserving the existing “!” arming
behavior. Also update restorePlainText to re-arm shell input after a failed
shell submission, ensuring subsequent typing is handled as shell input only in
that failure-recovery path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fd41dc3c-3dcf-4f5e-9f47-b6de6cd4db3c

📥 Commits

Reviewing files that changed from the base of the PR and between bdfda67 and b9e7f39.

📒 Files selected for processing (6)
  • apps/web/src/components/chat/composer/composer-prompt-editor.tsx
  • apps/web/src/components/chat/composer/index.tsx
  • apps/web/src/components/chat/composer/primary-action.tsx
  • apps/web/src/components/chat/feed/shell-execution-row.tsx
  • apps/web/src/hooks/chat/use-composer-editor.ts
  • apps/web/src/index.css
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/components/chat/feed/shell-execution-row.tsx
  • apps/web/src/components/chat/composer/index.tsx

Comment thread apps/web/src/components/chat/composer/primary-action.tsx
Comment thread apps/web/src/index.css
Drop the redundant summary line since the status dot already conveys
outcome; move exit-code detail into a tooltip instead. Hide the
accordion/chevron when a command produced no output, toast timeout
and spawn errors instead of rendering an extra row state, and drop
the status dot entirely for cancelled commands since there's no
outcome to convey.
@soorya-u

soorya-u commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Re the two coderabbit findings on apps/cli/src/handlers/controller/shell.ts (hardcoded sh -c, and subprocess.kill() not killing the process tree): both are real gaps, but cross-platform shell resolution and process-group teardown are bigger than issue #161's scope (adding the composer trigger + feed UI). Filing as follow-up work rather than blocking here.

…, dark-mode contrast

- Remove aria-expanded/aria-controls gap on the shell row accordion
- Surface cancelShellExecution failures via toast instead of discarding the result
- Keep TerminalOutput's provider tree stable across the running->done transition so completed output no longer flashes/re-animates
- Persist the restored shell command to the draft store on a failed submission
- Decouple shell submission from agent-selection gates (no agent required to run a shell command), including composer canSend and the busy-path "Add to queue" control
- Disarm shell input whenever the composer empties by any means, not just single-character backspace; drop the now-dead KEY_BACKSPACE_COMMAND wiring it relied on
- Merge a late shell_execution_start into an existing fallback record instead of clobbering accumulated output
- Dim the terminal-green accent for dark mode only via a new --terminal-border token; light mode keeps the original color

Also drop a few stray comments per direct feedback.
Comment thread apps/cli/src/handlers/controller/shell.ts Outdated
Comment thread apps/cli/src/handlers/controller/shell.ts Outdated
Comment thread apps/cli/src/handlers/controller/shell.ts
Comment thread apps/web/src/components/chat/feed/shell-execution-row.tsx Outdated
Comment on lines +23 to +24
const ItemIndexContext = createContext<number | null>(null);
const useItemIndex = () => useContext(ItemIndexContext);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

why do we need this? cant we use useId

Comment thread apps/web/src/components/ui/terminal-output.tsx
Comment thread shared/hooks/src/conversation/use-shell-execution.ts
Comment thread shared/utils/src/conversations/fold.ts Outdated
@soorya-u

soorya-u commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Re `terminal-output.tsx` — `ItemIndexContext`/`useItemIndex` (now in `components/ui/terminal-output.tsx`): can't swap this for `useId()`. `useId()` returns an opaque, unique string per component instance — it has no ordering. The sequencing logic here needs to know each `AnimatedSpan`'s ordinal position among its siblings (`sequence.activeIndex === itemIndex`) so it can animate them in order, one completing before the next starts. An opaque ID can't answer "am I the 3rd item" the way a numeric index can. Left as-is; happy to reconsider if there's a different concern I'm missing.

…lacement

- Add a shell error module in shared/errors (ShellSpawnFailedError, ShellStreamFailedError) instead of ad-hoc toError, mirroring the turn/connection modules
- Refine use-shell-execution's error conversion to extract a real message from unknown causes instead of blindly stringifying
- Move shell domain logic (pumpLines, runShellExecution, active-execution tracking) out of the oRPC handler into apps/cli/src/shell/, mirroring the git.ts/git/ split; the handler is now a thin oRPC wrapper
- for(;;) -> while(true) in pumpLines
- Move useShellFailureToast out of the row component into hooks/chat
- Move terminal-output.tsx into components/ui (it's a trimmed magicui Terminal, not feed-specific)
- Simplify fold.ts's applyEvent to check shellExecutionId once instead of in each of the three shell-event branches

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/web/src/components/chat/composer/primary-action.tsx (1)

76-81: ⚠️ Potential issue | 🟡 Minor

Keep the idle shell label consistent.

When shellInputArmed is true, the terminal styling is active, but the button still exposes "Send message" at Line 75. Use "Run command" and "Running command" while sending is true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/chat/composer/primary-action.tsx` around lines 76 -
81, Update the button label logic near the primary action’s `shellInputArmed`
styling so shell mode uses “Run command” when idle and “Running command” when
`sending` is true, while preserving the existing “Send message” labels for
normal chat mode.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/src/shell/run.ts`:
- Around line 136-160: Update the streamed.isErr() handling around pumpLines so
a stream failure terminates the subprocess and awaits subprocess.exited before
cleanup. Keep timeoutHandle active and retain the shellExecutionId in
activeShellExecutions until the child has exited, then clear and remove them
before calling finish. Add coverage that makes one output stream fail while the
command remains running.
- Around line 64-73: Update the shell output handling around emitLine and the
related lines 80–101 flow to enforce a bounded byte or line limit before
appending to lines or publishing through context.eventBus. Once the limit is
reached, stop buffering and emitting further output, and record a terminal
truncation/output-limit state so callers can distinguish capped output from
normal completion.

In `@apps/web/src/hooks/chat/use-shell-failure-toast.ts`:
- Around line 13-18: Update the status handling in useShellFailureToast to show
a failure toast when execution.status is "exited" and execution.exitCode is
non-zero, matching ShellStatusDot’s failure condition. Preserve the existing
timeout and spawn_error notifications, and include the command in the new toast
message.

---

Duplicate comments:
In `@apps/web/src/components/chat/composer/primary-action.tsx`:
- Around line 76-81: Update the button label logic near the primary action’s
`shellInputArmed` styling so shell mode uses “Run command” when idle and
“Running command” when `sending` is true, while preserving the existing “Send
message” labels for normal chat mode.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 089826b3-dbca-4c39-93db-1feba94163e5

📥 Commits

Reviewing files that changed from the base of the PR and between b9e7f39 and 3a656c0.

📒 Files selected for processing (16)
  • apps/cli/src/handlers/controller/shell.ts
  • apps/cli/src/shell/run.test.ts
  • apps/cli/src/shell/run.ts
  • apps/web/src/components/chat/composer/index.tsx
  • apps/web/src/components/chat/composer/primary-action.tsx
  • apps/web/src/components/chat/feed/shell-execution-row.tsx
  • apps/web/src/components/ui/terminal-output.tsx
  • apps/web/src/hooks/chat/use-composer-editor.ts
  • apps/web/src/hooks/chat/use-shell-failure-toast.ts
  • apps/web/src/index.css
  • shared/errors/src/common.ts
  • shared/errors/src/shell.ts
  • shared/hooks/src/conversation/use-shell-execution.ts
  • shared/utils/src/conversations/fold.test.ts
  • shared/utils/src/conversations/fold.ts
  • shared/utils/src/conversations/thread-feed.ts
💤 Files with no reviewable changes (1)
  • shared/utils/src/conversations/thread-feed.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/src/index.css
  • shared/utils/src/conversations/fold.test.ts
  • apps/web/src/components/chat/composer/index.tsx
  • shared/utils/src/conversations/fold.ts
  • apps/web/src/hooks/chat/use-composer-editor.ts

Comment thread apps/cli/src/shell/run.ts
Comment on lines +64 to +73
const lines: ShellExecutionLine[] = [];

function emitLine(line: ShellExecutionLine): void {
lines.push(line);
context.eventBus.publish({
threadId,
shellExecutionId,
seq: 0,
event: { type: "shell_execution_line", lines: [line] },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Bound shell output before buffering and persistence.

lines has no byte or line limit. A command such as yes can exhaust worker memory and flood the event bus well before the 60-second timeout. Define an output limit, expose a truncation or output-limit terminal state, and stop buffering after the limit.

Also applies to: 80-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/shell/run.ts` around lines 64 - 73, Update the shell output
handling around emitLine and the related lines 80–101 flow to enforce a bounded
byte or line limit before appending to lines or publishing through
context.eventBus. Once the limit is reached, stop buffering and emitting further
output, and record a terminal truncation/output-limit state so callers can
distinguish capped output from normal completion.

Comment thread apps/cli/src/shell/run.ts
Comment on lines +136 to +160
const streamed = await Result.tryPromise({
try: async () => {
await Promise.all([
pumpLines(subprocess.stdout, "stdout", emitLine),
pumpLines(subprocess.stderr, "stderr", emitLine),
]);
return await subprocess.exited;
},
catch: (cause) => shellStreamFailed(shellErrorMessageFromUnknown(cause)),
});

clearTimeout(timeoutHandle);
activeShellExecutions.delete(shellExecutionId);

if (streamed.isErr()) {
log.error({
kind: "shell_execution_stream_failed",
error: streamed.error,
shellExecutionId,
threadId,
});
await finish(killedFor ?? "exited", null);
return;
}
await finish(killedFor ?? "exited", killedFor ? null : streamed.value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep process cleanup active after a stream failure.

If either pumpLines call fails, Promise.all rejects before subprocess.exited is awaited. The code then clears timeoutHandle and removes the active execution entry. The child process can continue without timeout enforcement or cancellation support.

On this error path, terminate the subprocess, wait for its exit, and only then clear the timeout and remove the active entry. Add a test that forces one output stream to fail while the command remains running.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/shell/run.ts` around lines 136 - 160, Update the
streamed.isErr() handling around pumpLines so a stream failure terminates the
subprocess and awaits subprocess.exited before cleanup. Keep timeoutHandle
active and retain the shellExecutionId in activeShellExecutions until the child
has exited, then clear and remove them before calling finish. Add coverage that
makes one output stream fail while the command remains running.

Comment on lines +13 to +18
if (execution.status === "timeout") {
toast.error(`Command timed out: ${execution.command}`);
} else if (execution.status === "spawn_error") {
toast.error(`Failed to start command: ${execution.command}`);
}
}, [execution.status, execution.command]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Notify when an exited command fails.

ShellStatusDot marks an exited execution with a non-zero exit code as failed. This hook only notifies for timeouts and spawn errors. Add a failure toast for non-zero exit codes.

Proposed fix
 	if (execution.status === "timeout") {
 		toast.error(`Command timed out: ${execution.command}`);
 	} else if (execution.status === "spawn_error") {
 		toast.error(`Failed to start command: ${execution.command}`);
+	} else if (execution.status === "exited" && execution.exitCode !== 0) {
+		toast.error(
+			`Command exited with code ${execution.exitCode}: ${execution.command}`
+		);
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (execution.status === "timeout") {
toast.error(`Command timed out: ${execution.command}`);
} else if (execution.status === "spawn_error") {
toast.error(`Failed to start command: ${execution.command}`);
}
}, [execution.status, execution.command]);
if (execution.status === "timeout") {
toast.error(`Command timed out: ${execution.command}`);
} else if (execution.status === "spawn_error") {
toast.error(`Failed to start command: ${execution.command}`);
} else if (execution.status === "exited" && execution.exitCode !== 0) {
toast.error(
`Command exited with code ${execution.exitCode}: ${execution.command}`
);
}
}, [execution.status, execution.command]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/hooks/chat/use-shell-failure-toast.ts` around lines 13 - 18,
Update the status handling in useShellFailureToast to show a failure toast when
execution.status is "exited" and execution.exitCode is non-zero, matching
ShellStatusDot’s failure condition. Preserve the existing timeout and
spawn_error notifications, and include the command in the new toast message.

@soorya-u
soorya-u merged commit 884002a into main Aug 9, 2026
9 checks passed
@soorya-u
soorya-u deleted the shell-input-composer-161 branch August 9, 2026 08:55

This branch was successfully deployed

1 active deployment
Preview — 3a656c00 Deployed Aug 9, 2026 by vercel[bot]
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.

Add a shell mode on !on composer.

1 participant