Skip to content

fix(website): keyboard boundary for the picker portal above a Modal - #8496

Merged
iamwhatever merged 2 commits into
mainfrom
fix/sibling-portal-keyboard-isolation-6833
Sep 5, 2026
Merged

fix(website): keyboard boundary for the picker portal above a Modal#8496
iamwhatever merged 2 commits into
mainfrom
fix/sibling-portal-keyboard-isolation-6833

Conversation

@chenmingwei23

@chenmingwei23 chenmingwei23 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

What is the problem?

Modal gained a keyboard boundary in #6832 so the page's global shortcuts cannot
fire from inside a dialog holding unsaved input. That boundary is a bubble-phase
onKeyDown on the dialog panel (ModalDialog.isolateKeys in
website/src/components/Modal.tsx), so it covers the Modal's own React
subtree.

FolderConfigModal renders <ProjectPicker> after </Modal>, so the picker is
a React sibling of the dialog, not a descendant. React routes synthetic events
along the React tree even across a portal, so ModalDialog's handler is never an
ancestor on the picker's dispatch path and never sees its keystrokes.

A global chord typed in either picker field therefore reached
useKeyboardShortcuts' sink — a plain document.addEventListener('keydown', handler) with no capture flag — which navigates away and unmounts the dialog
with the part-filled folder draft still in it.

Verified at d0361761459476b8678e4fc864ee4673211ce2d3 by component and function,
not by line, because the line numbers in this issue's own analysis had already
shifted
before this branch was cut (:355/:361 → the
</Modal>/<ProjectPicker> pair) in a file nobody edited in between:

Fact Where
Overlay is a React sibling FolderConfigModal renders <ProjectPicker> after </Modal>
Overlay portals to document.body ProjectPickercreatePortal(…, document.body)
Boundary is React-subtree scoped Modal.tsxModalDialogisolateKeys
Chord sink is bubble-phase document useKeyboardShortcuts.tsuseKeyboardShortcuts
Focus containment is DOM-scoped useDialogFocusTrapcontainer.contains(active)

Visual evidence

Captured from a harness mounting the real call site — real FolderConfigModal,
real Modal, real ProjectPicker as its React sibling. The one stand-in is the
chord sink: the page's real sink is useKeyboardShortcuts' bubble-phase
document keydown listener, which navigates and thereby unmounts the route
holding the dialog; the harness reproduces a listener at the same target and phase
that unmounts it directly. Mounting the whole router/redux/query shell would add
nothing to the frame, and the property under test is only whether a keystroke
inside the sibling portal reaches a bubble-phase document listener at all
.

Shared starting state — the dialog with a folder name typed, and the picker
open above it at z-[9999] with a path draft in its free-text field:

Picker open above the folder dialog, both carrying a draft

Before — Ctrl+3 in the picker's path field destroys the dialog and the draft:

Before the fix: the chord navigates away and the folder dialog unmounts

After — the same chord is inert; dialog, picker and draft all survive:

After the fix: the chord does nothing and everything survives

The capture script asserts its own outcome and exits non-zero on a mismatch,
so a stale bundle cannot produce a confident screenshot of the wrong thing —
before requires jumped=1 dialogs=0, after requires jumped=0 dialogs=1 draft="/home/u/projects/kirocrew". Both asserted OK with zero page errors.

Note that the resting frames are identical by design: this fix changes what a
keystroke does, not what anything looks like. The discriminating frame is the one
taken after the chord.

Keyboard sequence tested

