feat(web): keep inert controls on the page, and give the board one control primitive - #362
Merged
Merged
Conversation
…ntrol primitive Two issues on one branch, because exactly one PR may touch cargento_runtime/web/ at a time and both of these do. One commit rather than two: the change to next-cockpit.js and styles.css interleaves, and the byte pins are shared, so a split would leave a first commit whose oracles are red. DRC-4588 -- always render the Held to reading control. The reading block took an early return whenever a reason withheld the offer, which deleted the button, the offer paragraph, the sending disclosure and the request counter together, in the one state a newcomer lands in. The control now renders in all four reason states and prints the reason after the button, so the sentence moves rather than going. The dangerous half is the attribute. `aria-disabled` keeps the control in the tab order and lets its reason be announced, but it also restores the click that `disabled` was suppressing, and neither handler behind these controls gated on anything that could refuse it: a press would have reached POST /api/reading and spent the reader's own model capacity from a state the page calls unavailable. Both handlers now refuse on the same expression their control renders, from one function, so the two cannot disagree. The save control is inert rather than `hidden` for the same reason, and `clear` keeps `hidden` because an empty box has nothing to clear and no explanation to offer. This supersedes the DRC-4565-era ruling that the offer stays withheld on the discarded and never-typed rows. Two assertions invert, and the comment above them is replaced rather than deleted: the reason the offer was withheld was that the sentence had to be load-bearing, and what changed is that the sentence is now bound to the control through `aria-describedby`. DRC-4590 -- one control primitive, and a disabled state that survives greyscale. The stylesheet had no way to say "this one": no shared control class and no radius token, against 24 resting control rules over six corner treatments. `.next-action` is that primitive, opted into by writing the class rather than by appending to a selector group, which is the drift that produced the 24 recipes. Seven rules collapse onto it, `.next-action--primary` marks the one tab that has an action to mark, and disabled is dashed rather than dimmer because `--ink3` is the resting colour of the prose these controls sit in. The dead `.next-tabs` class is removed from two shared selector groups rather than by line range. Both criteria are accepted on enumerated verifiers, not universal readings: the remaining five control recipes and the four tabs with no action to mark are filed as DRC-4604 and DRC-4603. The durable rationale for both, including the rejected selector-group alternative, is NUI-18 in docs/design-next-ui.md. Implements DRC-4588 Implements DRC-4590 Signed-off-by: Jared Scott <jared.scott@variable.team> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
CoverageThreshold: |
gcko
added a commit
that referenced
this pull request
Sep 17, 2026
NO-GO. The refusal sentence is printed twice and then goes false under an enabled button; the gates themselves are correct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Jared Scott <jared.scott@variable.team>
gcko
added a commit
that referenced
this pull request
Sep 17, 2026
Nothing in this issue's share blocks; four items filed rather than promoted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Jared Scott <jared.scott@variable.team>
There was a problem hiding this comment.
🟡 Changes recommended
The refusal response can become stale and contradictory, while two new tests do not reliably exercise their intended coverage paths.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR updates the dashboard UI so unavailable actions remain visible and explainable, while consolidating common controls into .next-action.
Changes:
- Keeps reading and save controls rendered with
aria-disabledand refusal handling. - Adds shared control styling with dashed disabled states.
- Updates documentation, behavioral tests, and byte pins.
File summaries
| File | Description |
|---|---|
docs/design-reader-state.md |
Documents refusal-state persistence. |
docs/design-next-ui.md |
Records control and accessibility decisions. |
cargento/skills/cargento/SKILL.md |
Updates user-facing Held to behavior. |
cargento/skills/cargento/cargento_runtime/web/styles.css |
Adds the shared control primitive. |
cargento/skills/cargento/cargento_runtime/web/next-cockpit.js |
Implements inert controls and reading gates. |
cargento/skills/cargento/cargento_runtime/web/next-controls.js |
Applies the primitive to project controls. |
cargento/skills/cargento/cargento_runtime/web/next-notify.js |
Applies shared styling to notifications. |
cargento/skills/cargento/cargento_runtime/web/next-chrome.js |
Applies shared styling to retry controls. |
cargento/skills/cargento/cargento_runtime/web/next-capacity.js |
Applies shared styling to quota controls. |
cargento/skills/cargento/cargento_runtime/web/next-boot.js |
Applies shared styling to session controls. |
cargento/skills/cargento/tests/test_next_cockpit.py |
Adds reading and inert-control behavior tests. |
cargento/skills/cargento/tests/test_next_page.py |
Adds primitive and CSS contract tests. |
cargento/skills/cargento/tests/test_next_controls.py |
Relaxes brittle markup matching. |
cargento/skills/cargento/tests/test_next_chrome.py |
Updates focus and primitive assertions. |
cargento/skills/cargento/tests/test_next_flag.py |
Updates assembled-page byte pins. |
cargento/skills/cargento/tests/test_focus.py |
Updates assembled-page digest pin. |
Review details
- Files reviewed: 16/16 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Correction round 1. The gate stored the refusal in `nextCockpitReadingRequests` and nothing ever deleted from that map, which this change made reachable: the early return used to delete the control, so there was no press to store. Two consequences, both reproduced before the fix. The stored message rendered as its own paragraph while the reason paragraph rendered the same sentence again, so a press took the refusal from one occurrence to two, adjacent and identical. And after the reader did what the sentence asked, the button rendered enabled with "Nothing has been typed for this session" still beneath it, clearable only by a reload or by paying for a reading. That second one is the board asserting an absence after it stopped being true, beside a control contradicting it, which is the shape this milestone exists to remove. A refusal is a state rather than an event, so it is now marked as one and dropped as soon as the reason it names stops holding, and it renders through a single node: while a refusal stands it IS the reason, so the announcement and the description are the same paragraph, carrying `role="status"` and the id the button points at. A response stays an event and is kept. The regression test asserts the lane and not only the render. Measured: with the single-node render in place, removing the clear left every rendered assertion green, because a stale refusal can no longer reach a render path. That would have shipped the leak back the first time someone added one. Both halves are mutation-checked: removing the clear fails on the lingering entry, restoring the duplicate render fails on the occurrence count. Byte pins regenerated from the assets. Docs: the reader-state row for this lane now records that a refusal is the one entry that does not survive. Signed-off-by: Jared Scott <jared.scott@variable.team> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ding as refused The four small findings from correction round 1, fixed in the PR in flight rather than filed. Three were false statements sitting in the code and the fourth was a register saying the wrong thing about a state. The comment on `nextCockpitHeldControl` said the inert `save` control "says why it cannot fire". It does not: the gate returns silently. The silence is defensible, because the box beside the control already shows that the draft matches what is stored, so the comment is corrected to the behaviour rather than behaviour invented to match the comment. The comment on the nothing-typed arm claimed its first sentence was verbatim what `/api/reading` refuses with. It is not. `reading.REFUSALS` says "Nothing is typed against this session" where this page says "has been typed for". The discard arm above it does carry the server's string, which is what made the claim look plausible. The comment now says which of the two is true of which. `.next-stalled button:disabled` inherited the primitive's dashed border. NUI-18 defines dashed as the register for a control refusing a press, and that control is waiting for a retry it makes itself. Its `cursor:wait` already drew the distinction and the border was contradicting it, so the override now states `border-style:solid` and the test that owns the criterion asserts it. The comment above `.next-session-raise` said `.next-session-copy` never got a `:focus-visible` ring. It has one now, through `.next-action`, which closes the DRC-4381 gap that comment was describing. The raise keeps its own brighter ring, which is the distinction the comment exists for. Byte pins regenerated from the assets. Signed-off-by: Jared Scott <jared.scott@variable.team> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two of Copilot's findings. The third, T3, is refuted below. T1. `aria-disabled` keeps the save control in the tab order where `hidden` removed it from the page, so a screen-reader user now reaches it and heard only that it was dimmed. The field's absence sentence, which says why, was not associated with it. The paragraph gains a per-field id and the control points at it, matching what the reading control already does. The pointer is conditional on the sentence rather than on the state: a field holding saved words renders no absence paragraph, and an `aria-describedby` resolving to nothing is worse than none. `nextCockpitHeldToggle` drops it on a keystroke alongside `aria-disabled`, because a live control described by "No goal typed for this session" is the same defect one state further on. T4. `if __name__ == "__main__"` sat 140 lines before the end of test_next_page.py, so a class defined after it would not load on the direct-run path. Moved to the end. That path fails on imports today for an unrelated reason, so nothing was actually being skipped, and the 31/31 under `unittest discover` was honest -- this removes a trap rather than fixing a miscount. T3 is refuted, with evidence rather than reading. The claim was that `\\s` in a JS regex inside a Python string reaches the engine as literal backslash-s, leaving the bare-`disabled` guard unable to fail. The enclosing string is a plain triple-quote, not a raw one, so Python collapses `\\s` to `\s` and the engine receives real whitespace. Run directly in node against both spellings: true for ` disabled`, false for ` aria-disabled`. Mutation-checked end to end by reverting the emitter to the bare attribute -- `bare` reads true for all four refused states and the assertion fires. The guard works. Signed-off-by: Jared Scott <jared.scott@variable.team> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gcko
added a commit
that referenced
this pull request
Sep 17, 2026
The dispatched SHA d9059ae was stale: PR #362 merged after it, moving the sentence-tier census from 54 blocks to 55 with no test noticing. Withdraw two carried figures that do not reproduce, and record the shorthand trap that made a first derivation understate the set threefold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gcko
added a commit
that referenced
this pull request
Sep 17, 2026
…raft the rewrites Two of fifteen value/absence pairs invert, both in the recovery briefing's WAITING ON YOU cell: the caps-label rule .next-cockpit-recovery span captures the value span at 11.5px while the absence outranks it at 12.5px. Derived at d9059ae as dispatched and re-derived at 21a0e93 after PR #362 merged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gcko
added a commit
that referenced
this pull request
Sep 17, 2026
…e syntax The PR Workflow bullet said to always use explicit Closes #NNNN lines. Work here is tracked in Linear as DRC-NNNN, which is not a GitHub issue number, so Closes #4589 points at nothing today and will point at an unrelated issue once this repository's own numbering reaches it -- it currently has #221 and #25 and nothing else. The bullet predates the Linear move. Found by the integrator for PR #364, which correctly used the Implements [DRC-NNNN](url) form that #361 and #362 both use, and flagged that doing so contradicted this file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Jared Scott <jared.scott@variable.team>
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.
Implements DRC-4588 — Always render the Held to reading control, disabled with its reason, instead of deleting the tab's only verb
Implements DRC-4590 — Add a control primitive with one primary action per tab and a disabled state that survives greyscale
A reader who has typed nothing can now see that a reading is something this tab does, instead of meeting a tab with its only verb deleted.
What changed
aria-disabledso the control keeps its place in the tab order and can be described..next-actionprimitive.Evidence
test_next_page,test_next_flagandtest_focuseach re-run alone and green.Review guidance
aria-disabledrestores the click thatdisabledwas suppressing, and neither handler previously checked the reason it rendered. The handler gates ship with the change for that reason — without them this would present a control a reader can press from a state the page calls unavailable, spending their own model capacity. The existingreading-ask"[^>]*disabledassertion matches both spellings and is not evidence either way; the new assertions pin the full attribute and the absence of the bare form.Two criteria were accepted on enumerated verifiers with their remainders filed as DRC-4603 and DRC-4604 rather than stretched to fit.
drc-4588