Skip to content

feat(dashboard): show what a session is waiting for, not just that it is (DRC-4015) - #124

Merged
kentwelcome merged 3 commits into
mainfrom
feat/b3-show-the-question
Aug 13, 2026
Merged

feat(dashboard): show what a session is waiting for, not just that it is (DRC-4015)#124
kentwelcome merged 3 commits into
mainfrom
feat/b3-show-the-question

Conversation

@kentwelcome

Copy link
Copy Markdown
Contributor

Summary

before  ▸ open question (AskUserQuestion), waiting 2m     ← and an overlay then blanked even this
after   ▸ Force push to main? (+1 more), waiting 2m

Real output from the parse-render-overlay path end to end, not a mockup.

Knowing a session is blocked is worth less than knowing what on. The first line is a reason to go and look, which is the trip the row exists to save.

Two changes, because the ticket's premise was half wrong

The board note called this "render what is already parsed". The audit on the issue found otherwise, and both halves needed doing.

The parse threw the text away. A pending input tool was kept as {name, ts}; the question lives in the tool_use block's input, dropped on the floor. Now summarised at parse time rather than kept raw, which is the deliberate part: ExitPlanMode carries a whole plan, sometimes thousands of words, and a plan has no business in a session row or in the caches a row is built from.

Tool Shown
AskUserQuestion the first question, plus (+N more)
ExitPlanMode the plan's first line, which is its own title in practice

Both bounded (input_summary_cap_chars, 160) and scrubbed through records.safe_text, because a transcript is a file Cargento does not write and this text lands in the DOM. There is a test for control characters specifically.

An agreeing overlay then blanked it anyway. No overlay constructor sets detail, so every needs-input patch carried None and apply_patch wrote that over whatever the collector had found. On a default install, with the bundled hook present, that erasure happened every time.

The fix is narrow on purpose. The collector's detail survives only when the row was already Needs input and stays Needs input, which is the case where the overlay agrees about the state and contradicts nothing.

What must keep clearing, and why

Working and Idle still blank the field, and that is not an oversight:

  • running Bash is true of a session that is running and false of one stopped at a gate, so a working detail must not follow the row into a wait;
  • a question that has been answered must not outlive the overlay that retired the wait, which is DRC-4095 and DRC-4097 exactly.

Three of the four new tests in WaitDetailTest pin those boundaries rather than the feature. They pass with the fix reverted, deliberately: they exist to stop the rule widening later, and the fourth is the one that fails without it.

What this does not do

The text still comes from the transcript alone, so it is there when the record reached disk and absent when it did not (N-4 measured that as neither guaranteed nor rate-known). The row says a question is open either way and can only sometimes say which.

SKILL.md says so rather than smoothing it over. A field that is usually present teaches a reader to trust it and then fails silently on exactly the sessions that matter.

Making it reliable means sourcing the text from the event path, and that is not a rendering change: the envelope drops the tool name and tool input at the hook, deliberately, and SECURITY.md states that as a property. Reopening it is a security decision and is step 3 on the issue, not this PR.

Test plan

  • Ran the pre-PR suite in AGENTS.md § Pre-PR Checks against a venv pinned to requirements-dev.txt, clean
  • 1154 tests pass (up 11), coverage 89.4%, over the fail_under threshold
  • Verified the fourth WaitDetailTest fails with the fix reverted; the other three are boundary pins
  • End-to-end check over a real transcript file: parse, render, overlay. Output above
  • Preserved Python 3.11 compatibility; CI's runtime-floor job covers the floor
  • Ran /sync-docs; N-7 added, SKILL.md updated in two places
  • No version field was touched
  • Commits are signed off (git commit -s, DCO)

Closes DRC-4015

… is (DRC-4015)

Knowing a session is blocked is worth less than knowing what on. "Force push to
main?" is a decision a person can make from the board. "open question
(AskUserQuestion)" is a reason to go and look, which is the trip the row exists
to save.

