Skip to content

fix(dashboard): give pending and requeued steers their own treatment - #9037

Merged
bolichen97 merged 1 commit into
mainfrom
fix/steer-honest-state-treatments-8069
Sep 6, 2026
Merged

fix(dashboard): give pending and requeued steers their own treatment#9037
bolichen97 merged 1 commit into
mainfrom
fix/steer-honest-state-treatments-8069

Conversation

@NicholasRBowers

@NicholasRBowers NicholasRBowers commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

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:

  1. A written or optimistic steer row renders as a plain user message, indistinguishable from an ordinary Queue send — the operator cannot tell the steer registered.
  2. A requeued row 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.steerState written 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.tsx plus catalogs:

  • Derive pendingSteer (written, or the client's optimistic bubble with no state yet) and requeuedSteer (steerState === 'requeued') from the row meta. The existing isSteer derivation 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.
  • pendingSteer rows 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 the animatedSteers guard, so the one-shot entrance still fires when the same row is patched to consumed.
  • requeuedSteer rows 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).
  • Two new catalog keys in en.manual.json + all 11 translated catalogs, en-XA regenerated, and translator-context entries in en.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):

  • pending indicator renders for written and for optimistic-unstated rows; NOT for consumed, requeued, or legacy state-less rows
  • requeued line renders only for requeued
  • no treatment leaks onto ordinary user messages
  • pending indicator is muted (never text-accent / font-semibold) and plays no entrance ring
  • pending → consumed hand-off: indicator replaced by the badge AND the one-shot entrance still fires (pins the transition latch against regression)
  • pending → requeued hand-off: indicator replaced by the requeued line, nothing celebrates

Pre-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, full vitest suite (29,488 passed), electron suite (1,788 passed), full i18n:check chain 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):

steer lifecycle states

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

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@NicholasRBowers
NicholasRBowers requested a review from a team September 6, 2026 13:57
@NicholasRBowers
NicholasRBowers requested a review from a team as a code owner September 6, 2026 13:57
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of e0ec7c733e66a2089359fce77f07a893e0af1288 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] e0ec7c7

False positive or not applicable? A repository writer can comment:
/ai-review override gpt e0ec7c733e66a2089359fce77f07a893e0af1288: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of e0ec7c733e66a2089359fce77f07a893e0af1288 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All facts verified. Composing the review.

First-Principles-Verdict: CONCERNS

The slotRunning gate ships a visibility condition the description never states: it claims the pending indicator "renders for written" — the diff renders it only while a turn runs.

What this change ships

Intent: let the operator see that an unconfirmed steer registered, and that a failed one will run as its own message — a FIX.

  1. Unconfirmed steer rows show a muted pulsing "Steering…" line — justified (the reported gap).
  2. Requeued steer rows say the turn ended and the message runs on its own — justified (the reported gap).
  3. Pending indicator carries a click-to-open explainer tooltip — justified, reuses existing InfoTip (website/src/components/InfoTip.tsx).
  4. Requeued line keeps the Target icon of the badge family — declared, derived from the UX lane.
  5. Pending indicator appears only while the slot has a running turn (slotRunning prop threaded from ChatPage) — undeclared.
  6. Three catalog keys across 13 catalogs plus context notes — mandated by the i18n invariant.
  7. Two evidence PNGs under temp-screenshots/steer-state/ — sanctioned location (.github/PULL_REQUEST_TEMPLATE.md:47).

Watch

  • Item 5 is undeclared and contradicts the description's test claim ("pending indicator renders for written and for optimistic-unstated rows" — no running-turn condition anywhere in the description). The gate itself is justified (a best-effort settle can strand written forever), but a stranded-row-renders-plain behavior change and a new ChatPage → UserMessage prop are shipped silently; the human reviewer should see it stated.
  • Counted <UserMessage render sites outside tests: 2 (ChatPage.tsx:7922, app-sdk/messageRenderers.tsx:389). The app-sdk site passes meta but never slotRunning, so it can render the requeued line but never the pending one — fail-closed, yet an asymmetry in the same root cause ("a backend-distinguished state has no treatment") left at 1 of 2 sites for pending.

[FIRST-PRINCIPLES-REVIEWED] e0ec7c7

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Design-level review of e0ec7c733e66a2089359fce77f07a893e0af1288 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: CONCERNS

The slotRunning gate is slot-scoped, not turn-scoped, so a stranded written steer can falsely pulse "Steering…" during any later turn.

