fix(dashboard): give pending and requeued steers their own treatment - #9037
Conversation
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All facts verified. Composing the review. First-Principles-Verdict: CONCERNS The What this change shipsIntent: let the operator see that an unconfirmed steer registered, and that a failed one will run as its own message — a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] e0ec7c7 |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS The Watch
Suggestions
[DESIGN-REVIEWED] e0ec7c7 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsNo findings. Candidate 1 is a real edge case but stays advisory: the backend does strand steer rows in Candidate 2 dies: the FINDING — website/src/pages/chat/UserMessage.tsx:109 — [OPUS-REVIEWED] e0ec7c7 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of All evidence reviewed. The blind reader correctly parsed all three steer states (pending, requeued, consumed) and the tooltip; the main residual risks are the unrecorded in-place state hand-offs and the requeued line's ambiguous valence. Final review: UX-Verdict: CONCERNS Blind read parsed all three steer states correctly, but every reading was "a guess," and the pending→consumed/requeued hand-offs ship with no recording. Watch
Evidence gaps
Suggestions
[UX-REVIEWED] e0ec7c7 |
ec4104e to
9668669
Compare
|
|
bolichen97
left a comment
There was a problem hiding this comment.
Tech Lead review — approved.
Correctness. Render-only: the diff is UserMessage.tsx + its test + the 13 mandated catalogs + one temp-screenshots/ evidence image (documented convention, auto-pruned by cleanup-temp-screenshots.yml). No delivery, settle, or requeue code is touched, so this cannot introduce a silent drop or a double delivery. Read UserMessage.tsx:85-102 at head: pendingSteer (written | optimistic-unstated) and requeuedSteer (requeued) are mutually exclusive with isSteer by construction — every state that makes either true is already excluded from the isSteer derivation — so #7997's invariant (the accent badge asserts backend confirmation) is preserved, and the legacy state-less row keeps its original rendering. The indicator sits in the badge's slot inside the same flex flex-col items-end parent, so the pending → consumed / requeued hand-off is a content swap, not a layout jump, and it never touches animatedSteers, so the one-shot entrance still fires on promotion.
Relationship to #8826. No overlap and no regression: #8826 (merged 06:08Z, backend chat_delivery.py/chat_runner.py/steer_settle.py) made the state fail closed to written unless positive consumption evidence exists; this PR is the frontend complement that renders the two states #8826 made honest. Zero shared files. Together they close the loop — the backend stops claiming a consumption it cannot prove, and the frontend stops rendering nothing when it cannot. Making requeued visible also removes the operator's reason to re-send by hand, which is the path that would have produced an actual double delivery.
Tests. The state matrix covers all five inputs (written, optimistic-unstated, consumed, requeued, legacy) in both directions, pins the muted styling negatively (not text-accent, not font-semibold), pins no entrance ring on a pending row, and pins both hand-offs including that the entrance still fires on written → consumed — the exact latch a future refactor would break.
Non-blocking follow-ups (both raised by the advisory UX lane; PASS/CONCERNS do not gate readiness):
- A row stranded in
written—_mark_steer_row_stateis best-effort by design (chat_runner.py:4592) and returns early on an ambiguous same-content match — pulses "Steering…" indefinitely, including when re-read from history. Weaker than the class of defect #7997 fixed (it claims pending, never success, and the tooltip says "not yet confirmed"), and strictly more informative than today's silence, but worth a follow-up to drop the pulse when the slot has no running turn. - The explanatory copy lives in a
titleon a non-interactive div: no touch, no keyboard. The visible label carries the essential meaning, so this is friction rather than failure.
Neither is a merge blocker. i18n spot-checked: ja/ko render the progressive form of the same verb the existing consumed badge uses, which is what en.context.json instructs.
…8069) PR #7997 gated the 'Steered into the running turn' badge to backend-confirmed (consumed) steers, which removed the false claim but left the two honest intermediate states with no treatment at all: a written or optimistic steer rendered as a plain user message (the operator could not tell the steer registered), and a requeued steer stayed silent even though the system knows the redirect failed and the message ran as its own turn. Derive the two states from the row meta the backend already writes: pendingSteer (written, or the client's optimistic bubble with no state yet) renders a muted pulsing 'Steering…' indicator where the accent badge would sit; requeuedSteer renders one muted line saying the turn ended before the steer applied. Both are mutually exclusive with the untouched isSteer derivation, so the #7997 invariant — the accent badge asserts backend confirmation — is preserved, and the pending row never pre-claims the animatedSteers guard, so the one-shot entrance still fires on the written→consumed patch. Two new catalog keys (en.manual.json + 11 locales + regenerated en-XA) with translator context entries; state-matrix component tests pin the treatments to their exact states, the pending→consumed/requeued hand-offs, and the untouched consumed badge + entrance latch. Closes #8069
9668669 to
e0ec7c7
Compare
|
For the record — the PR merged (e0ec7c7) while the round-2 UX review round was still in flight; both of its Watch items and the evidence gap were already fixed in the merged head:
|
Problem / Motivation
PR #7997 fixed the dashboard falsely claiming a mid-turn steer reached the running turn: it gated the "Steered into the running turn" badge to backend-confirmed (
consumed) steers. That removed the false claim but left the two honest states with no treatment of their own:writtenor optimistic steer row renders as a plain user message, indistinguishable from an ordinary Queue send — the operator cannot tell the steer registered.requeuedrow stays silent even though the system knows the redirect failed and the message runs as its own turn afterwards — exactly the Queue semantics the user declined.Both gaps were raised and accepted by the UX Review lane on #7997. Item 3 from the issue (the entrance-animation latch) was already fixed on main (
5767e0d9c); this PR deliberately does not touch it.Why it matters
A steer is the operator's "redirect this NOW" gesture. While the backend has not confirmed it, the transcript currently shows nothing — so the operator cannot distinguish "the steer registered and is pending" from "I accidentally queued an ordinary message". And when the redirect fails (requeued), the transcript never says so, leaving the operator to believe an injection happened that did not.
What changed (motivation → approach → change)
Symptom → cause: the tri-state
meta.steerStatewritten by the backend (#7997/#7998) already distinguishes the states; the frontend just renders nothing for two of them.Change, scoped to
website/src/pages/chat/UserMessage.tsxplus catalogs:pendingSteer(written, or the client's optimistic bubble with no state yet) andrequeuedSteer(steerState === 'requeued') from the row meta. The existingisSteerderivation is untouched, and the two new states are mutually exclusive with it by construction — the fix(dashboard): stop reporting a written steer as one the turn consumed #7997 invariant (the accent badge asserts backend confirmation) is preserved.pendingSteerrows render a muted, gently pulsing "Steering…" indicator where the accent badge would sit: muted color, lower font weight, no entrance animation — visually distinct from both the consumed badge and a plain send. It never touches theanimatedSteersguard, so the one-shot entrance still fires when the same row is patched toconsumed.requeuedSteerrows render one muted line: "Turn ended before this applied — runs as its own message." (state-neutral present tense: at render time the follow-up message may not have executed yet, and the same line is read later in history).en.manual.json+ all 11 translated catalogs,en-XAregenerated, and translator-context entries inen.context.json(the "Steering…" note disambiguates the progressive verb from the "steering files" noun used elsewhere).Post-open, the UX Review lane raised two advisory concerns, both adopted: the requeued line keeps the same Target icon as the pending/consumed treatments so the three lifecycle states read as one indicator family, and the pending indicator carries an explanatory tooltip (“Redirecting the running turn — not yet confirmed.”, a 13th-catalog key) so “steer” is not unexplained jargon to a first-time user.
The pre-push GPT review lane flagged the original past-tense copy ("ran as its own message") as falsely reporting execution before delivery; the copy and key were reworked to the present-tense form in this commit.
Tests
New state-matrix tests in
website/src/test/UserMessage.test.tsx(all fail against unmodified main):writtenand for optimistic-unstated rows; NOT forconsumed,requeued, or legacy state-less rowsrequeuedtext-accent/font-semibold) and plays no entrance ringPre-existing consumed-badge and entrance-latch tests pass unchanged.
Manual verification
Rendered all three states in a live Vite harness and captured them (below). Local gates:
npx tsc -b, fullvitestsuite (29,488 passed), electron suite (1,788 passed), fulli18n:checkchain including the diff-scoped gates against base, isort/flake8/mypy clean.Screenshots / video
All three lifecycle states (pending / requeued / consumed — the consumed badge unchanged from main):
Related Issues
Closes #8069
Pattern harvest
Not generalizable: UI copy gap specific to the steer lifecycle introduced by #7997's badge gating; no mechanical rule catches "a state the backend distinguishes has no visual treatment".
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)