Two changes, because the board note's "render what is already parsed" was half
right and the wrong half is where the work was.

The parse threw the text away. A pending input tool was kept as {name, ts}, and
the question lives in the tool_use block's `input`, which was dropped. It is now
summarised at parse time rather than kept raw: `ExitPlanMode` carries a whole
plan, sometimes thousands of words, and a plan has no business in a session row
or in the caches a row is built from. A plan becomes its first line, which is its
own title in practice; a question becomes the first question with a count of the
rest. Both bounded, both scrubbed, because a transcript is a file Cargento does
not write and this text lands in the DOM.

An agreeing overlay then blanked it anyway. No overlay constructor sets `detail`,
so every needs-input patch carried None and applying it wrote that over whatever
the collector had found. On a default install, with the bundled hook present,
that happened every time. The collector's detail now survives, narrowly: only
when the row was already Needs input and stays Needs input, which is the case
where the overlay agrees about the state and contradicts nothing.

Working and Idle still clear the field. A working detail such as `running Bash`
is true of a running session and false of one stopped at a gate, so it must not
follow the row into a wait, and an answered question must not outlive the overlay
that retired it, which is DRC-4095 and DRC-4097.

The text still comes from the transcript alone, so it is there when the record
reached disk and absent when it did not (N-4). The row says a question is open
either way and can only sometimes say which. SKILL.md says so rather than
smoothing it over: a field that is usually present teaches a reader to trust it
and then fails silently on the sessions that matter.

Rationale in docs/design-needs-input.md (N-7).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kent Huang <kent@infuseai.io>
Copilot AI lite review requested due to automatic review settings August 12, 2026 10:18
@github-actions

github-actions Bot commented Aug 12, 2026

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                  152      1     32      0  99.5%
cargento/skills/cargento/cargento_runtime/claude_data.py                258     29    118     14  88.6%
cargento/skills/cargento/cargento_runtime/cli.py                        102     15     22      3  83.9%
cargento/skills/cargento/cargento_runtime/collectors/__init__.py          0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/collectors/antigravity.py     410     42    166     26  86.8%
cargento/skills/cargento/cargento_runtime/collectors/claude.py          210     17     70      7  90.7%
cargento/skills/cargento/cargento_runtime/collectors/codex.py            97      7     38      7  89.6%
cargento/skills/cargento/cargento_runtime/collectors/copilot.py         124      6     42      2  95.2%
cargento/skills/cargento/cargento_runtime/collectors/cursor.py          172     18     68     12  86.7%
cargento/skills/cargento/cargento_runtime/collectors/droid.py            31      3      6      1  89.2%
cargento/skills/cargento/cargento_runtime/collectors/gemini.py           52      7     16      4  83.8%
cargento/skills/cargento/cargento_runtime/collectors/goose.py            89     11     28      4  87.2%
cargento/skills/cargento/cargento_runtime/collectors/opencode.py         78      6     26      2  92.3%
cargento/skills/cargento/cargento_runtime/collectors/pi.py              280     30    128     18  88.2%
cargento/skills/cargento/cargento_runtime/config.py                     165      1     20      1  98.9%
cargento/skills/cargento/cargento_runtime/diagnostics.py                 84      4     26      4  92.7%
cargento/skills/cargento/cargento_runtime/events.py                     160      0     60      0 100.0%
cargento/skills/cargento/cargento_runtime/http_api.py                   291     17     94      5  94.3%
cargento/skills/cargento/cargento_runtime/io.py                         126      2     28      0  98.7%
cargento/skills/cargento/cargento_runtime/lifecycle.py                  314     17     98      7  94.2%
cargento/skills/cargento/cargento_runtime/notifications.py              128     12     50      2  91.0%
cargento/skills/cargento/cargento_runtime/observation.py                210      2     54      0  99.2%
cargento/skills/cargento/cargento_runtime/probe.py                       44      0     18      1  98.4%
cargento/skills/cargento/cargento_runtime/quota.py                      333      2    112      1  99.3%
cargento/skills/cargento/cargento_runtime/records.py                    125      4     64      5  95.2%
cargento/skills/cargento/cargento_runtime/sessions.py                    98      0     42      0 100.0%
cargento/skills/cargento/cargento_runtime/snapshot.py                    36      0      4      0 100.0%
cargento/skills/cargento/cargento_runtime/spacedock.py                  366     61    180     23  83.5%
cargento/skills/cargento/cargento_runtime/state.py                       57      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                232     15    114      7  93.1%
cargento/skills/cargento/cargento_runtime/turns.py                      143     17     82     14  85.3%
cargento/skills/cargento/cargento_runtime/web/__init__.py                 0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/web/page.py                    18      0      4      0 100.0%
cargento/skills/cargento/event_hook.py                                   86      4     28      3  93.9%
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                                                211      9     54      5  94.7%
scripts/bench_event_latency.py                                           67     67     14      0   0.0%
scripts/bump_version.py                                                  60     12     24      5  77.4%
scripts/capture_hook.py                                                 279     31     82     12  87.5%
scripts/lint_embedded.py                                                 85      7     28      3  89.4%
scripts/validate_plugins.py                                             592    182    336     53  66.3%
-------------------------------------------------------------------------------------------------------
TOTAL                                                                  6684    700   2468    268  88.0%

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

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.

