Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Use the Cursor provider (
@cursor/sdk1.2.0) and start a turn. - Ask the agent to run a command that prints over several seconds, e.g.
for i in 1 2 3 4 5; do echo "tick $i"; sleep 2; done. - Expand the command row while the command is still running.
Expected behavior
Output appears incrementally while the command runs, the same way it does for Codex and Claude Agent. The expanded row should show tick 1, tick 2, … as they are printed.
Actual behavior
The expanded command row stays empty for the whole run. The complete output appears at once when the tool call finishes, so a long command looks hung.
This affects every Cursor shell command, including a real ~3 minute remote deploy where the whole log landed only at completion.
Impact
Major degradation or frequent failure
Version or commit
main @ cefb544 (also reproduced on 0.1.7-nightly.20261103.4119)
Environment
Linux, web client, Cursor provider via @cursor/sdk 1.2.0, default Cursor model, local mode.
Logs or stack traces
Redacted reproduction log. The command emits 5 lines over ~10s; T3 shows nothing until the end.
[12:00:00] INFO running command
[12:00:10] INFO command finished (5 lines of stdout)
# T3 UI while running: command row expanded, no output
# T3 UI after completion: all 5 lines appear at once
Root cause is in the adapter, not the transport. Cursor sends the delta as a protobuf oneof:
{ "case": "stdout", "value": { "data": "tick 1\n" } }
{ "case": "stderr", "value": { "data": "" } }
{ "case": "exit", "value": { "code": 0, "aborted": false } }packages/provider-cursor/src/server/adapter.ts (shellOutputText, ~L1975) only looks at
event.text, event.output, event.data, event.stdout, event.stderr, event.chunk, so it
returns "". shell-output-delta (~L2048) then returns early at:
const chunk = shellOutputText(update.event);
if (chunk.length === 0) return;So command_output.delta is never emitted for Cursor, while the final output still arrives through
the completion fallback in emitToolArtifacts (cursorToolOutputText(toolCall) || streamedOutput).
The SDK does stream live: @cursor/sdk dist/esm/336.js runs handleShellStream concurrently with
the tool call and sends one shellOutputDelta update per stdout/stderr chunk.
Workaround
Run the command outside T3 (terminal/ssh) and paste the result, or keep the row collapsed and wait
for the completion output.
Activity
Zburgers commentedon Nov 3, 2026
Proposed implementation plan
Posted as the working plan for this issue. No code is implemented yet.
Cursor live shell output — Implementation Plan
Goal: Cursor shell tool calls emit
command_output.deltawhile the command runs, so anexpanded command row streams output live instead of dumping everything on completion.
Architecture:
@cursor/sdkalready emits oneshell-output-deltainteraction update perstdout/stderr chunk, shaped
{ case: "stdout" | "stderr", value: { data } }. T3 receives everyupdate (
adapter.ts→onDelta→handleInteractionUpdate) butshellOutputTextreads flat keys,returns
"", andadapter.ts:2055drops the chunk. Fix the extractor; the hub, RPC, capability,and UI already work.
Tech Stack: TypeScript, Effect,
@effect/vitest, Cursor replay harness,vp.Constraints and non-goals
@cursor/sdkornode_modules. It is closed source and already correct.packages/contracts,apps/server, orapps/webchanges.CommandOutputHub,subscribeCommandOutput, andCommandOutputPanelalready stream for Codex/Claude.nested task updates (
NestedTaskUpdateSchemacomment).exitpayloads ({ code, aborted }) carry no text and must be ignored.Invariants the fix must preserve
chunkis still appended toshell.streamedOutput(bounded tail, used as completion fallback).command_output.deltaevent per non-empty chunk, with the sameitemIdderivation.cursorToolOutputText(toolCall) || streamedOutput.Baseline (must fail before the fix)
Expected on
main: FAIL —the running command must stream its output.Task 1 — Add the failing Cursor replay fixture
Files:
apps/server/src/orchestration-v2/testkit/fixtures/command_output_streaming/cursor_transcript.ndjsonapps/server/src/orchestration-v2/testkit/fixtures/command_output_streaming/output.ts:17-64apps/server/src/orchestration-v2/testkit/fixtures/index.ts:1883-1905Step 1: Author the transcript
The replay harness accepts hand-authored frames; model it on
fixtures/turn_interrupt_mid_tool/cursor_transcript.ndjson. Required order:{"type":"transcript_start","provider":"cursor","protocol":"cursor-agent-sdk.local","version":"1","scenario":"command_output_streaming","metadata":{"generatedBy":"hand-authored","nativeAgentId":"agent-command-output-streaming"}} {"type":"expect_outbound","label":"agent.open","frame":{"type":"agent.open","operation":"create","options":{"model":{"id":"composer-2.5"},"hasName":true,"mode":"agent","local":{"hasCwd":true,"autoReview":false,"settingSources":["project","user","team","mdm","plugins"],"sandboxEnabled":true,"enableAgentRetries":true}}}} {"type":"emit_inbound","label":"agent.opened","frame":{"type":"agent.opened","agentId":"agent-command-output-streaming"}} {"type":"expect_outbound","label":"run.start:1","frame":{"type":"run.start","message":"<the COMMAND_OUTPUT_STREAMING_PROMPT text>","options":{"model":{"id":"composer-2.5"},"mode":"agent"}}} {"type":"emit_inbound","label":"run.started:1","frame":{"type":"run.started","runId":"run-command-output-streaming","agentId":"agent-command-output-streaming"}} {"type":"emit_inbound","label":"tool-call-started","frame":{"type":"interaction.update","runId":"run-command-output-streaming","update":{"type":"tool-call-started","callId":"tool_command_output_streaming","toolCall":{"type":"shell","args":{"command":"printf 'cursor line 1\\n'; sleep 1; printf 'cursor line 2\\n'; sleep 1; printf '\\033[0;32mcursor line 3\\033[0m\\n'; sleep 1; printf 'cursor line 4\\nprogress 100%\\n'","timeout":30000}},"modelCallId":"model-call-command-output-streaming"}}} {"type":"emit_inbound","label":"shell-stdout-1","frame":{"type":"interaction.update","runId":"run-command-output-streaming","update":{"type":"shell-output-delta","event":{"case":"stdout","value":{"data":"cursor line 1\n"}}}}} {"type":"emit_inbound","label":"shell-stdout-2","frame":{"type":"interaction.update","runId":"run-command-output-streaming","update":{"type":"shell-output-delta","event":{"case":"stdout","value":{"data":"cursor line 2\n"}}}}} {"type":"emit_inbound","label":"shell-stdout-3","frame":{"type":"interaction.update","runId":"run-command-output-streaming","update":{"type":"shell-output-delta","event":{"case":"stdout","value":{"data":"\u001b[0;32mcursor line 3\u001b[0m\n"}}}}} {"type":"emit_inbound","label":"shell-stdout-4","frame":{"type":"interaction.update","runId":"run-command-output-streaming","update":{"type":"shell-output-delta","event":{"case":"stdout","value":{"data":"cursor line 4\nprogress 100%\n"}}}}} {"type":"emit_inbound","label":"shell-exit","frame":{"type":"interaction.update","runId":"run-command-output-streaming","update":{"type":"shell-output-delta","event":{"case":"exit","value":{"code":0,"aborted":false}}}}} {"type":"emit_inbound","label":"tool-call-completed","frame":{"type":"interaction.update","runId":"run-command-output-streaming","update":{"type":"tool-call-completed","callId":"tool_command_output_streaming","toolCall":{"type":"shell","args":{"command":"..."},"result":{"status":"success","value":{"exitCode":0,"signal":"","stdout":"cursor line 1\ncursor line 2\ncursor line 3\ncursor line 4\nprogress 100%\n","stderr":"","executionTime":4000}}},"modelCallId":"model-call-command-output-streaming"}}} {"type":"emit_inbound","label":"turn-ended","frame":{"type":"interaction.update","runId":"run-command-output-streaming","update":{"type":"turn-ended"}}} {"type":"emit_inbound","label":"run.completed:1","frame":{"type":"run.completed","result":{"id":"run-command-output-streaming","requestId":"model-call-command-output-streaming","status":"finished","model":{"id":"composer-2.5"},"durationMs":4500}}} {"type":"expect_outbound","label":"agent.close","frame":{"type":"agent.close","agentId":"agent-command-output-streaming"}}Use the exact text of
COMMAND_OUTPUT_STREAMING_PROMPTfromapps/server/src/orchestration-v2/testkit/fixtures/shared.tsinrun.start.message, otherwise theuser-message assertion fails.
Step 2: Parameterize the assertion
In
output.ts, extract the provider-neutral body intoassertCommandOutputStreamingLines(input)with fields
{ result, transcript, liveLines, liveAnsi?, excludedLiveLines? }. Keep two exports:Move the "Codex omits pre-yield command output" comment into the Codex wrapper.
Step 3: Register the provider
In
fixtures/index.ts, add tocommand_output_streaming.providers:Also import
assertCursorCommandOutputStreamingOutputfrom./command_output_streaming/output.ts.Step 4: Verify it fails for the right reason
Expected: FAIL at
the running command must stream its output(chunks === 0).Step 5: Commit
git add apps/server/src/orchestration-v2/testkit/fixtures/command_output_streaming/ \ apps/server/src/orchestration-v2/testkit/fixtures/index.ts git commit -m "test(server): cover Cursor live shell output streaming"Task 2 — Read the Cursor
{ case, value }payloadFiles:
packages/provider-cursor/src/server/adapter.ts:1975-1984Step 1: Replace the extractor
The trailing flat-key path stays as a fallback for SDK builds that emit unwrapped events.
Step 2: Run the fixture
Expected: PASS for
command_output_streaming/cursor, and the existingcommand_output_streaming/codex+command_output_streaming/claudeAgentstill pass.Step 3: Commit
git add packages/provider-cursor/src/server/adapter.ts git commit -m "fix(provider-cursor): stream live shell output"Task 3 — Verification
Run these, in order:
Expected: all pass;
check:changedreports no format, lint, knip, or typecheck errors.Manual check (one real Cursor turn, cheapest model):
for i in 1 2 3 4 5; do echo "tick $i"; sleep 2; donetick 1…tick 5to appear incrementally, then the completed output to remain.If only the final output appears, the transcript test passed for the wrong reason: confirm the
event.case/event.value.datashape in the provider event log before changing anything else.Risks
exitchunk appended as textkind === "string"early return; fixture includes anexitframeoutputRollback
Revert the two commits. No migrations, contracts, or persisted state change.
Zburgers commentedon Nov 3, 2026
Implemented
PR: #30229 —
fix(provider-cursor): stream live shell outputTwo details from the plan above changed while writing it against the replay harness:
shellOutputTextdecodes the oneof directly and returns""for any case other thanstdout/stderr. The flat-key fallback is gone: it never matched a real frame, and keeping it would hide the next SDK shape change instead of failing the fixture.endsWithcheck already requires every line, and the Codex/Claude expectations stay in one place. The change is the transcript plus one provider row.The transcript includes an
exitframe after the stdout chunks, so treating the exit status as text fails the test, and a\r-rewritten progress line so the live tail is checked after terminal normalization.Verification
Before the fix (
main@cefb544af3):After:
Zburgers commentedon Nov 4, 2026
Hi maintainers, let me know if theres anything you'd like me to do here.
I'd love to work more on this issue and become a contributor to the t3 community as well, the pr is already done and good to go.
would love a vouch!