Skip to content

fix(sessions): make the capped project-notes list reachable by keyboard - #4831

Merged
michellemxm merged 1 commit into
mainfrom
fix/summary-notes-keyboard-a11y
Aug 21, 2026
Merged

fix(sessions): make the capped project-notes list reachable by keyboard#4831
michellemxm merged 1 commit into
mainfrom
fix/summary-notes-keyboard-a11y

Conversation

@michellemxm

@michellemxm michellemxm commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

#4779 bounded the session-summary panel's "How this project works" footer at 33vh with
internal scrolling. That turned it into a scroll region whose only children are plain
<li> text
— nothing focusable, and no tabIndex on the container itself.

So on a session carrying more notes than fit, everything below the fold is reachable by
pointer only. A keyboard-only user can open the section and then cannot move through it.

This is a regression that PR introduced rather than a pre-existing gap: before the cap the
footer had no ceiling, so it grew to fit every note and nothing was ever out of reach.
Adding the ceiling is what created the trap.

The open-items card above it is unaffected, and for a reason worth stating because it is
why the fix lands on only one of the two capped regions: its rows are <button>s, so they
are already in the tab order and the browser scrolls each into view as focus moves.

Why it matters

WCAG 2.4.7 aside, the failure is total for the affected cohort rather than cosmetic: the
content is not merely awkward to reach, it is unreachable. It also fails silently — the
section opens, the first few notes render, and nothing signals that the rest exists but
cannot be got to without a mouse.

What changed (motivation → approach → change)