Pull request overview

This PR improves the dashboard’s “Needs input” rows by showing what a session is waiting for (when available from the transcript), and by preventing an agreeing overlay from erasing that detail.

Changes:

  • Parse and persist a bounded, scrubbed asks summary for pending AskUserQuestion / ExitPlanMode tool uses (rather than discarding tool input).
  • Render the row using the summarized question/plan title when present, falling back to the tool name when not.
  • Preserve an existing state_detail when an overlay keeps a session in needs_input but carries detail=None, preventing accidental blanking.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
docs/design-needs-input.md Documents the two-part change (parse-time summarization + overlay non-blanking) and the intentional boundary behavior.
cargento/skills/cargento/tests/test_observation.py Adds regression tests ensuring agreeing needs-input overlays do not wipe collector-provided wait detail.
cargento/skills/cargento/tests/test_claude.py Adds tests for input_summary shape handling, caps, scrubbing, and end-to-end transcript carry-through.
cargento/skills/cargento/SKILL.md Updates user-facing documentation to reflect that the row may show the question/plan title when present on disk.
cargento/skills/cargento/cargento_runtime/config.py Introduces input_summary_cap_chars to bound summarized wait-detail text.
cargento/skills/cargento/cargento_runtime/collectors/claude.py Renders needs-input detail using asks when present, otherwise falls back to the prior tool-name-based phrasing.
cargento/skills/cargento/cargento_runtime/claude_data.py Implements input_summary and stores asks in pending_input_tool during transcript analysis.
cargento/skills/cargento/cargento_runtime/aggregate.py Applies _keep_wait_detail to prevent overlays from blanking an existing wait detail when state remains needs_input.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

kentwelcome and others added 2 commits August 12, 2026 18:38
…what is unmeasured

Two review findings, both about the writing outrunning the evidence rather than
about the code.

N-7 documented one reason the question can be missing, the record not having
reached disk, and missed the ordinary one. For up to `overlay_working_ttl_sec`
after a turn starts, a live working overlay overrules the collector outright: the
row reads Working, the detail is cleared, and the question is invisible even
though the parse has it. `_keep_wait_detail` cannot help, because it only holds
when the patch agrees the row is waiting. N-7's own argument about a field that
is usually there applies to that window word for word, so it now names it, and
points at N-6's recorder as the thing that counts it. SKILL.md gains the same
caveat.

"On a default install the erasure happened every time" was asserted. It needs an
`input_requested` overlay, which only `PermissionRequest` mints, and `captures/`
holds no Claude `PermissionRequest` record: the only capture of that name is
Codex's, and it is a negative result. Whether the hook fires for
`AskUserQuestion` and `ExitPlanMode` is an adapter claim this repository requires
a capture for, so the paragraph now says the erasure is reachable and its
frequency unknown, and names what would settle it.