Screenshots are the weaker half of the evidence for a keyboard fix. These are the
assertions an image cannot carry, each pinned by a test:

  • Tab out of the picker — focus is not stranded in the overlay. The trap
    (useDialogFocusTrap, window capture) hits its refocuses branch precisely
    because the active element is outside the dialog container, so it reclaims focus
    into the dialog. Asserted: after Tab, [role=dialog] contains
    document.activeElement.
  • Tab into the picker — not reachable, for the same reason: the trap pulls a
    Tab back before it can land there. The picker's own controls are mouse-activated
    via onMouseDown + preventDefault(), so they never take focus either.
  • Escape with the picker open above the Modal — dismisses only the picker,
    from both fields (Recent search and Browse path); the dialog stays open with its
    draft. An IME-owned Escape dismisses neither — mid-composition it is
    cancelling a candidate list.
  • Where focus lands on dismissal — on the Browse… anchor inside the
    dialog, not on <body>. Asserted with a real anchorRef, because landing on
    <body> would drop the user out of the dialog and restart the next Tab from the
    top of the page.
  • Arrow keys still navigate the Recent list — the guard must not swallow the
    keys the overlay itself needs.
  • A capture-phase document listener still receives the chord — the rail
    against "hardening" this into a capture-phase guard.

Why this issue matters to the user

Configuring a folder means typing a path. The Browse field is free text, and the
Recent search field is where focus lands the instant the picker opens
(autoFocus, and Recent is selected whenever any recent project exists). One
mistyped Ctrl+digit in either did not beep or no-op — it jumped to another
session, taking the dialog and everything typed into it. That is silent data loss
on the exact surface where the user is mid-edit, and an accessibility defect
independent of any styling question, because a keyboard user has no way to know
the chord is live there.

How our fix solves it

This EXTENDS the isolation. It does not re-parent the overlay. The
createPortal(…, document.body) call is untouched, the fixed z-[9999]
positioning is untouched, and the picker stays exactly where it was in the React
tree. Re-parenting would have moved an anchored popover into the dialog's stacking
and focus context and changed dismissal semantics at the call site; that is a
layout change with visual consequences and it is not what this does. The entire
behavioural change is one onKeyDown prop on the picker's own portal root.