Give the scroll region a tab stop, a name, and a visible focus indicator — on the
SCROLLER, not on the list.
The scroller is a wrapping div; the <ul> inside it stays a
plain list. That split is load-bearing rather than incidental: an explicit role REPLACES
an element's implicit one, so role="region" on the <ul> would stop its <li>s being
exposed as listitems, costing "list, N items" and list navigation for exactly the cohort
this fix is for. It matches the existing precedent for a bounded scroll region in this
codebase (SubagentCompletionCard's transcript body).

  • tabIndex={0} so the region enters the tab order and arrow keys then scroll it natively.
    It has to be the scroller itself, not an outer wrapper: arrow keys scroll the focused
    element's own scroll box, so a tab stop one level up would leave the list unscrollable.
  • role="region" plus an accessible name, so a keyboard user who lands there is told what
    they landed in rather than hearing an unlabelled group. The name reuses the existing
    project_notes catalog key
    rather than adding a string — a new key would land in 13
    locale catalogs and interact with the dead-key ratchet for no benefit, since the visible
    heading already says exactly this.
  • focus-visible:ring-2 ring-inset rather than an outline. An outline on a scroll
    container is clipped to a hairline along one edge, which is not a visible focus
    indicator; an inset ring is drawn inside the box and survives.

One browser behaviour is worth recording, because it changes what the evidence proves.
Chromium 145 puts a scroll container with no focusable children into the tab order by
itself
(keyboard-focusable scrollers). Measured, not assumed: with tabIndex removed,
Tab still reaches the region in the Playwright harness. Safari and Firefox implement no such
behaviour, and the dashboard supports both, so the attribute stays — but it means a
Chromium-based harness cannot be the thing that pins it. The jsdom unit test is, since
jsdom has no equivalent. The harness instead asserts the three things Chromium does not
supply: the accessible name, the surviving list semantics, and a visible focus indicator.

jsx-a11y/no-noninteractive-tabindex fires here and is a false positive. A bounded
scroll region legitimately needs a tab stop — that is the requirement, not a violation of
it. Suppressed with a scoped eslint-disable block carrying the reason, matching the four
existing suppressions in this repo (CodeBlock, ActivityViewer, ui.tsx, SpecDetail).
The block form is deliberate: disable-next-line lands on the element's opening line while
the rule reports against tabIndex several lines below it, so the directive reads as unused
and the warning still fires.

Also in this diff: the "rail" vocabulary is removed wherever it appears. "Storage rail"
is language invented while writing #4779, not something a reader of the settings UI has any
reason to know. Both session_summary help strings now lead with the plain-language
consequence — that what exceeds the ceiling is dropped from the record rather than hidden —
and the term is gone from the two docs/system-specs/modules/session-summary.md rows
documenting those same keys plus one code comment, so the settings UI and the module spec do
not disagree. Removing it from the help text alone would have left them diverged, which is
worse than not starting. Text only; no default, validator, or behaviour changes.

Tests

  • the bounded notes list is reachable by keyboard (vitest, new) — asserts the named
    region carries tabindex="0" and takes focus, that it is the wrapping div rather than
    the <ul>, and that the list is still exposed as a list with its items intact. This is
    the assertion that actually pins the tab stop, for the Chromium reason above.
    Mutation-verified twice: removing tabIndex fails exactly this test and no other, and
    putting role="region" back on the <ul> fails it again on the list-semantics half.
  • focus lands on the notes region and it is NAMED (Playwright harness, new) — tabs
    forward from the disclosure button and asserts the focused element is the region and
    carries a non-empty accessible name.
  • the notes list inside it is still exposed as a list (Playwright harness, new) —
    reads the <ul>'s role and requires it to remain implicit, which is what catches the
    role-clobbering regression in the browser as well as in jsdom.
  • the focused region shows a visible focus indicator (Playwright harness, new) —
    reads the focused element's computed box-shadow, since Tailwind's ring utilities render
    as one; an empty value means the reader cannot see where focus went.
    The name and ring assertions were mutation-verified together — removing the aria-label
    and the ring classes fails both and nothing else, so neither is decorative.
  • The pre-existing geometry and fade assertions still pass in both themes (44 total now).
    They earned their keep here: moving the scroller to the div broke the notes measurement
    until it was retargeted, because the two-sided checks refuse a list that reports natural
    content height instead of a bound one.

Manual verification

The focus ring is a rendered state that could not previously be reached at all, so it is
evidenced by a real frame rather than the no-visual-delta waiver. Captured with the repo's
own scripted Playwright harness (website/scripts/capture-session-summary-height-caps.mjs)
at a 1400x900 viewport, then read to confirm the ring renders rather than trusting the class
name — the computed value is a 2px inset ring in the accent colour.

At rest there is no visual delta: role, aria-label and tabIndex render nothing, and
the ring paints only while the region holds keyboard focus.

Screenshots / video

The notes region holding keyboard focus, reached by tabbing from the "How this project
works" disclosure button — the 2px inset accent ring is the indicator that was missing:

Project notes region focused, showing the inset accent focus ring, dark theme

Light theme, same focused state

Project notes region focused, light theme

Related Issues

no linked issue: this is a follow-up to #4779, raised by that PR's UX review after it had
already merged, so there is nothing filed for it to close.

Checklist

  • Single commit with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@michellemxm
michellemxm requested a review from a team August 21, 2026 03:29
@michellemxm
michellemxm requested a review from a team as a code owner August 21, 2026 03:29
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of 2d74e54ad2cdc0cf5d420a995abc93698ac50cbf — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

The PR's actual diff (via gh pr diff, since the local two-dot diff picks up unrelated main-side commits) contains only the session-summary accessibility fix, the harness/test additions, the "rail" wording cleanup, and two screenshots. I've verified the project_notes i18n key exists, the temp-screenshots/ convention is an established committed-deliverable path per .gitignore, and the wrapper-div-over-role-on-ul choice preserves list semantics correctly.

Design-Verdict: PASS

Real regression (keyboard trap from #4779's height cap), fixed at root with the standard focusable-scroll-region pattern; scope, semantics, and tests all proportionate.

[DESIGN-REVIEWED] 2d74e54

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 2d74e54ad2cdc0cf5d420a995abc93698ac50cbf and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 2d74e54

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 2d74e54ad2cdc0cf5d420a995abc93698ac50cbf: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of 2d74e54ad2cdc0cf5d420a995abc93698ac50cbf — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: PASS

A narrow, well-evidenced keyboard-access fix: the trapped notes list gains a tab stop, an accessible name reusing the visible heading, and a focus ring verified in both theme screenshots.

[UX-REVIEWED] 2d74e54

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 2d74e54ad2cdc0cf5d420a995abc93698ac50cbf — this comment is updated in place on each push.

Review details

The change is a clean, consistent accessibility fix. The ref moves from <ul> to the wrapping <div> which is now the actual overflow-y-auto scroller (notesFade.ref on line 768, type HTMLDivElement on line 301), the i18n key project_notes exists across all 12 locales, and the docstring/comment edits are string-only with no logic change. The discovery pass found no candidates, and my independent falsification pass surfaces nothing grounded to the (a)/(b)/(c) bar.

No findings.

[OPUS-REVIEWED] 2d74e54

Verdict parsed from the review's SHA-scoped output markers for commit 2d74e54ad2cdc0cf5d420a995abc93698ac50cbf.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 2d74e54ad2cdc0cf5d420a995abc93698ac50cbf: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of 2d74e54ad2cdc0cf5d420a995abc93698ac50cbf — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Both data files are read; I verified the claims against the repository (existing suppressions, the SubagentCompletionCard precedent at website/src/pages/chat/SubagentCompletionCard.tsx:250, the project_notes catalog key in 15 locale files, and the remaining "rail" vocabulary). Final review follows.

First-Principles-Verdict: CONCERNS

The keyboard fix earns every attribute it adds, but the "rail" de-jargoning rides along and is itself a point patch.

What this change ships

Intent: let a keyboard-only user reach project notes below the fold that #4779's height cap made pointer-only — a FIX.

  1. The capped notes list is now a Tab stop; arrow keys scroll it — justified
  2. Screen readers hear "How this project works, region" on landing — justified, reuses existing catalog key
  3. A 2px inset accent ring appears while the list holds focus — justified (WCAG 2.4.7)
  4. Settings help for max_intents/max_constraints drops "storage rail" — declared, rides along, symptom-level
  5. Spec table reworded to match the help strings — mandated same-commit spec sync
  6. Capture harness measures the new scroller and asserts name + visible ring — test surface
  7. Comment reword in types/sessionSummary.ts ("storage rail" → "high storage ceiling") — undeclared, rides along
  8. Two focused-state screenshots under temp-screenshots/ — matches the repo's evidence convention

The fix duplicates nothing: grep for focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent finds exactly the one precedent the description cites (SubagentCompletionCard.tsx:250), and the pattern is three attributes, not a mechanism worth sharing.

Watch

  • Item 4's stated harm is "vocabulary invented while writing feat: bound the summary panel height caps and raise the storage rails #4779, not language a reader has any reason to know" — but grep \brail\b leaves 6 unfixed siblings in the same subsystem: 5 in src/kiro_crew/session_summary.py (including the two operator-visible WARNING strings at lines 433 and 459, fired exactly when notes are dropped — the moment the jargon costs most) and 1 in website/scripts/capture-session-summary-height-caps.mjs:57. Either the vocabulary is fine (revert item 4) or it isn't (finish the log strings); the current cut is neither.

[FIRST-PRINCIPLES-REVIEWED] 2d74e54

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 21, 2026
@michellemxm

Copy link
Copy Markdown
Contributor Author

Do not merge on the green label yet — this head carries a known a11y regression

PR Readiness is green on 9cb2bc4f9 and every lane passes, but two lanes independently
found a real defect in this diff and it is not fixed on this head. Flagging it here so
the passing rollup is not read as "clean":

role="region" on the <ul> replaces its implicit list role, so the <li>s stop
being exposed as listitems — a screen reader loses "list, N items" and list navigation
over the notes. Raised by Opus (advisory FINDING) and, independently, by Design
Review
(Suggestion). It is legitimate: an explicit role overrides the host element's
implicit one, so the fix for one accessibility failure introduced another. Design's
framing is the sharpest part — the test in this PR asserts the named region resolves to
the <ul>, which is the clobbered-role symptom written down as a passing expectation.

Also outstanding on this head, both legitimate:

  • UX — the reworded max_intents help still said "sidecar", the same mechanism
    vocabulary this change exists to remove.
  • First Principles — removing "storage rail" from the two settings strings left it in
    docs/system-specs/modules/session-summary.md, the spec rows documenting those exact
    two keys, so the settings UI and the module spec would have disagreed.

All three are fixed and verified locally: the tab stop and name move to the scroller
div so the <ul> stays a plain list, the help string says "before the summary is saved",
and the term is gone from the spec rows (and a last code comment) — 0 occurrences repo-wide.
Both test layers now pin the regression rather than encoding it, mutation-verified by
putting role="region" back on the <ul>: the vitest test fails and the harness reports
list=region. 44 harness assertions, 60 frontend tests, docs-lint, tsc, eslint and flake8
all clean.

They are not pushed because this branch's push authorization is per-push and the initial
one is spent. Per-lane dispositions with evidence follow on the amend, which will also
re-pin the two screenshot URLs to the new commit.

@michellemxm
michellemxm force-pushed the fix/summary-notes-keyboard-a11y branch from 9cb2bc4 to 2d74e54 Compare August 21, 2026 04:04
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: passed Eligible automated validation passed for the current revision labels Aug 21, 2026
@michellemxm

Copy link
Copy Markdown
Contributor Author

Disposition — Opus 4.8 Review (advisory FINDING): fixed in 2d74e54ad

FINDING — role="region" on the <ul> overrides its implicit list role, so the <li>
children are no longer exposed as listitems; a screen reader stops announcing
"list, N items" and loses list-navigation over the notes, a semantics regression this diff
introduces while adding the tab stop.

fixed. Legitimate, and it is this diff's own regression rather than a pre-existing gap:
an explicit role replaces the host element's implicit one, so the fix for one
accessibility failure introduced a second. Taken as given — no rebuttal.

The suggested shape is what landed. role="region", the accessible name, tabIndex and the
focus ring move to a wrapping div, and the <ul> inside it goes back to being a plain
list. One constraint shaped the choice of which element: it had to be the scroller itself,
not the existing outer flex wrapper, because arrow keys scroll the focused element's own
scroll box — a tab stop one level up would have left the list unscrollable and re-opened the
original bug. So the wrapper that gains the attributes is also the element that carries
overflow-y-auto, and the fade stays positioned against the outer box so it does not scroll
away with the content.

Both test layers now pin the regression instead of encoding it, which matters because the
prior test asserted the named region resolved to the <ul> — the symptom written down as
a passing expectation. The vitest test now requires the region to be the div and the list
to still resolve with its items; the Playwright harness gained
the notes list inside it is still exposed as a list. Mutation-verified by putting
role="region" back on the <ul>: the vitest test fails and the harness reports
list=region.

Geometry is unchanged — card 297px, list 261px, 402px of content — verified after the move,
since relocating the scroller initially broke the notes measurement until the harness was
retargeted at the region.

@michellemxm

Copy link
Copy Markdown
Contributor Author

Disposition — Design Review (✅ PASS, Suggestion): fixed in 2d74e54ad

role="region" on the <ul> clobbers its implicit list role (your own test proves it:
the region resolves to the UL), so screen readers lose "list, N items" semantics for the
very cohort this fixes. Moving scroll + tabIndex + role="region" to the existing
wrapper div keeps the list announced as a list.

fixed, and the parenthetical is the most useful line in this round, so it is worth
answering on its own rather than folding into the Opus reply for the same defect.

The observation that the test proved it is correct and was the thing I had not seen. The
assertion read expect(region.tagName).toBe('UL') — the clobbered-role symptom recorded as
an expectation, which is why the suite was green on a real regression. A test that encodes
the bug cannot fail on it, so the fix is not only the markup: the assertion is now inverted
to require the region to be the div and to require getByRole('list') to still resolve
with its listitems, and it was mutation-verified against exactly the shape you named
(putting role="region" back on the <ul> fails it).

The prescription landed as written — scroll, tabIndex, role and the name move to the
wrapper, the <ul> stays plain — with one detail worth recording since it constrains any
future move: the attributes have to sit on the element that actually scrolls, because arrow
keys act on the focused element's own scroll box. Putting them on the outer flex wrapper
while leaving overflow-y-auto on the list would have satisfied the semantics and silently
restored the unreachability this PR exists to fix.

@michellemxm

Copy link
Copy Markdown
Contributor Author

Disposition — UX Review (✅ PASS, Suggestion): fixed in 2d74e54ad

In the max_constraints config help, the rewrite removes "storage rail" but keeps
"sidecar" ("before the sidecar is written") — same class of mechanism vocabulary this PR
exists to purge; say "before the summary is saved".

fixed. Legitimate and self-consistent with the change's own stated purpose: swapping one
piece of internal vocabulary while leaving another in the same sentence is not a cleanup. The
string (in the max_intents help) now reads "before the summary is saved".

Checked the rest of the term before changing it rather than assuming: sidecar appears four
more times in config/loader.py, and all four are code comments or a gatewayd field
unrelated to session summaries, so none of them is user-facing copy and none was touched. The
purge is complete for the surface you flagged without widening into unrelated prose.

Your first-round concern from #4779 — the capped list being pointer-only — is what this PR
exists to fix, and the fix is in 2d74e54ad with its semantics regression resolved (see the
Opus and Design dispositions on this PR).

@michellemxm

Copy link
Copy Markdown
Contributor Author

Disposition — First Principles Review (🟡 CONCERNS): fixed in 2d74e54ad

Two Watch items, taken separately.

The half-removed vocabulary: fixed

Items 5–6 are a vocabulary cleanup riding in an a11y fix. Their own rationale is applied to
2 of the term's sites and not the others: grep leaves occurrences in
docs/system-specs/modules/session-summary.md:393-394, the spec rows documenting these
exact two keys. Either defer the rewrite or delete the term from those two rows in the same
change; half-removed vocabulary now diverges between the settings UI and the module spec.

fixed, and this is the finding I would not have caught. The criticism is exact: the
rationale for the rewrite ("invented while writing #4779") applies uniformly to every site,
so applying it to two of them and stopping leaves the settings UI and the module spec
documenting the same two keys in different language — strictly worse than either finishing or
not starting.

Of the two options you named, finishing is the cheaper one and the one that keeps the
surfaces consistent, so the term is deleted from both spec rows (now "Safety ceiling") and
from the one remaining code comment in website/src/types/sessionSummary.ts. grep -rn 'storage rail|rail, not a display' across *.md, *.py, *.ts and *.tsx now returns
0 occurrences repo-wide, so the claim is checkable rather than asserted. docs-lint
passes over all 216 markdown files.

The repeated attribute triplet: rebutted (no action, as you noted)

The fix itself repeats the attribute triplet inline for the third time
(SubagentCompletionCard.tsx:250, CodeBlock.tsx:35) — acceptable here only because the
alternative is an extraction this lane may not ask for; no action required.

rebutted on proportionality, agreeing with your own conclusion. Three call sites is not
yet the threshold where a shared <ScrollRegion> earns its abstraction, and extracting one
inside an accessibility fix would put a new shared component on the critical path of a
regression repair. Recording it rather than silently accepting it: if a fourth bounded scroll
region appears, that is the point to extract, and the three existing sites are named here so
whoever does it can find them.

@michellemxm
michellemxm enabled auto-merge (squash) August 21, 2026 04:14
@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 21, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved after a description-vs-diff consistency review: every claim in the PR description is backed by the diff, and the diff carries no material change the description leaves unmentioned.

@michellemxm
michellemxm merged commit 2e2ea29 into main Aug 21, 2026
70 of 71 checks passed
@michellemxm
michellemxm deleted the fix/summary-notes-keyboard-a11y branch August 21, 2026 21:29
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 21, 2026
encomjp pushed a commit to encomjp/kirocrew-customapi that referenced this pull request Aug 22, 2026
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