Four smaller things from the same pass:

`safe_text` stripped C0 and DEL and passed the zero-width, bidi-embedding and
isolate ranges. Those reorder how the text after them renders, so a record could
make a row read as something it does not say. Shared function, so this covers
every untrusted string on a row, not only the new one.

A plan now summarises to its first *usable* line. A plan opening with a blank
line, a fence or a bullet gave "```, waiting 2m", which is a worse row than the
tool's name it replaced.

The seam between the parse and the row had no test: `InputSummaryTest` stopped at
`analyze_transcript` and `WaitDetailTest` started at hand-built rows. Two
collector-level tests now pin both readings, the question present and absent.

SECURITY.md's rationale for keeping `state_detail` out of dispute records said it
can carry a permission prompt's text. That is now also an open question's and a
plan's first line, which makes the exclusion better justified rather than
different.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kent Huang <kent@infuseai.io>
The scrub I widened a commit ago took U+200B to U+200F as a range, which sweeps
in ZWNJ and ZWJ. ZWNJ is orthographic in Persian and several Indic scripts and
sits inside words; ZWJ is what composes an emoji sequence. The substitution
replaces with a space, so a Persian title would have been broken mid-word.

This is a shared function, so the blast radius was every untrusted string on a
row: titles, prompts, model names, not only the question this branch adds. My
own check passed because CJK does not use joiners.

Neither joiner can reorder text, so keeping them costs no protection. The
spoofing vector is the directional marks and the embedding, override and isolate
ranges, and those stay covered. The range is now listed character by character
across that span with the reason written beside it, since a range is what caused
this.

Tests both ways: what must be stripped, and what must survive. Escapes rather
than literals in the fixture, for the same reason ruff bans literals in source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kent Huang <kent@infuseai.io>
@kentwelcome
kentwelcome merged commit daf7421 into main Aug 13, 2026
11 checks passed
@kentwelcome
kentwelcome deleted the feat/b3-show-the-question branch August 13, 2026 00:56
kentwelcome added a commit that referenced this pull request Aug 18, 2026
* feat(dashboard): batch the gates into one queue (DRC-4018)

Attention has a fixed cost per switch, so three separate pings cost more than
one list of three. The blocked sessions were already a list on screen. What
they were not was a queue: the band rendered them in session id order, which is
an arbitrary order to be stopped in, and gave you nothing to act with — you read
a session id off the screen by eye and went hunting for its terminal.

Four changes, and none of them is new data.

Order. Needs-input rows are published longest-blocked first. The gate that has
held you up longest is the one still costing something and the one you are
least likely to have remembered; the one that just opened is the one you already
know about. It ranks on `blocked_since`, a fixed timestamp rather than an
elapsed time, so the queue does not reshuffle under you while every row in it
waits longer. Calm's `attention` ordering makes the same call for the same rows
— its needs group was newest-first, which is right for a row you are watching
and wrong for one waiting on you. `recent` is untouched and still genuinely
newest-first for every state.

Position. Rows are numbered from the top and the band head says how many are
waiting, so the list reads as a queue and you can watch it shrink.

A handle. Each row carries `copy id`, the control calm's rows already had,
routed through the same action channel so there is one copy implementation
rather than two that report success differently.

A pass. `j`/`k` step the queue, Enter takes the row you are on, and `g` reaches
the queue from either mode. Nothing marks a gate handled: Cargento does not
write to a session, and a mark it did not measure would hide a gate that is
still open. You answer in the terminal, the server stops calling the session
blocked, the row leaves on the next refresh and the cursor inherits the new
head. The queue clears itself out of measurements.

One thing that is not interface. DRC-4015 found the per-row text reaches disk
only sometimes. A row without it used to render an empty div — the same blank a
row with nothing to add would leave — so the one row you cannot triage was also
the one row that looked ordinary. It now says so, in both views.