Chaining from symptom to root cause:

  1. Symptom — Ctrl+digit in the picker discards the folder dialog's draft.
  2. Because the chord reaches the page's bubble-phase document listener.
  3. Because nothing on the picker's React dispatch path stops it: the picker's
    handlers claim only navigation keys (useListKeyboardNav takes
    Escape/Enter/Tab/arrows; the Browse field's onKeyDown takes the same set), so
    key === '3' falls straight through.
  4. Root cause — the boundary is scoped to the Modal's React subtree, and this
    overlay is a React sibling of it.
const isolateKeys = (e: React.KeyboardEvent) => {
  if (e.key === 'Escape') { ime.claimKey(e); return }
  e.stopPropagation()
}

Three properties are load-bearing, and I had one of them wrong at first:

  • Bubble phase, on the overlay's own root. Capture-phase listeners must keep
    receiving keys: useListKeyboardNav (document capture) drives the Recent list
    and useDialogFocusTrap (window capture) is the Tab trap. A guard hoisted to
    capture phase would pass a naive test while killing both.
  • Escape excepted. Both dismissal paths that exist today already consume
    Escape before this handler runs, so the exception changes nothing observable
    now — it protects the contract, since stopPropagation() on a synthetic event
    stops the native event too and bubble-phase window is exactly where Modal's
    dismissal listens. A blanket-stop mutant passed every other assertion in the
    new test file
    , so the file now pins the window-bubble property directly. My
    first version of this comment claimed a blanket stop would strand the popover
    open; the mutation disproved that and the comment was corrected to what was
    measured.
  • No third IME latch. The Escape exception reuses the component's existing
    useImeGuard instance instead of mounting another useDocumentImeLatch. This
    issue flags latch proliferation as a real cost of this fix shape; this avoids
    adding to it.

Scope correction, independently reproduced. This issue names two members;
there is one. SimpleSelect renders before </Modal>, so it is already a React
descendant and already covered. I surveyed every <Modal> file for overlays
outside a <Modal> span, with a control requiring ProjectPicker OUTSIDE and
SimpleSelect INSIDE — and the control failed twice first, because my matcher
missed <Comp at end-of-line, which is how both real call sites are written. The
survey was therefore skipping the very file in question while returning a
clean-looking negative. Once the control passed, the only other candidates were
InfoTip (a tooltip: {open && createPortal(…)}, no input, no tabIndex, no
onKeyDown, so no keystroke can originate in it) and page-level SimpleSelect
filters in pages/knowledge/index.tsx that sit behind the only modal's backdrop.
One member with keystroke exposure.

Why two documentation files, in a focus-trap fix

Fair challenge, so here is the accounting for the 432 added lines:

File Added What it is
website/src/test/ProjectPicker.keyboardIsolation.test.tsx 301 the regression pin (12 cases, incl. a control pair)
website/docs/frontend-conventions.md 85 one new convention section
website/src/components/ProjectPicker.tsx 45 9 lines of guard, the rest comment
website/docs/README.md 1 one-line index row

The second "documentation file" is a single line: that README is an index with
one contents row per doc, so adding a section without updating it leaves the index
stale. It is part of the same edit, not a drive-by.

The 85-line section is the deliberate part, and it is the convention case, not a
cleanup: #6832 shipped this boundary with no contract written down anywhere.
Zero .md hits in this repo for keyboard-isolation / isolation-boundary /
global-chord — every match was sandbox and Docker text. Nothing stops the next
overlay from landing beside a </Modal> and reintroducing this exact defect, and
the reason it stayed open this long is that the two overlays in this one dialog
share a portal target and a z-index while only one is covered. So the section
states the rule other portals must follow — coverage tracks the React tree, not
the DOM tree and not the stacking order — with those two overlays as the worked
example. If you would rather review the fix alone, say so and I will split the
docs into their own PR; I have not done so unprompted because the contract is
what stops the recurrence, and the fix without it is one instance of a class.

What tests we did

website/src/test/ProjectPicker.keyboardIsolation.test.tsx, 12 cases, opening
with a control pair because every other assertion is a negative: the same
chord fired inside the Modal body must be stopped, and a chord fired outside every
boundary must be seen. Without both, a green "not called" could just mean the
harness never delivered the key.

  • RED → GREEN: before the fix, 2 failed / 7 passed — exactly the two chord
    cases, both controls green. After: 12/12.
  • Mutation-verified: removing the guard reddens 3 cases; the blanket-stop
    mutant (no Escape exception) reddens the window-bubble case. That second mutant
    survived the first version of the suite, which is why that pin exists.
  • No regressions: 150/150 across the 8 directly-related files (ProjectPicker
    ×3, FolderConfigModal, Modal ×3, and ImeEnterGuardSites — the IME-claim
    ratchet my new claim site could have tripped), plus 28/28 in
    ChatPageMoreCoverage, the only other file that drives real picker elements
    (every ChatSidebar test mocks ProjectPicker to () => null).
  • Gates: eslint src/ --max-warnings 0 clean. The guard first tripped
    jsx-a11y/no-static-element-interactions; resolved the way this repo already
    handles an event-catching container — the disable-with-justification used at
    CommandPalette.tsx's stopPropagation barrier — rather than inventing a role
    that would advertise an interaction the element does not have. i18n:check PASS
    with I18N_BASE_REF=origin/main set so the diff-scoped checks actually ran.
    lint:i18n, lint:phantom-classes, lint:theme-colors, jscpd PASS.
  • Pre-existing, not from this diff: tsc -b reports exactly 1 error,
    ShareMessageModal.tsx / html-to-image, byte-identical on the pristine main
    checkout
    — the package is in package.json and the lockfile but absent from
    node_modules. Zero type errors outside it.

Any other suggestions on the work

  • The design decision is still the maintainer's to ratify. This issue
    enumerates three mutually exclusive designs and carries needs-human. I
    implemented option 2 because it is bounded and visually inert, and did the
    documentation work every option needs. Filed as Refs #6833 rather than a
    closing keyword, so the titled scope stays open for that call.
  • A lint rule would be worth more than this fix. The contract now exists in
    prose only; the survey in this PR is essentially the rule's logic.
  • The two-latch item from the issue body is untouched. useDialogFocusTrap
    exporting its useDocumentImeLatch so Modal's boundary reuses it (Opus 5
    advisory, declined in fix(website): stop global chords at the shared Modal panel (#6800) #6832 as a public-API change) is unaffected here — I
    avoided adding a third latch but did not consolidate the existing two.
  • InfoTip is the class's boundary case. It portals at z-[9999] and does
    render outside <Modal> spans; it is excluded only because it takes no
    keystrokes. If it ever gains a focusable control, it joins the class.

Pattern harvest

Rule candidate: eslint
Pattern: a component calling createPortal rendered as a JSX sibling of
<Modal> in the same return, with no onKeyDown on its portal root — the
sibling position is what puts it outside the dialog's keyboard boundary, and it
is statically detectable. The control-bearing survey in this PR is the rule's
logic; it found exactly one such site.

  • A z-index is a paint-order fact and says nothing about event routing. Both
    overlays here portal to document.body at the same z-[9999]; one is inside the
    boundary and one is not, and the difference is React-tree position. The mix-up
    traces to a source comment that made a correct paint-order claim which was then
    read as an event claim.
  • A survey's negatives are worth nothing until its control passes. Mine
    reported a tidy "only InfoTip is affected" while silently skipping the one file
    everyone already knew was affected, because <Comp[\s/>] does not match <Comp
    at end-of-line. State what a positive looks like, then require the method to
    produce it before believing any absence.
  • Mutate the branch you claim is load-bearing, not just the code you added.
    The chord fix was pinned immediately; the Escape exception was not — a blanket
    stop passed all nine tests. A branch that is unreachable by focus needs a pin at
    the contract level (does the key still reach the phase a future dismissal would
    use?) rather than the behaviour level.
  • "No visual delta" is usually a claim about the wrong frame. I first argued
    this fix could not be screenshotted because before/after would be identical
    images. True of the resting state, false of the state after the keystroke —
    which is the whole point of the fix. Capture the frame after the input, not the
    component at rest.
  • A negative result is a finding to publish, not a section to omit. This issue
    implies the overlay's dismissal competes with the modal's; measured, it does not,
    and focus does not leak into the overlay either — the trap actively reclaims it.
    Both are now pinned so the correct behaviour cannot silently regress into the
    behaviour the issue described.

Refs #6833

@chenmingwei23
chenmingwei23 requested a review from a team September 4, 2026 16:54
@chenmingwei23
chenmingwei23 requested a review from a team as a code owner September 4, 2026 16:54
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

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

All evidence reviewed — the diff is a single behavioral guard (one onKeyDown on the picker's portal root), a regression test file, and developer docs. No user-facing strings, layout, or controls change; the screenshots confirm the resting UI is pixel-identical and the only delta is that a mistyped Ctrl+digit no longer destroys the folder dialog's draft.

UX-Verdict: PASS

Pure loss-prevention fix: a mistyped global chord in the picker no longer silently destroys the folder dialog's draft, matching the boundary every Modal already has.

The chord now no-ops inside the picker exactly as it already does inside every <Modal> body, so the behavior is consistent with the product's learned pattern rather than a new mode. Escape still dismisses only the picker, arrow keys still drive the Recent list, and Tab focus containment is unchanged — the flow a user actually performs is untouched except for the removed data-loss path.

[UX-REVIEWED] 9183954

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

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

Design-Verdict: PASS

Guard sits at the right ownership layer — the sibling overlay owns its own boundary — bounded, visually inert, control-and-mutation-verified, with the class contract written down.

Watch

Suggestions

  • The class guard is prose-only; the next createPortal sibling of </Modal> reintroduces the defect silently. The PR's own survey logic is the eslint rule — file it as a concrete follow-up issue so it doesn't evaporate.

[DESIGN-REVIEWED] 9183954

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 9183954f8e970d50a763c1b2daa3135c3ed6f824 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 9183954

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

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of 9183954f8e970d50a763c1b2daa3135c3ed6f824 — 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.

All checks complete. temp-screenshots/ is a documented convention (PR template line 47, cleanup workflow), the fix responds to reported defect #6833, and I verified the consumers of ProjectPicker (FolderConfigModal.tsx:499, ChatPage.tsx:9616) and the rejected re-parent alternative against the doc the PR itself ships.

First-Principles-Verdict: CONCERNS

The fix earns its place at cause level, but the description rejects the smaller re-parent fix with a rationale the PR's own doc contradicts.

What this change ships

Intent: stop a global chord typed in the project picker from destroying the folder dialog and its draft — a FIX.

  1. Ctrl+digit/Settings chord in the picker above the folder dialog is now inert — justified (reported defect Sibling-portal overlays above a Modal are outside its keyboard-isolation boundary #6833), cause-level for every picker mount.
  2. Escape still dismisses only the picker; IME-owned Escape dismisses nothing — justified, preserves existing dismissal.
  3. Chords are now also inert in the ChatPage input-bar picker (ChatPage.tsx:9616, no dialog beneath) — rides along; not in the visible description.
  4. New 87-line "Keyboard isolation" doc section plus README index line — declared; same-commit doc rule.
  5. Three before/after screenshots under temp-screenshots/ — documented convention (PR template, cleanup workflow).
  6. 301-line test file pinning the boundary's contracts — justified.

Watch

  • The description rejects re-parenting as "a layout change with visual consequences", but the doc this same PR adds says a React descendant whose portal targets document.body "costs you no stacking freedom" and shows SimpleSelect covered exactly that way — and its rule 1 says to prefer that shape. Moving {pickerOpen && <ProjectPicker/>} inside <Modal> at FolderConfigModal.tsx:498 is a ~3-line fix by the PR's own guidance. The component-level guard is broader (it also covers ChatPage.tsx:9616), which is a valid justification — but it is not the one stated.
  • Item 3 changes shipped behavior on a surface the fix was not about; confirm it is intended, not incidental.

Subtractions

  • Shrink the ~40-line comment at ProjectPicker.tsx (above isolateKeys) to the invariants and a pointer: it restates the new frontend-conventions.md section nearly line for line (React-tree routing, paint-order conflation, Escape exception, mutation evidence) — the same explanation now lives in three places and will drift.

[FIRST-PRINCIPLES-REVIEWED] 9183954

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 9183954f8e970d50a763c1b2daa3135c3ed6f824 — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 9183954

Verdict parsed from the review's SHA-scoped output markers for commit 9183954f8e970d50a763c1b2daa3135c3ed6f824.

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

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Sep 4, 2026
@chenmingwei23

chenmingwei23 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Disposition of both reds on 2fa2b3b915174537a60e122910b83782b0f0d47b — measured, zero delta

Neither red is reachable from this diff. This diff is 7 files, and none of them is Python:

temp-screenshots/sibling-portal-keyboard-isolation-6833/01-picker-open-above-dialog.png
temp-screenshots/sibling-portal-keyboard-isolation-6833/02-before-chord-destroys-dialog.png
temp-screenshots/sibling-portal-keyboard-isolation-6833/03-after-chord-is-inert.png
website/docs/README.md
website/docs/frontend-conventions.md
website/src/components/ProjectPicker.tsx
website/src/test/ProjectPicker.keyboardIsolation.test.tsx

1. GPT 5.6 ReviewBLOCK-MERGE on a file this branch does not contain

The finding is src/kiro_crew/config/superseded_defaults.py:272, os.close(fd), anchored to
no-blocking-call-on-event-loop.

  • The file is not in the diff: git diff --name-only origin/main...HEAD | grep -c superseded_defaults0.
  • It is byte-identical to main: git diff origin/main...HEAD -- src/kiro_crew/config/superseded_defaults.py → empty.
  • The flagged call exists on origin/main already, inside a finally: block:
    except OSError as e:
        logger.debug("Ignoring unreadable superseded-default acknowledgments: %s", e)
        return None
    finally:
        os.close(fd)

So the finding is zero-delta and pre-existing. It is not a false positive about the code — the
rule may well be right about that os.close — it is not applicable to this PR, which changes
no Python whatsoever.

The cause is visible in the sibling lane: First Principles Review independently reported that
"the workflow's diff was cut against a stale base", that "the remaining ~130 files in the provided
patch are base drift from commits already on main", and identified the giveaway — the patch
"adds" temp-screenshots/remote-crew-local-session/ and prompt-draft-back-guard/, which are the
evidence directories of bad30a003 (#7693) and e125819af (#8131), both already merged. The GPT
lane reviewed main's history alongside this branch and blocked on a line from it.

I am not posting /ai-review override — that is not mine to use. Flagging for a maintainer.

2. Backend Tests (Windows) (4) — main-owned, landed today, arrives via the merge ref

5 failures, all in test/test_slot_close_recreation_race.py, rooted in one signature that then
crashes four xdist workers:

AttributeError: '_Req' object has no attribute 'can_read_body'
5 failed, 3556 passed, 292 skipped in 564.49s

That test's _Req is documented in-file as a "minimal stand-in for the aiohttp request the
handlers read", and it does not implement can_read_body, while the shared body guard on main
does require it (src/kiro_crew/dashboard/handlers/_shared.py:142,
if allow_absent and not request.can_read_body:).

Provenance, by ancestry rather than by assertion:

So the failing test entered CI only through the pull_request merge ref (this branch merged
with main's tip). It is main-owned and should be reddening every open PR that runs this lane.

I am deliberately not fixing it here: a backend test-double repair does not belong in a
frontend keyboard-isolation diff, and folding it in would make this PR unreviewable for what it
actually changes. Rebasing now would inherit the same red, since the breakage is on main itself —
so the plan is to rebase onto main once that lane is green there, in a single push, and re-pin
the screenshot URLs afterwards (a pinned raw URL only resolves the commit it names).

Verdicts held on this head, for the record

UX-REVIEWED, DESIGN-REVIEWED, FIRST-PRINCIPLES-REVIEWED and OPUS-REVIEWED are all
PASS / no-blocking on 2fa2b3b915174537a60e122910b83782b0f0d47b, and PR Hygiene and
Screenshot Evidence both went green on it. Each of those binds this sha only, and only until
re-run.

Two advisories from First Principles, accepted

  • Undeclared rider, now declared. The guard lives on the shared ProjectPicker, so it applies
    to every call site, not only FolderConfigModal — including the composer's picker
    (ChatPage.tsx). Same harm class (a chord destroying the picker's own path draft), so the
    broader coverage is intended; it was simply not stated. The defect class in Sibling-portal overlays above a Modal are outside its keyboard-isolation boundary #6833 — an
    input-bearing overlay rendered as a React sibling of a <Modal> — still has exactly one member.
  • Two copies of the boundary must now co-evolve (Modal.tsx's isolateKeys and this one),
    differing only in which IME primitive they hold. That divergence is deliberate and commented —
    reusing this component's existing useImeGuard avoids mounting a third document-tracked latch,
    which the issue names as a cost of this fix shape — but the coupling is real and is called out
    in the new convention section so the next editor sees it.

Update — main's own CI confirms red #2 is not this branch's

The ancestry argument above is now backed by a direct measurement rather than inference. Main's own
completed ci.yml run 33904819490, on main's head 701f8f981 with no pull request
involved
, reports:

Backend Tests (Windows) (1) -> success
Backend Tests (Windows) (2) -> success
Backend Tests (Windows) (3) -> success
Backend Tests (Windows) (4) -> failure    <-- same lane, same shard as this PR's red

So Backend Tests (Windows) (4) fails on main by itself. This PR inherits that red and cannot
influence it.

Still unfixed as of main 17a2abe43: can_read_body appears zero times in
test/test_slot_close_recreation_race.py, while src/kiro_crew/dashboard/handlers/_shared.py
requires it. A repo-wide search of open issues and PRs for can_read_body and for
test_slot_close_recreation_race returns nothing, so the break appears untracked and should be
reddening shard 4 on every open PR.


Note on marker hygiene in this comment

This comment originally spelled the lanes' verdict markers verbatim, in square brackets, while
merely describing them. That was a mistake: those tokens are a machine contract, so quoting one
in prose makes human commentary indistinguishable from a lane's own verdict and inflates any count
taken over comment bodies — this PR read as carrying two blocking verdicts when the GPT lane
emitted exactly one (its own adjudication line records total=1 uphold=1 downgrade=0). The
markers above are now backticked without brackets so only the lanes emit the real tokens.


Update — rebased onto current main; the Python finding is re-measured and still not in this diff

Head is now 9183954f8e970d50a763c1b2daa3135c3ed6f824, rebased onto 5a9b5f0d8 (clean, no
conflicts, 2 commits, the same 7 files). The screenshot URLs above are re-pinned to the new commit
and each returns HTTP 200.

Re-measured on the new base, so nothing here rests on the earlier reading:

$ git diff --name-only origin/main...HEAD | grep -c superseded_defaults
0
$ git diff origin/main...HEAD -- src/kiro_crew/config/superseded_defaults.py | wc -l
0

The GPT lane's single finding (total=1 uphold=1 downgrade=0) names
src/kiro_crew/config/superseded_defaults.py:272 and prescribes moving an acknowledgment-file read
and os.close(fd) onto an executor-backed path. This PR contains zero lines of that file, so
the finding cannot be satisfied from here: changing it would mean editing unrelated backend Python
inside a frontend keyboard-isolation diff purely to quiet a lane, which is worse than the red.

Recorded so a reviewer can see the lane is red for a reason that is not this change. The finding
may well be correct about that os.close; it belongs to whoever owns that file.

For completeness, the verdict was also checked for the shape that would have been in scope — a
focus-order, aria-semantics, or overlay-placement objection. It contains none: zero occurrences of
re-parent, child of, descendant, focus order, aria, or tab order.

…oundary

Modal's keyboard boundary (#6832) is a bubble-phase handler on the dialog
PANEL, so it covers the Modal's own React subtree. FolderConfigModal renders
ProjectPicker after </Modal>, making it a React SIBLING, and React routes
synthetic events along the React tree - so Modal's handler is never an ancestor
on the picker's dispatch path. A Ctrl+digit typed in either picker field
therefore reached useKeyboardShortcuts' bubble-phase document listener,
navigated away, and unmounted the dialog with its part-filled folder draft.

Adds the same surgical guard to the picker's portal root, reusing the
component's existing IME guard rather than mounting a third document-tracked
latch. Escape stays excepted, so bubble-phase dismissal keeps working.

Also writes the contract down: coverage follows the REACT tree, not the DOM
tree and not the stacking order. Both overlays in FolderConfigModal portal to
document.body at the same z-[9999]; only one is inside the boundary. Treating a
shared stacking context as an event-routing fact is what kept this open.

Refs #6833
…boundary

Captured from a harness mounting the real FolderConfigModal (real Modal, real
ProjectPicker as its React sibling). The only stand-in is the chord SINK: the
page's real one is useKeyboardShortcuts' bubble-phase document keydown listener
which navigates and thereby unmounts the dialog; the harness reproduces it at
the same target and phase.

The capture script asserts its own outcome and exits non-zero on a mismatch, so
a stale bundle cannot yield a confident screenshot of the wrong thing:
  before -> jumped=1 dialogs=0
  after  -> jumped=0 dialogs=1 draft='/home/u/projects/kirocrew'

Refs #6833
@chenmingwei23
chenmingwei23 force-pushed the fix/sibling-portal-keyboard-isolation-6833 branch from 2fa2b3b to 9183954 Compare September 5, 2026 00:07
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 5, 2026
@iamwhatever
iamwhatever merged commit fc30d57 into main Sep 5, 2026
66 of 72 checks passed
@iamwhatever
iamwhatever deleted the fix/sibling-portal-keyboard-isolation-6833 branch September 5, 2026 00:43
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 5, 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