Files
OpenResearch/ui
Daniel Kim 012672958d Plan-mode chat: AskUserQuestion double-card/hang fix + desktop-parity plan/question cards (v0.1.61) (#84)
* Fix harness prompt bugs: AskUserQuestion double-card + hang, resolved-card rendering

- plan_auto_policy auto-allows AskUserQuestion: the bridge no longer fronts
  the question card with a redundant permission card whose held request also
  deadlocked the end-turn resume (answer rejected as busy until interrupt).
- respond() resolves a card only after its answer is delivered, so a rejected
  send leaves the card actionable instead of resolved-with-the-answer-dropped.
- Resolved cards mirror Claude Code: permission cards vanish, plan/question
  cards collapse to compact inline expandable rows with an answer echo
  (answers/approved/note stamped on resolve, carried through turn flushes).

* Review round 1 fixes: doc split, echo-preserving adopt, neutral outcome, read-only class rename

* Review round 2 fixes: PendingGuard doc, best-effort resolve folded into helper, neutral question echo

* Post-review nits: doc wording, dedupe question text in collapsed body, word-break on collapsed rows

* Plan-mode AskUserQuestion truly blocks: bridge holds the call as the question card

Auto-allowing AskUserQuestion was wrong: headless the tool returns no
answer, so the model guessed and kept planning instead of waiting. The
bridge now surfaces the QUESTION itself as a held mid-turn card (like
ExitPlanMode's plan card); the answer settles the held call as a denial
whose message is the answer, so the model continues the same turn with
the user's actual choice. The turn-status line says 'Waiting for your
input…' instead of 'Working…' while a card holds the turn open.

* Post-review nits: deny-message hardening, question cards don't suppress plan-synth, empty-options fallback, held-card status via nativeId, comment accuracy

* Polish: no tool row for bridge-held calls (deny-with-answer painted them red), plan pane gets padding + plain background

The bridged AskUserQuestion/ExitPlanMode tool_use now renders nothing —
the held card is the representation, and the denial that carries the
user's answer back made the CLI report the tool as errored, painting a
successful Q&A as a red error row. The right-pane plan tab had no CSS
at all: flush text on the panel tint; it's a markdown document, so it
gets a padded, plain reading surface.

* PlanStrip: Claude-desktop shape — title line, real buttons in a row below

The strip's buttons used .btn-primary/.btn-ghost, which are only styled
inside .prompt-actions — in the strip they rendered as bare text crammed
next to the title. The actions row now reuses .prompt-actions (same
buttons as every card) and sits on its own line under the title.

* PlanStrip buttons: monochrome — black text/borders, no brand red

* PlanStrip split button: single internal divider (caret drops its own left border)

* PlanStrip split button: flat inner corners (radius reset must out-rank .prompt-actions shorthand)

* Composer: attention bell instead of Stop while a held card awaits the user

A blocked-on-input turn is busy, so the composer showed the Stop X — a
loading/cancel affordance while the agent is actually waiting on an
answer. When the session is busy AND a held card (nativeId) is
unresolved, show a pulsing bell that scrolls to the pending card; Esc
still interrupts. The transcript status line shares the same
awaitingInput signal.

* Composer: two states only — Send by default, Stop X only while actively streaming

Drop the bell experiment. A turn held on an unanswered card shows the
idle Send button; the card and the status line are the waiting
affordance. Esc still interrupts a held turn.

* Composer answers the pending card (Claude-desktop): typed text = plan revision feedback / custom question answer

- send() routes plain typed text to the newest unresolved plan/question
  card (respond with note) instead of racing the held turn with a new
  message the busy guard would drop; placeholder says so. Questions
  route only on Claude sessions (opencode rejects note-only replies).
- Rail busy-dot stops pulsing when the turn is held waiting on the user
  (steady dot; same awaitingInput signal as the status line).
- Note-only question answers echo the note in the collapsed row.

* PlanStrip: Claude-desktop parity actions + popovers above the strip

Reject / Revise… / Accept / Accept-and-auto-mode (+ bypass in the caret),
Open plan as a title-row link. Reject is a true rejection: the note-less
plan denial now tells the model to stop and wait (was 'keep refining',
which made it improvise revisions/questions); collapsed rows echo
'Rejected' vs 'Revision requested' by note presence. Accept resumes
under accept-edits (guarded), Accept-and-auto under Auto — the primary
is the solid dark button, not brand red. The strip moved inside
.composer so the mode/model picker popovers (z 50 in that stacking
context) layer above it — as a sibling, .composer's z-index:4 capped
them below the strip.

* PlanStrip: single Accept-and-auto-mode primary, drop the plan excerpt

Matches the desktop reference exactly: [Reject] [Revise…] on the left,
one primary button on the right whose caret menu holds the two guarded
tiers (Accept = accept-edits, Accept and bypass all). The separate
outline Accept button and the plan-title excerpt next to the header
are gone — plan.excerpt/plan prop was fully unused after removal.

* PlanStrip: self-contained Revise mode (Claude-desktop parity)

Swaps the strip into its own textarea + [Back] [Revise ↵] footer
instead of routing through the main composer — matches the desktop
reference exactly. Enter submits, Shift+Enter newlines, Esc cancels
back to the action row. The main-composer typed-text routing
(pendingPrompt) is now question-only; plans have their own dedicated
UI and no longer need it.

* PlanStrip: drop the plain accept-edits Accept option

We have no partial-approval story (edits-only, everything else still
gated) — the caret now holds only Accept and bypass all.

* Plan revision: visible feedback + never silently dead-end

- Submitting a revision now swaps the strip to a 'Revising the plan…'
  placeholder until the revised card arrives (or the turn ends) —
  previously the strip snapped back to an identical-looking action row
  and the submit read as a no-op.
- An empty revise note maps to a generic 'Please revise the plan.'
  instead of riding the note-less wire shape, which the backend reads
  as a hard rejection ('stop and wait') — the '(optional)' field was
  accidentally a Reject.
- respond() reconciles messages AND busy state from the server after
  every answer: a stale card (e.g. resolved by the bridge timeout while
  the tab's SSE was dropped) previously stayed actionable forever and
  swallowed every answer.

* PlanStrip: drop the 'Revising the plan…' placeholder

The transcript's Working… spinner already communicates it. The strip
still hides while a submitted revision is in flight (the outgoing card
must not look actionable) and the revised card swaps in on arrival.

* Review round 1 fixes: zombie-card recovery, honest composer button, reject-loop fix

Blockers:
- A held card orphaned by a process restart (PendingGuard never ran)
  permanently captured typed input: the composer routed every message to
  it and errors were swallowed. Now the backend collapses the zombie on
  the first answer attempt, and the composer only routes typed text to a
  question card that is actually answerable (live turn for native cards).
- The Send/Stop swap keyed on awaitingInput while send() could only
  service question cards — a held plan/permission card showed a lying
  Send (silent no-op) with no Stop control. Now keyed on pendingQuestion.

Also: note-less Reject on an end-turn/synthesized plan card resolves
with no resume (resuming just to say 'stop' re-synthesized a card — an
un-dismissable strip); the bridge plan-deny arm reuses synthesize_resume
verbatim; respond() reconciles busy for its own session only and reports
failure so the question route can restore a consumed draft; revising
clear gated to its session; un-docked plan card fallback matches the
strip's semantics; Rejected gets its own outcome class; note-only
answers echo once; split-button CSS consolidated; dead CSS/union
removed; test pins the reject wording.

* Post-clean-round nits: keep optimistic busy on failed reconcile fetch, sweep zombie held cards at claude turn entry

- respond()'s busy reconcile no longer clears busy when the sessions
  fetch fails — during a Handled resume nothing re-asserts busy
  mid-stream, so a transient fetch failure would hide Working…/Stop for
  the rest of the turn.
- claude run_turn sweeps unresolved native_id cards at turn entry
  (resolve_stale_prompts gains a native_only mode; codex keeps sweeping
  everything). A held card can never outlive its process, so one left
  unresolved is a crash artifact — previously it could capture the
  composer's typed-text routing once the new turn made the session busy.

* Bump version to 0.1.61
2026-07-17 00:15:45 -07:00
..
2026-07-06 20:05:41 -07:00
2026-07-08 18:23:16 -07:00
2026-07-06 20:05:41 -07:00
2026-07-06 20:05:41 -07:00