feat(portal): render the reject answer and keep the action bar usable - #84
Merged
Conversation
Flow's attended vocabulary gains `reject` -- an explicit "Stop, this is wrong" that TERMINATES the run, as distinct from `escalate`, which parks it for a colleague. The shell decides nothing; it renders what the signed task allows, so this adds the wire name, its closed disposition, its copy, and its terminal receipt pair. The two answers are worded so they can never read as synonyms. Escalate says someone will pick this up; reject says nobody will, because there is no longer a run to pick up. An operator told the wrong one of those acts on it. Reject is NOT the new emphasised answer. R1 removed all visual recommendation because `allowed_actions[0]` is set by a predicate about what the engine can re-verify, not about what the operator should do; making the disagreement answer prominent instead would be the same mistake pointed the other way, and nothing on the task means "recommended". Layout. Four two-line buttons is now the ordinary bar and five is reachable. Measured in headless Chromium at 360x640, the tightest device that still ships: three buttons 207px (32% of the viewport), four 270px (42%), five 333px (52%) -- at five the operator could see 307px of the decision at a time. A count-keyed compaction rule brings four to 248px and five to 286px (45%). Every button shrinks by the same amount, because a per-action size would be a prominence difference decided by list position. 44px is the floor and is not crossed: a mis-tap here is a wrong answer to a governed decision, so the bar sheds padding rather than touch target. A browser without `:has()` keeps today's taller buttons, which is safe -- the page reserves the bar's MEASURED height, so nothing is occluded. The `openadapt-flow==` pin is unchanged and still 1.25.0. The shell filters buttons from the signed task, so against a runner that does not offer `reject` nothing appears; the pin bumps when the Flow release carrying it lands.
The decision-portal doc enumerated three answers and described them by whether the saved workflow changes. Reject differs on a second axis -- what becomes of the RUN -- and the doc now states both, plus the reason reject and escalate are separate actions rather than two labels on one, the measured action-bar heights, and that nothing on the bar is emphasised.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third of four repos. Renders Flow's new
rejectaction — an explicit "Stop, this is wrong" that terminates the run — on the runner-local phone portal.Depends on
openadapt-types0.6.3 (merged, published) andopenadapt-flow#295.What the shell adds
The shell decides nothing; it renders what the signed task allows and filters buttons through
ACTION_WIRE. So this is the wire name, its closed disposition, its copy, and its terminal receipt pair.escalaterejectWorded so they can never read as synonyms: escalate says someone will pick this up; reject says nobody will, because there is no longer a run to pick up. An operator told the wrong one of those acts on it. Pinned by test in both directions, including that the rejection receipt copy does not contain "picks it up".
Reject's label is the assertion, and it is about this run — not the saved workflow, which is
teach's job and carries a requalification gate this button does not. The consequence text says so explicitly.Reject is not the new emphasised answer
R1 removed all visual recommendation, because
allowed_actions[0]is set by an engine predicate about what can be re-verified rather than about what the operator should do. Making the disagreement answer prominent instead would be the same mistake pointed the other way. Nothing on the task means "recommended", so nothing is emphasised — asserted behaviourally (all four buttons, identicalclassName) and structurally (no.primaryrule, no--accentinside any.actionsrule).The action bar, measured rather than assumed
app.jsalready carried a comment recording that a constant height guess broke at three buttons. Reject makes four the ordinary case and five reachable. Measured in headless Chromium at three viewports:At five buttons on the tightest device the operator could see 307px of the decision at a time. Nothing was occluded — the page reserves the bar's measured height and no label wrapped — but half the screen is a legibility problem in its own right.
A count-keyed compaction rule brings it to 248px (39%) at four and 286px (45%) at five:
[data-action="…"]selector may appear in a sizing rule.min(min-height) >= 44.:has(button:nth-child(n)). A browser without:has()keeps today's taller buttons — which degrades to the current layout, safe because the reserve is measured, rather than to a broken one.The alternative — letting the bar scroll — was rejected: it would put whichever answers fall below its fold out of sight, which is the same prominence-by-position defect.
Tests
src/portalShell.test.tsdrives the real script in a DOM: the four-button bar, reject vs escalate as distinct consequences, the terminal rejection outcome (and that it never says someone will pick it up), the legacy{status: "rejected"}reply, and that the posted body carriesdisposition: "rejected_by_operator"and exactly six keys — no field on the wire could carry a reason an operator typed.tests/test_portal/test_shell.pypins the contract surface: the wire vocabulary is exactly Flow's five, every receipt state has copy, the compaction rule is count-keyed and above the touch-target floor, and nothing is emphasised.Verified locally: 128 pytest passed, 65 vitest passed.
Deferred, deliberately
openadapt-flow==1.25.0is unchanged. The shell filters buttons from the signed task, so against a runner that does not offerrejectnothing appears and nothing breaks. The pin bumps in a follow-up once the Flow release carryingrejectis published — bumping it now would pin to a version that does not exist.🤖 Generated with Claude Code
https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM