Skip to content

fix(chat): keep plan responses visible in the transcript - #342

Merged
heath-s merged 3 commits into
mainfrom
fix/plan-no-show
Aug 6, 2026
Merged

fix(chat): keep plan responses visible in the transcript#342
heath-s merged 3 commits into
mainfrom
fix/plan-no-show

Conversation

@heath-s

@heath-s heath-s commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

A plan shown with "shall I proceed?" disappeared from the conversation as soon as the user replied, and any assistant content produced after the plan was swallowed with it.

Changes

Fixes

  • chat: keep plan responses in the transcript as a dedicated plan card so an approved or revised plan stays readable after the floating plan viewer closes.
  • provider: stop folding post-plan content (follow-up text, tool calls, pending approvals) into the hidden plan message — a renderable event arriving after a plan now starts a fresh assistant message.
  • chat: render already-stored merged plan records as card + existing parts, so conversations saved before this fix become visible again.

Refactors / Chores

  • Add resolvePlanMessagePresentation in chat-panel.utils.ts and a new ConversationPlanCard component.

Test Evidence

  • bun test tests/provider-event-replay.test.ts tests/chat-panel-utils.test.ts — 86 pass, 0 fail. 7 new tests pin the regression (written test-first).
  • bun test (full) — 3,168 pass. The 12 failures are byte-identical to the pre-change baseline (executable path discovery, env, 5s git timeout).
  • bun run check:max-lines-ratchet / check:switch-exhaustiveness / check:doc-paths / check:reliability-gates — all pass.
  • bun run typecheck — the 3 remaining errors are pre-existing OrbState mismatches in turn-activity.utils.ts (a thinking-orbs dependency type, untouched here) and reproduce at HEAD without this change.
  • Verified in the real app shell via the Playwright renderer harness, seeded with the actual stored plan record (planText 6,765 chars / content 9,300 chars / 24 parts). Before the fix the legacy record rendered only two user messages — the entire assistant turn was gone; after the fix the plan card renders and the 24 previously hidden parts render below it.

Notes

  • The floating PlanViewer is unchanged and keeps its approve/revise controls; this PR only adds the persistent transcript copy.
  • plan_ready re-presentation still updates the plan message in place rather than starting a new one.
  • Under the plan card, interim parts (tool_use / system_event) are collapsed by default and only the final text is expanded. That is the existing showInterimMessages behaviour for assistant traces, not something introduced here.
  • No new theme tokens: only existing semantic tokens (border, card, primary), so no theme file changes were needed.

🤖 Generated with Claude Code

`isPlanResponse` was effectively a "hide this message" flag, while the only
renderer for a plan was the floating `PlanViewer` — which closes once the
task moves past plan review. The reviewed plan therefore disappeared from
the conversation as soon as the user replied.

Worse, everything the agent produced after the plan (follow-up text, tool
calls, even pending approvals) was appended to the same hidden message and
vanished with it. One real record carried a 6,765-char plan alongside 24
unrendered parts.

- provider-event-replay: finalize the plan message and start a fresh
  assistant message when a renderable event arrives after a plan.
  `plan_ready` still updates the plan in place.
- ChatPanel: drop the `isPlanResponse` filter and render plans through a
  new `ConversationPlanCard` so they persist in the transcript. The
  floating `PlanViewer` keeps its approve/revise controls.
- chat-panel.utils: `resolvePlanMessagePresentation` shows the card alone
  for fresh plan messages, and card + existing parts for legacy merged
  records so already-stored conversations become readable again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@heath-s
heath-s requested a review from a team as a code owner August 6, 2026 04:54
@heath-s
heath-s requested review from paul-seo73 and png-1105 and removed request for a team August 6, 2026 04:54

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92d01c80ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1065 to +1070
target = createStreamingAssistantMessage({
taskId: args.taskId,
count: current.length + messageIndexOffset,
provider: args.provider,
model: args.model,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve native turn metadata when splitting after a plan

When provider_turn or history_boundary is emitted before a plan and its follow-up content—as the provider adapters do for assistant content—the metadata is attached to the plan message, but this newly created message starts without nativeProviderSessionId, nativeProviderTurnId, or providerBoundary. Consequently, the visible post-plan response is treated as predating native turn tracking, disabling its fork/rollback actions, while the plan row incorrectly retains the boundary for the entire provider turn. Transfer the turn metadata to the new message, or otherwise ensure the split rows continue to represent the same native turn correctly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 3e262b6.

Reproduced the exact sequence through replayProviderEventsToTaskState: provider_turn(turn-a)plan_readyprovider_turn(turn-b)text produced a plan row carrying turn-b and a follow-up row with no native turn at all, so buildConversationTurnActionStateByMessageId returned fork.enabled: false with "This response predates native turn tracking" on the only visible row of the turn.

Rather than copying the (already clobbered) metadata forward, the split now happens when the metadata arrives: a plan row is sealed once presented, so a provider_turn or history_boundary announcing a different native turn opens the follow-up message and lands there, leaving the plan row on its own turn. Claude emits history_boundary ahead of provider_turn and Codex emits it after, so both events can start the split. Rows that continue the plan's own turn (no new provider_turn) inherit its identity instead, which also closes the pre-existing case where a plan split off from streamed commentary started with no turn metadata.

Both rows now report their own turn and both expose Fork. Four regression tests in tests/provider-event-replay.test.ts cover the new-turn split (both emitter orderings), the same-turn inheritance, and the commentary-split plan row.

heath-s and others added 2 commits August 6, 2026 14:10
Splitting the post-plan response into its own message left that row without
`nativeProviderSessionId`/`nativeProviderTurnId`, so the only visible row of
the turn reported "this response predates native turn tracking" and lost its
fork/rollback actions. The sealed plan row meanwhile absorbed the follow-up
turn's id and history boundary, because `provider_turn` and `history_boundary`
arrive before the content they describe and landed on whatever assistant row
was last.

A plan row is sealed once presented, so metadata for a *different* native turn
now opens the follow-up message instead of overwriting the plan's. Claude emits
`history_boundary` ahead of `provider_turn` and Codex emits it after, so both
events can start the split. Rows that continue the plan's own turn inherit its
identity, which also covers the pre-existing gap where a plan split off from
streamed commentary started with no turn metadata at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@heath-s
heath-s merged commit 424e87a into main Aug 6, 2026
4 checks passed
@heath-s
heath-s deleted the fix/plan-no-show branch August 6, 2026 05:35
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.

2 participants