Skip to content

feat(web): keep inert controls on the page, and give the board one control primitive - #362

Merged
gcko merged 5 commits into
mainfrom
spacedock-ensign/drc-4588
Sep 17, 2026
Merged

gcko merged 5 commits into
mainfrom
spacedock-ensign/drc-4588

Conversation

@gcko

@gcko gcko commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

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

  • Render the reading control in all four reason states, with the reason after the button.
  • Use aria-disabled so the control keeps its place in the tab order and can be described.
  • Gate both handlers on the same expression their control renders, so a refused press reaches no endpoint.
  • Collapse seven control recipes onto one .next-action primitive.
  • Make the disabled state dashed rather than dimmer, so it survives greyscale.

Evidence

  • Dashboard suite 3540/3540 passed, scripts suite 515/515 passed, coverage 86.8%.
  • Byte pins regenerated from the assets after rebase; test_next_page, test_next_flag and test_focus each re-run alone and green.

Review guidance

aria-disabled restores the click that disabled was 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 existing reading-ask"[^>]*disabled assertion 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

…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>
@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Name                                                                  Stmts   Miss Branch BrPart  Cover
-------------------------------------------------------------------------------------------------------
cargento/skills/cargento/agy_hook.py                                     79     14     28      7  78.5%
cargento/skills/cargento/cargento_runtime/__init__.py                     0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/aggregate.py                  323      3    110      3  98.6%
cargento/skills/cargento/cargento_runtime/annotations.py                373     19    130     21  92.0%
cargento/skills/cargento/cargento_runtime/asks.py                       110      0     28      0 100.0%
cargento/skills/cargento/cargento_runtime/claude_data.py                340     40    158     18  88.4%
cargento/skills/cargento/cargento_runtime/cli.py                        212     20     52      6  89.4%
cargento/skills/cargento/cargento_runtime/collectors/__init__.py          0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/collectors/antigravity.py     423     41    174     25  87.3%
cargento/skills/cargento/cargento_runtime/collectors/claude.py          305     17    112     15  91.8%
cargento/skills/cargento/cargento_runtime/collectors/codex.py           258     18     96     15  90.7%
cargento/skills/cargento/cargento_runtime/collectors/copilot.py         153      4     52      2  97.1%
cargento/skills/cargento/cargento_runtime/collectors/cursor.py          287     19    108     16  90.6%
cargento/skills/cargento/cargento_runtime/collectors/droid.py            34      3      6      1  90.0%
cargento/skills/cargento/cargento_runtime/collectors/gemini.py           54      7     16      4  84.3%
cargento/skills/cargento/cargento_runtime/collectors/goose.py            95      6     28      4  91.9%
cargento/skills/cargento/cargento_runtime/collectors/opencode.py        124      4     44      4  95.2%
cargento/skills/cargento/cargento_runtime/collectors/pi.py              328     34    152     20  88.8%
cargento/skills/cargento/cargento_runtime/config.py                     232      1     20      1  99.2%
cargento/skills/cargento/cargento_runtime/deliveries.py                 128     12     30      7  88.0%
cargento/skills/cargento/cargento_runtime/departures.py                 131      6     30      6  92.5%
cargento/skills/cargento/cargento_runtime/diagnostics.py                 88      3     26      3  94.7%
cargento/skills/cargento/cargento_runtime/dismissals.py                 111      3     32      3  95.8%
cargento/skills/cargento/cargento_runtime/ends.py                        89      5     18      0  95.3%
cargento/skills/cargento/cargento_runtime/events.py                     230      1     86      3  98.7%
cargento/skills/cargento/cargento_runtime/focus.py                       72      2     20      2  95.7%
cargento/skills/cargento/cargento_runtime/git_status.py                  59      1     22      2  96.3%
cargento/skills/cargento/cargento_runtime/history.py                    206      9     56      7  93.9%
cargento/skills/cargento/cargento_runtime/http_api.py                   933    107    300     37  88.3%
cargento/skills/cargento/cargento_runtime/interaction_prototype.py      709    221    198     38  65.7%
cargento/skills/cargento/cargento_runtime/io.py                         163      2     38      1  98.5%
cargento/skills/cargento/cargento_runtime/irreversible.py                39      0      6      0 100.0%
cargento/skills/cargento/cargento_runtime/lifecycle.py                  412     47    140     14  87.5%
cargento/skills/cargento/cargento_runtime/notifications.py              264     12     92      7  94.7%
cargento/skills/cargento/cargento_runtime/observation.py                412      6    122      3  98.3%
cargento/skills/cargento/cargento_runtime/observer.py                   388     24    156     15  92.8%
cargento/skills/cargento/cargento_runtime/probe.py                       44      0     18      1  98.4%
cargento/skills/cargento/cargento_runtime/project_context.py           1588    179    686    121  85.3%
cargento/skills/cargento/cargento_runtime/quota.py                      392      7    144      6  97.2%
cargento/skills/cargento/cargento_runtime/reach.py                       78      6     30      4  90.7%
cargento/skills/cargento/cargento_runtime/reading.py                    365      7    122      6  97.3%
cargento/skills/cargento/cargento_runtime/records.py                    265      5    118      9  96.3%
cargento/skills/cargento/cargento_runtime/semantic_history.py           290     14    110     18  92.0%
cargento/skills/cargento/cargento_runtime/sessions.py                   189      4     72      4  96.9%
cargento/skills/cargento/cargento_runtime/snapshot.py                    36      0      4      0 100.0%
cargento/skills/cargento/cargento_runtime/spacedock.py                  510     50    260     26  89.9%
cargento/skills/cargento/cargento_runtime/state.py                       83      0      2      0 100.0%
cargento/skills/cargento/cargento_runtime/stream.py                      57      0      8      0 100.0%
cargento/skills/cargento/cargento_runtime/transcripts.py                529     29    272     25  93.3%
cargento/skills/cargento/cargento_runtime/tripwires.py                  232     14     94     13  91.7%
cargento/skills/cargento/cargento_runtime/turns.py                      209     11    104     12  92.7%
cargento/skills/cargento/cargento_runtime/unasked.py                    119      2     34      1  98.0%
cargento/skills/cargento/cargento_runtime/web/__init__.py                 0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/web/page.py                    38      0     10      0 100.0%
cargento/skills/cargento/event_hook.py                                  213     54     92     14  68.5%
cargento/skills/cargento/mcp_server.py                                  377     22    112     14  92.2%
cargento/skills/cargento/notify_hook.py                                  49     15      6      1  67.3%
cargento/skills/cargento/server.py                                        3      0      2      1  80.0%
cargento/skills/cargento/statusline_hook.py                             131     13     46      8  87.0%
scripts/bench_collect.py                                                214     13     54      6  92.9%
scripts/bench_event_latency.py                                           67     21     14      1  67.9%
scripts/bump_version.py                                                  60     12     24      5  77.4%
scripts/capture_focus_raise.py                                          729    131    236     16  79.4%
scripts/capture_hook.py                                                 292     30     90     11  88.7%
scripts/capture_team_registry.py                                        172     33     48      6  80.5%
scripts/capture_terminal_identity.py                                    347    108    104     14  66.3%
scripts/derive_prompt_shapes.py                                         210     16     88     14  89.3%
scripts/lint_embedded.py                                                 97      3     32      3  95.3%
scripts/mark_abstention.py                                              382    142    140     23  58.4%
scripts/score_abstention.py                                             493     52    182     18  88.4%
scripts/serve_operator_cockpit.py                                       448    191    112     20  54.5%
scripts/validate_plugins.py                                             762    202    434     67  70.8%
-------------------------------------------------------------------------------------------------------
TOTAL                                                                 18234   2097   6590    798  86.6%

Threshold: fail_under in pyproject.toml · label coverage-exception to bypass (visible in PR timeline).

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>
@gcko
gcko requested a lite review from Copilot September 17, 2026 13:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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-disabled and 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.

Comment thread cargento/skills/cargento/cargento_runtime/web/next-cockpit.js Outdated
Comment thread cargento/skills/cargento/cargento_runtime/web/next-cockpit.js Outdated
Comment thread cargento/skills/cargento/tests/test_next_cockpit.py
Comment thread cargento/skills/cargento/tests/test_next_page.py
gcko and others added 4 commits September 17, 2026 22:06
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
gcko merged commit 21a0e93 into main Sep 17, 2026
12 checks passed
@gcko
gcko deleted the spacedock-ensign/drc-4588 branch September 17, 2026 16:46
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>
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.

2 participants