Scope note: D7 (DRC-4030) stays separate. This orders the gate queue; ranking
working cards and the idle list against each other is a different question.

Signed-off-by: Kent Huang <kent@infuseai.io>

* fix(dashboard): review findings on the gate queue (DRC-4018)

Two of these were bugs, and both were bugs of the same kind: a binding that
looked right in calm mode and was wrong in the view it was copied into.

The regular view took Space and the arrow keys. Calm can bind those because its
ledger scrolls inside its own frame under its own cursor; the regular view is an
ordinary long page, so preventDefault there removed paging and line-scrolling —
and removed them only while something was blocked, since the branch returns
early on an empty queue. Scroll keys that work or not depending on the payload
are worse than no bindings. `j`/`k` and Enter only, now.

`g` into calm narrowed to the blocked rows without setting the ordering. From
`recent` that renders the queue exactly backwards with the cursor parked on the
last row; from `fastest` it files the gates under "not working now". SKILL.md
asserted both modes show the same queue in the same order, which was true only
under `attention`. It sets the ordering too, so the doc is now true as written.

Also from the review:

The band's `copy id` dropped keyboard focus on the render it triggered. Calm
solves this with calmFocusKey/calmRestoreFocus; the regular path called neither,
so activating the button from the keyboard sent focus to <body> and the next Tab
restarted at the top of the document.

Calm's `attention` ranked the queue on `waitSec`, the rendered elapsed time,
which floors at zero. Two rows carrying implausibly future stamps would tie
there while the server still separated them — the one way the two views could
name a different gate at the head. It ranks on the raw timestamp now, as
aggregate.py does.

`gateCursorKey` outlived an emptied queue, so a session that blocked, was
answered, and blocked again later inherited a cursor that belonged on whichever
gate had waited longest.

test_the_order_does_not_move_as_the_wait_lengthens could not fail: sorted() over
one list with one key is deterministic by construction, and flipping the key's
sign left it green. Replaced with an assertion on the key itself.

Two gaps closed: no test clicked the band's copy button, so the data-calm reuse
— the riskiest part — was only ever exercised through the keyboard, which does
not go through the global listener; and nothing covered the focus hand-off. Both
new tests were mutation-checked against the un-fixed code.

Docs: the calm-mode controls paragraph had ended up inside the gate-queue
section though it owns sorting, filtering and the `u` key, so it moves back
under Display modes. The queue's key list said `⏎` copies, which is regular
mode only — in calm it expands. The "nothing marks a gate handled" argument was
being restated in five places; it moves to docs/design-needs-input.md as N-8,
which already owns the needs-input reasoning, and the comments become pointers.

Comment trimming throughout, on the same standard: the paragraph in main.js
restated the three lines under it, and row_order carried an 18-line docstring
over a 4-line function while leaving the one branch a reader would stop at — the
last_activity fallback — unexplained.

Signed-off-by: Kent Huang <kent@infuseai.io>

* docs(sync): reconcile the docs with the gate queue (DRC-4018)

The module ownership table in the architecture doc did not say where the queue
lives. It is split three ways on purpose: the queue itself is shared page state
because both views read it, the band's cursor belongs to the regular view, and
the listeners that route both views' controls have always been in calm.js and
are the page's rather than calm's. A reader checking the inward-only rule
against that table would have read the last one as drift.

The N-8 entry added in the previous commit carried two em dashes, which the
voice standard bans in docs/design-*.md. Nothing in CI checks that.

The sync marker was stamped at 8f859f8 and four PRs have merged since: #121
through #124. Their docs are current, so this is the orphaned-marker failure the
skill warns about rather than a real backlog, and the new marker is the merge
base so the next squash cannot orphan it again.

Signed-off-by: Kent Huang <kent@infuseai.io>

---------

Signed-off-by: Kent Huang <kent@infuseai.io>
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