fix(codex): show a Plan-mode plan once, as a card, live and reopened
A codex Plan-mode session reopened from history lost its plan and grew a user
turn nobody typed. Reopening re-parses the rollout, and a Plan turn publishes
its answer as `event_msg.item_completed { item.type = "Plan" }` INSTEAD of
`agent_message` — an event the parser had no arm for — while the same plan's
`<proposed_plan>` copy in the model history was denied outright. The turn
rendered as nothing but its reasoning, with even the copy button gone because
the message body was an empty string. The approval that follows is written as
an ordinary `user_message` reading "Implement the approved plan.", identical
field for field to something typed, so one plan interaction read as two.
The plan now renders from either copy the rollout carries. They are not
interchangeable: the announcement holds the body alone, the assistant record
holds that body plus whatever prose codex wrote around the block, so the record
takes the announcement's message over rather than adding a second — rendering
only the announcement would silently drop the follow-up paragraph codex likes
to end on. The pairing slot is filled only by an announcement and claimed only
by the next plan record, so two turns proposing an identical body stay two
plans instead of collapsing into one.
The synthetic approval prompt is suppressed behind two signals: the turn's
`collaboration_mode` flipping out of `plan`, and codex's wording compared
verbatim. Requiring both leaves the bubble to a person who happens to type that
sentence, and comparing without trimming keeps the filter off text codex never
writes. Only a transition moves the arm, so a `turn_context` re-emitted
mid-turn — which newer codex does — cannot disarm it before the prompt arrives.
The decision is not discarded, it moves: it settles a `plan_review` call
carrying codex-acp's own approval wording, the same shape the live permission
gate seeds. The summary parser mirrors both rules so the sidebar count tracks
what the opened conversation renders.
Live, codex publishes the plan on two channels at once — as ordinary assistant
prose and as `rawInput.plan` on the review request — and both land in one turn,
so the whole plan appeared twice, once bare and once boxed. The bare copy is
dropped when a review call in the same turn carries exactly that text; a
message that merely quotes or extends the plan keeps its own.
Both carriers of a plan now render the same card. The reloaded one had its own
chrome and so lost the prose styling and the clamp/expand footer the live one
had; they share one component and one title key now, so the two cannot drift
again. A review gate shows its decision marker UNDER the plan rather than in
place of it — the card is the plan, the marker is what the user did about it,
and an approved plan used to display neither.
Verified against the local rollout corpus: of 2779 sessions, the only ones
whose parse changes are the plan sessions. X
xintaofei committed
a6325ce7044605c4df436b60c3f8ef47baf345fb
Parent: 30e4570