Watch

  • Stale pending indicator on later turns: the new prop comment admits "the backend settle is best-effort, so a row can be stranded in written forever," but the guard is pendingSteer = steerMeta && !!slotRunning && (steerState === 'written' …) — slot-level, with no tie to which turn is running. A steer stranded in written after its turn ends will resume pulsing "Steering…" the moment the operator starts any subsequent turn, asserting an in-flight redirect that is long dead — the same class of false claim (fix(dashboard): stop reporting a written steer as one the turn consumed #7997/Steer's honest states have no visible treatment after the badge fix #8069) this PR exists to eliminate. The fail-closed reasoning is right for idle slots but incomplete for running ones.

Suggestions

  • Scope the pending treatment to the steer that targets the current turn — e.g. only the newest user message, or compare the row's timestamp against the running turn's start — rather than any written row while the slot runs.

[DESIGN-REVIEWED] e0ec7c7

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed e0ec7c733e66a2089359fce77f07a893e0af1288 — this comment is updated in place on each push.

Review details

No findings.

Candidate 1 is a real edge case but stays advisory: the backend does strand steer rows in written forever on a hard-killed turn (confirmed at chat_delivery.py:170-181, "hard-killed rows stay written for the slot's life"), and slotRunning is a single slot-wide flag, so when a later turn runs, the stranded row's pendingSteer re-fires and pulses "Steering…" for a turn that already died — the exact false in-flight claim the gate was added to prevent, closed only for the idle case. But the consequence is a misleading indicator, not a crash/data-loss/security defect, and the minimal fix requires correlating the steer with the currently-running turn (a turn-id comparison) — new machinery outside the changed lines — so it is a FINDING, not a merge blocker.

Candidate 2 dies: the requeued line is status text about a redirect that did not take while the message still runs as its own turn — its value is a lifecycle state, not an error {error, code}/rejected-request/exception. The errors-use-error-notice rule explicitly excludes "status text about something that has not failed," so text-muted is correct and there is no violation.

FINDING — website/src/pages/chat/UserMessage.tsx:109 — pendingSteer = steerMeta && !!slotRunning && (steerState === 'written' …) gates on the slot-wide slotRunning, so a steer stranded in written by a hard-killed turn re-asserts a pulsing "Steering…" during any unrelated later turn → Fix: gate on the steer belonging to the currently-running turn (compare a turn/run id on the meta against the active turn) or clear stale written steer meta when a new turn starts.

[OPUS-REVIEWED] e0ec7c7

Verdict parsed from the review's SHA-scoped output markers for commit e0ec7c733e66a2089359fce77f07a893e0af1288.

False positive or not applicable? A repository writer can comment:
/ai-review override fable e0ec7c733e66a2089359fce77f07a893e0af1288: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — 🟡 CONCERNS

UX-level review of e0ec7c733e66a2089359fce77f07a893e0af1288 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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

  • Every product string was read correctly but rated "a guess" — "Steering…" ("something is in progress… a guess"), the requeued line ("arrived too late… treated as a fresh message… a guess"). Comprehension holds, confidence doesn't; every steer user hits these, so the copy is working at its floor, not comfortably.
  • Requeued valence is undecidable: the reader "can't tell whether the second one is a failure, a neutral outcome, or something I should act on" — only consumed goes green, so requeued reads as maybe-broken with no way to close the question. Moderate frequency (every failed steer), persists in history.
  • The Target icon "on its own" meant nothing to the reader; it carries the three states' family identity, so it only works while the text does.

Evidence gaps

  • Pending "Steering…" → consumed badge and pending → requeued line are in-place replacements of an element the user is watching (pendingSteer && <div…> vs the isSteer badge branch); screenshots can't show the hand-off — commit a recording of both transitions.

Suggestions

  • Give the requeued line the same InfoTip the pending indicator got, e.g. "The redirect didn't land in time — no action needed; it runs next." — closes the "should I act?" question the reader couldn't.

[UX-REVIEWED] e0ec7c7

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 6, 2026
@NicholasRBowers
NicholasRBowers force-pushed the fix/steer-honest-state-treatments-8069 branch from ec4104e to 9668669 Compare September 6, 2026 14:40
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: passed Eligible automated validation passed for the current revision labels Sep 6, 2026
@NicholasRBowers

Copy link
Copy Markdown
Contributor Author
  • Requeued line doesn't read as part of the steer-indicator family — disposition: fixed in 9668669.

The requeued line now carries the same Target icon as the pending indicator and the consumed badge, so all three lifecycle states read as one indicator; pinned by a new component test (keeps the Target icon on the requeued line). Adopted the review's smallest fix.

@NicholasRBowers

Copy link
Copy Markdown
Contributor Author
  • "Steering…" leans on unexplained steer jargon — disposition: fixed in 9668669.

The pending indicator now carries the review's suggested tooltip ("Redirecting the running turn — not yet confirmed.") as a title, added as a catalog key across all 13 locales with a translator-context entry, and pinned by a component test. The harness-caption suggestion was also adopted: the evidence screenshot's scaffolding captions are now prefixed [demo label — not product UI].

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 6, 2026
bolichen97
bolichen97 previously approved these changes Sep 6, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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):

  1. A row stranded in written_mark_steer_row_state is 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.
  2. The explanatory copy lives in a title on 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
@NicholasRBowers
NicholasRBowers force-pushed the fix/steer-honest-state-treatments-8069 branch from 9668669 to e0ec7c7 Compare September 6, 2026 15:31
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: passed Eligible automated validation passed for the current revision labels Sep 6, 2026
@bolichen97
bolichen97 merged commit 5bb7728 into main Sep 6, 2026
36 of 37 checks passed
@bolichen97
bolichen97 deleted the fix/steer-honest-state-treatments-8069 branch September 6, 2026 15:31
@github-actions github-actions Bot removed the readiness: checking Automated validation is still running label Sep 6, 2026
@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

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:

  • Stale perpetual "Steering…" on a stranded written row — the pending indicator is gated on a new slotRunning prop (fail-closed default): a written row on an idle slot, including one re-read from history, renders as a plain message. Pinned by two component tests.
  • Explainer unreachable on touch/keyboard — the title attribute was replaced with the focusable click-to-open InfoTip pattern (the needs_auth 'Not verified' hint is hover-only (inaccessible on keyboard/touch/AT) #3626 precedent), pinned by a test asserting the button and tooltip role.
  • Evidence gaptemp-screenshots/steer-state/steer-pending-explainer.png in the merged head shows the opened explainer.

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.

Steer's honest states have no visible treatment after the badge fix

2 participants