fix(claude): emit plan events for TodoWrite during input streaming#1541
fix(claude): emit plan events for TodoWrite during input streaming#1541TimCrooker wants to merge 15 commits intopingdotgg:mainfrom
Conversation
When Claude calls TodoWrite, emit turn.plan.updated events during input streaming so the plan sidebar displays Claude's todos the same way it already works for Codex plan steps. Events are emitted alongside existing tool lifecycle events, not as a replacement. Also passes through the data field on item.completed activities to match item.updated behavior, and auto-opens the plan sidebar when plan steps arrive. Closes pingdotgg#1539
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Plan state now falls back to the most recent plan from any previous turn when the current turn has no plan activity, so TodoWrite tasks stay visible across follow-up messages. Simplified redundant isTodoTool check.
There was a problem hiding this comment.
Pull request overview
This PR wires Claude’s TodoWrite tool into the existing plan sidebar by emitting turn.plan.updated events during Claude input streaming, and includes several related UX/data-flow fixes so todos and task activity render consistently in the UI.
Changes:
- Emit
turn.plan.updatedduring Claudeinput_json_deltaprocessing forTodoWrite, without suppressing existing tool lifecycle events. - Persist/restore plan state across turns and auto-open the plan sidebar when plan steps arrive.
- Improve work log rendering (include
task.completed, prefer task payload summaries) and forwarddataforitem.completedactivities.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/web/src/session-logic.ts | Persist active plan across turns; include task.completed; adjust task entry label/tone logic. |
| apps/web/src/session-logic.test.ts | Add coverage for plan fallback and updated work log filtering/label behavior. |
| apps/web/src/components/ChatView.tsx | Auto-open plan sidebar when an active plan appears (respecting dismiss state). |
| apps/server/src/provider/Layers/ClaudeAdapter.ts | Emit turn.plan.updated events for TodoWrite during streamed tool input parsing; improve tool request summaries for agent tools. |
| apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | Forward payload.data for item.completed tool lifecycle activities. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Only force "thinking" tone for task.progress, not task.completed, so failed tasks preserve their error tone. Also check payload.detail for task labels since task.completed stores its summary there. Add regression test for failed task.completed rendering.
Use a sentinel string when turnId is null so the dismissed ref still gets set, preventing the auto-open effect from immediately reopening the sidebar.
Apply the same __dismissed__ sentinel to the onClose handler on the plan sidebar X button, matching the fix already applied to togglePlanSidebar.
Use the same turnKey fallback chain (activePlan.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__") in both the auto-open effect and the dismiss handlers so they always match.
Dynamically switch the sidebar label between "Plan" and "Tasks" based on context. When a proposed plan exists or the user is in plan mode, the label reads "Plan". Otherwise it reads "Tasks". Applies to the composer button, compact menu, sidebar badge, and aria labels.
…tail Only auto-open the sidebar for plans from the current turn, not fallbacks from previous turns. Add explicit parentheses to the label ternary for clarity. Skip detail assignment when the detail text is already used as the label to avoid duplication in the work log.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Quick summary of the iteration here since there are a few rounds of commits: Started by wiring TodoWrite into the existing plan event path so the sidebar actually shows Claude's tasks. Bugbot caught a few real issues (dismiss key mismatch, error tone override, label/detail duplication, auto-open on thread switch) each one got a focused fix. Midway through I thought this would need a dedicated task UI component separate from the plan sidebar, since the plan panel was designed around Codex's plan mode. Almost closed it to go rethink. Then realized the simpler answer: just dynamically label the sidebar "Tasks" vs "Plan" based on context. Same component, no new UI, no plan mode required. Users get task visibility without being forced into a workflow they didn't ask for. Current state: all Bugbot feedback resolved, tests passing, parentheses fix for the label ternary pushed. Ready for review. |
…into fix/claude-todowrite-plan-events
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| return todos | ||
| .filter((t): t is Record<string, unknown> => t !== null && typeof t === "object") | ||
| .map((todo) => ({ | ||
| step: typeof todo.content === "string" ? todo.content : "Task", |
There was a problem hiding this comment.
Empty todo content produces invalid plan step string
Low Severity
extractPlanStepsFromTodoInput sets step to todo.content when it's a string, but doesn't guard against empty or whitespace-only strings. The RuntimePlanStep schema requires step to be a TrimmedNonEmptyString. If Claude sends a todo with content: "" or content: " ", the resulting plan step would fail schema validation, potentially disrupting the streaming event pipeline. The fallback "Task" only applies when content is not a string at all.


What Changed
Claude's
TodoWritetool calls now emitturn.plan.updatedevents during input streaming so the plan sidebar shows task progress in real-time. The plan event fires alongside the existing tool lifecycle events, not instead of them.Related fixes:
item.completedactivities now forward thedatafield to the UI, matchingitem.updatedtask.completedentries show up in the work log (previously filtered out withtask.started)payload.summarywhen available instead of the generic activity summaryCloses #1539
Why
TodoWritegets classified asfile_changeinclassifyToolItemTypebecause the name contains "write". It renders as a generic "File change - TodoWrite: {raw JSON}" line in the work log. Noturn.plan.updatedevent gets emitted, so the plan sidebar never activates for Claude sessions. This is core functionality that works for Codex but is completely broken for Claude.There's an existing PR for this (#1387) that intercepts at tool result time and replaces the normal
item.updated/content.deltaemissions. This PR takes a different approach:input_json_deltameans the sidebar populates as Claude writes the input, not after the full round-trip.deriveActivePlanStatefalls back to the most recent plan from any previous turn. Without this, the sidebar clears every time you send a follow-up.Validated with
bun fmt,bun lint,bun typecheck, andbun run test(all passing).UI Changes
Before -- TodoWrite is invisible to the sidebar
Tasks exist but are completely invisible. There is no way to see them.
After -- Tasks stream in live without plan mode
Tasks are now streamed in real-time and persist between turns.
After -- Button label adapts to context
When the session has tasks but no plan, the button shows "Tasks":
When the session is in plan mode, the same button shows "Plan":
Checklist
Note
Medium Risk
Touches provider event emission and client-side derivation/rendering of plan/work-log state, which can affect real-time UI behavior across turns. Changes are localized but involve streaming/event ordering and sidebar auto-open logic that could introduce regressions in activity display.
Overview
Enables real-time plan/task step updates for Claude by parsing
TodoWriteinput_json_deltaduring streaming and emittingturn.plan.updatedalongside existing tool lifecycle events.Improves activity payloads and labeling:
item.completednow forwardspayload.data, agent/subagent tool requests prefer human-readabledescription/prompt, and task work-log entries now includetask.completed, usepayload.summary/payload.detailas the label, and rendertask.progresswith a thinking tone.Updates the web plan sidebar UX by persisting the latest plan across turns, auto-opening the sidebar when new steps arrive (while respecting per-turn dismissal), and dynamically labeling the sidebar/button as “Tasks” outside plan mode vs “Plan” in plan/proposed-plan contexts.
Written by Cursor Bugbot for commit 80dfdbd. This will update automatically on new commits. Configure here.
Note
Emit
turn.plan.updatedevents for TodoWrite tool calls during Claude input streamingTodoWritetool calls during input streaming and emitsturn.plan.updatedruntime events with extracted plan steps, parsed frominput.todos.deriveActivePlanStatefalls back to the most recent plan from a previous turn when no plan exists for the active turn.task.completeditems, and task progress entries show a 'thinking' tone withpayload.summary/payload.detailas the label.Macroscope summarized 80dfdbd.