feat(dashboard): let the session-card PR/issue chips be switched off - #7005
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of
Design-Verdict: PASS Real harm (dead row + credentialed polling nobody wants), gated at both the payload and the refresh feed, reversible default-true key — sound and proportionate. Suggestions
[DESIGN-REVIEWED] 93c6d13 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of I've gathered what I need: consumer counts, sibling counts, and the existing mechanisms this change rides on. Writing the review. First-Principles-Verdict: PASS Every item removes a named cost — a reclaimed row, PR numbers off a shared screen, and credentialed polling nobody renders — and none of it duplicates an existing mechanism. What this change shipsIntent: let someone stop their session cards advertising every pull request and issue their transcripts happened to mention. ADDITION.
Watch
[FIRST-PRINCIPLES-REVIEWED] 93c6d13 |
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS Real toggle, honest copy: the label names the exact surface, "chips" matches the sibling "File Change Chips" vocabulary, the description states the off-switch's side effect, the config write pushes fresh slots so the chips vanish instantly, and the on/off screenshots prove both states. [UX-REVIEWED] 93c6d13 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
c414e0c to
52f5aa5
Compare
Disposition: GPT 5.6 BLOCKING on
|
52f5aa5 to
1a097f9
Compare
Disposition round 2 -- head
|
80f3fad to
e170670
Compare
Disposition round 4 -- head
|
e170670 to
ac40b1c
Compare
ac40b1c to
6a18547
Compare
Disposition round 6 -- head
|
6a18547 to
2f37a41
Compare
Disposition round 7 -- head
|
2f37a41 to
5dcfbad
Compare
Disposition round 8 -- head
|
Disposition round 9 -- head
|
bolichen97
left a comment
There was a problem hiding this comment.
Full-diff maintainer review passed: change matches its stated scope, no regressions or trust-boundary weakening found, checks green and no outstanding change requests. Approving.
The chip strip on every sidebar session card shipped unconditionally: two fields in _ChatSlot.to_dict and the periodic credentialed status refresh fed by DashboardState.source_link_urls, with nothing in between consulting config. A session that merely quoted someone else's pull request advertised it like a workstream, the strip cost a row per card on the densest surface in the app, and the refresh kept spawning provider subprocesses for chips a user may not want at all. Add dashboard.session_card_source_links, default true so existing behaviour is byte-identical, and gate both halves: * Payload. serialize_slots resolves the switch ONCE per push and threads it to to_dict, which skips extraction entirely rather than emptying two fields after paying for the transcript scan. serialize_slot resolves it itself when called alone. The read is not inside to_dict because that runs synchronously on the event loop for every slot on every push and _raw_config re-reads config.json on each call. * Refresh. source_link_urls and source_link_urls_for_slot return nothing while the switch is off, so the periodic and turn-boundary refreshes stop reaching the provider instead of polling for chips nobody renders. The overflow-expand endpoint answers an empty payload for the same reason. Settings -> Chat gets the toggle beside Link Previews and Auto-Open Git, with the string in all 12 catalogs and the regenerated settings registry. Closes #6574
5dcfbad to
93c6d13
Compare
Rebased onto main at maintainer request -- head
|
|
Rebased this onto current main as part of a maintainer conflict sweep and stopped short of pushing, because one half is an author decision rather than a conflict resolution.
def to_dict(self, *, include_check_status: bool = False) -> dict:
# Skip extraction itself when the chips are off ...
from kiro_crew.dashboard.handlers.source_providers import (
session_card_source_links_enabled,
)
source_links = self._pr_source_links() if session_card_source_links_enabled() else []
return self._projection.to_dict(
self,
...
)Your other two gates (
Your PR converts the same mutation to patch-only (
Taking main's side compiles and typechecks, but then 12 of this PR's own ChatPanel tests fail, because the test files merged without conflict and therefore carry your assertions. Taking your side would require reworking the overlay's Worth separating out: the behaviour your patch-only change fixes is real and main's overlay does NOT subsume it. The overlay's own comment describes protecting against a second toggle in this tab during an in-flight save; a whole-object PUT built from this tab's shown config still writes back every other setting at its cached value, so a change a second dashboard tab (or Nothing was pushed to your branch, so it is exactly as you left it. |
1. What is the problem?
Every session card in the chat sidebar renders a chip strip for each pull
request, merge request and issue URL mentioned anywhere in that session's
transcript, and there is no setting, no per-folder option and no config key that
turns it off.
_ChatSlot.to_dict()always serializessource_linksandsource_links_total, andSessionSourceChipsrenders whenever that payload isnon-empty. Nothing between the two consults config.
The chips also drive a periodic credentialed refresh: the owner websocket loop
calls
DashboardState.source_link_urls()every TTL and schedules a check-statusround that reaches the provider CLI.
2. Why this issue matters to the user
A mention is not a workstream. The extractor accepts any
/pull/,/merge_requests/,/issues/or/browse/URL it finds, so a session thatmerely read or quoted someone else's pull request gets a chip that reads as
ownership.
The sidebar is the densest surface in the app and the strip is a whole extra row
per card, while
link_previews,session_grid,auto_open_git_paneland therest of the chat display preferences are all already switchable. Those numbers
are also on screen for the entire session, so pull-request and issue numbers
from unrelated work land in every screen share, screenshot and bug report.
And it is not free: a user who does not want the chips is still paying for the
background provider calls that keep their status fresh.
3. How our fix solves it
New
dashboard.session_card_source_links, default true, so an install thatnever mentions the key behaves exactly as it does today. Both halves of the
feature are gated, because gating only the payload would leave the provider
polling for chips nobody renders:
Payload.
to_dictreads the switch where it uses it and skips_pr_source_links()entirely rather than paying for the transcript scan and thenemptying two fields. No flag is threaded through the serializers: the read is an
in-memory snapshot lookup, so there is no per-slot cost to amortize, and
publication happens on the loop, so a synchronous slot loop cannot observe a flip
mid-push and emit a mixed payload. (An earlier revision did thread a flag from
serialize_slots; the First Principles lane correctly pointed out that itsjustification -- a per-slot config read -- stopped existing once the getter became
cache-only.)
Refresh.
source_link_urls()andsource_link_urls_for_slot()returnnothing while the switch is off, which stops both the periodic owner-WS round and
the turn-boundary refresh before they reach the provider.
The
"+N"overflow endpoint is deliberately NOT gated: its only caller is the pill,which exists only while the strip renders, and the write pushes fresh slots so the
pill goes at once. An app token that owns the slot could ask directly, but it can
already read that slot's messages -- these URLs are extracted FROM those messages, so
a gate there would withhold nothing it does not already have. (An earlier revision did
gate it; the First Principles lane counted the consumers.)
Where the value is read from. Not from config on the loop. The switch rides
the snapshot
ensure_gitlab_hosts_loaded()already refreshes in a worker threadfor the self-managed GitLab and Jira allowlists: same config read, same TTL, same
lock, same generation counter. That is the mechanism this file's own
test_gitlab_allowlist_never_reads_config_on_the_event_loopexists to protect,and the reason is identical -- the value is consumed by synchronous slot
serialization that runs on the sole event loop for every slot on every push,
while
KiroCrewConfig.load()stats, reads, parses and validates config files.Two things worth stating plainly:
PUT /api/dashboard/configwrites the value,then installs it in the snapshot and pushes the slots, so the chips appear or
disappear on the next frame. That publish takes the same lock
ensure_gitlab_hosts_loadedholds across its threaded load, which is what keepsthe write and the poll ordered: a poll already in flight holds a pre-write reading,
and the lock makes the write land last. The 30s TTL refresh remains the backstop that picks
up an edit made outside the dashboard (
kirocrew config set, a hand edit).Raised by the UX lane, which was right that leaving it to the poll made an
instant-looking switch sit inert for up to half a minute.
Truewhere the allowlists start empty. The asymmetry isdeliberate and commented: an unknown host must fail CLOSED, but the chip strip
predates this switch, so a cold snapshot has to fail OPEN or every install would
render no chips until the first refresh landed. An unreadable config is treated
the same way.
One shared fix came with it.
ChatPanel's dashboard mutation rebuilt the WHOLEconfig from its own query cache on every toggle (
mutate({ ...dashCfg, ...patch }))and PUT all of it. Because the handler applies whichever keys are present, that
wrote every other setting back at its cached value -- so with two dashboards open,
flipping any switch restored the panel to the state that tab last read. It now
sends only the changed keys, which is what
BrowserPanel's own dashboard mutationalready does, with the optimistic cache write merged instead of replaced. Found by
the GPT lane on this PR's new toggle; the window predates it and covered all
thirteen switches on the page.
No renderer change was needed: the call site in
ChatSidebar.tsxalready guardson
s.source_links.length > 0, so an empty strip draws no row.The toggle lands in Settings -> Chat beside Link Previews and Auto-Open Git, as
the issue proposed, with the string in all 12 catalogs, the regenerated
settingsRegistry.gen.tsentry (so settings search finds it) and the regeneratedconfig-baseline.json.What this does not fix
Point 1 of the issue -- a chip for a pull request the session merely QUOTED reads as
ownership -- is only answered here for someone who turns the strip off. The cause is
_ChatSlot._pr_source_links, which accepts every/pull/,/merge_requests/,/issues/and/browse/URL in a transcript with no ownership signal at all, andthis PR does not touch it. Default-true therefore means every existing install keeps
that mis-attribution. So: mechanism-level for the extra row and the credentialed
polling, symptom-level for the mis-attribution. Narrowing the extractor is a
behaviour change to what a chip MEANS and wants its own decision, not a rider here.
Raised by the First Principles lane.
Out of scope, as filed: the in-session Resources / Changes side panel. Opening a
pull request from inside the session it belongs to is a different act from
advertising it on the card.
Screenshots
The evidence is a comparison, because the change is a subtraction: a lone shot of
a sidebar without chips is indistinguishable from a session that never mentioned
a pull request.
The strip as it ships -- a session that only quoted someone else's PR carries its
number, and a five-link session shows three chips plus
+2:The new row in Settings -> Chat, on the untouched default:
The same row after clicking it off, read back from the server:
And the same three cards with the switch off -- no strip, no
+2, the rowreclaimed:
Captured by the committed harness
website/scripts/capture-session-card-source-links.mjs,which runs the real built SPA behind the shared loopback static server with every
/api/**call answered from fixtures (no gateway, no dashboard token, no providerCLI). The sequence is driven by the real control: clicking the toggle issues the
same
PUT /api/dashboard/configthe dashboard sends, and the fixture then appliesthe same rule the server applies, so the fourth frame photographs the payload
shape production pushes rather than a hand-blanked one. The harness asserts the
chip is present in frame 1, that the PUT carried
false, that the switch readsaria-checked=false, and that no chip renders in frame 4 -- it fails rather thansaving a misleading frame. The backend half is pinned by tests, not by a still
image.
4. What tests we did
New
test/test_session_card_source_links_knob.py, 22 tests, every assertionmutation-verified red against the un-gated code:
getter never reaches
KiroCrewConfig.load()or the off-loop loader (bothpatched to fail the test if called) -- the companion to
test_gitlab_allowlist_never_reads_config_on_the_event_loop; a flip bumps theshared generation so open tabs get pushed, and republishing the same value does
not; an unreadable config loads as on while the hosts in the same tuple stay
closed.
the keys; off does not call
_pr_source_links()at all (patched to raise).serialize_slots: off strips every slot, the switch is resolved exactly oncefor a five-slot push (counting fake), and a lone
serialize_slotresolves ititself.
refresh_slot_source_statusnever reachesrequest_check_refresh_now.and switched-off are different answers).
generated JSON schema carries label + help, and all three endpoint wiring points
-- PUT allowlist, validation branch (
invalid_session_card_source_links), GETecho.
Mutation verification: reverting the
to_dictgate reds 4 tests; removing eitherURL-feed gate reds the refresh tests; making
serialize_slotsdefer the read toeach slot reds the read-once test; removing the endpoint gate reds the expand
test.
Eight assertions in
SettingsChatPanelCoverage.test.tsxpinned the oldfull-object PUT body (one was even named "keeping siblings"). They now assert the
changed key alone: the REQUIREMENT -- do not lose sibling settings -- is unchanged
and is met by the handler, which writes only the keys in the body; the assertions
had encoded the mechanism. 68/68 still pass, and the pre-existing
capture-verbosity-levels.mjsharness -- whose whole point is that the valuere-read after the write is the one just chosen -- still passes end to end.
The row-framing geometry my screenshot harness needed already existed in that
verbosity harness, and
jscpdruns at a 0% duplication threshold, so the secondcopy failed Frontend Lint. Extracted to
scripts/lib/settings-row-shot.mjsandboth harnesses now call it;
npx jscpd .is back to one clone, the pre-existingcapture-grid-divider-align/capture-sidebar-dragbarpair, which measures 0%and passes.
Also green:
test_source_providers.py(the two harness fakes updated for thewidened loader return),
test_dashboard_source_links_expand.pyandtest_dashboard_state_ws.pyunchanged -- which is thedefault-behaviour-identical check -- plus
test_auto_open_git_panel_config.pyand
test_link_previews_config.py.tsc --noEmitclean;vitest runoncatalogParity/duplicateKeys/deadKeys(107) andSettingsChatPanelCoverage(68);check-i18n-keys,check-dnt-catalogsandcheck-i18n-stringsclean; isort, flake8 and the black gate clean. mypy on thetouched modules reports only 2 pre-existing errors in
transcribe.py, which thisdiff does not touch.
Two
test_source_providers.pytests (test_local_token_uses_configured_owner_subject,test_local_token_carries_embed_parent_port_claim) fail in a whole-file run andpass in isolation. A/B against the same file with this diff stashed reproduces
both, so they are a pre-existing ordering artefact, not this change.
Per the repo's CPU rule I ran targeted suites only and left the full run to CI.
5. Any other suggestions on the work
_raw_config()'s docstring claims "cached per process" and the body re-readsthe file on every call. This PR no longer depends on it, but several dashboard
tunables still read through it -- including
effective_max_background_turns(),which is reached from the turn-dispatch path on the loop. Worth its own PR:
either a real fingerprint-keyed cache or a corrected docstring, because the
current one invites exactly the mistake this review caught.
chosen. If a settings toggle should feel immediate, the honest fix is for
/api/dashboard/configto expire that snapshot after a successful write, whichwould make the whole family (allowlists included) refresh promptly. Left out
here to keep one owner for the refresh.
the natural follow-on once more than one source can exist, per the discussion on
the issue. Deliberately not in this PR.
label,neutral icon fallthrough, widened payload type) and the two source/check
registries -- stay separate; feat: pluggable source-provider seam for the Changes panel and sidebar chips #6925 is already carrying the seam work.
Closes #6574