Skip to content

fix(dashboard): one list-shell recipe for sessions sidebar and members roster - #9428

Open
CrysisDeu wants to merge 1 commit into
mainfrom
fix/shared-drag-handle
Open

fix(dashboard): one list-shell recipe for sessions sidebar and members roster#9428
CrysisDeu wants to merge 1 commit into
mainfrom
fix/shared-drag-handle

Conversation

@CrysisDeu

@CrysisDeu CrysisDeu commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Closes #9416

What

The Crew Members roster and the Sessions sidebar are now built from one set of recipes — same card (and the same theme hooks), same header line, same title, same list body, same row box, same row type scale, same column-resize grip on the card's border, and the same search row with the sort/filter menu docked in the field. Three of the drifts were reported by an insider ("这个 drag handle 和 session 页面的不一样", "crew list 的背景和 session list 背景颜色不一样", "crew member 的搜索 bar 能不能和 session 的看齐(把 filter 集成进去)"); the audit found the rest.

Sidebar look wins throughout (older, what users know). Where the roster keeps a difference, it is named below with the reason.

Measured before → after

Playwright getComputedStyle on two pods (base = main @ 6fbb06b, fix = this branch), 1440×900, idle row. Full JSON in the PR's capture run; the user-facing numbers:

Property Sessions sidebar Roster BEFORE Roster AFTER
Panel background, kiro-light #fafafa (--panel) #ffffff #fafafa
Panel background, kiro-dark #211d25 #211d25 #211d25
Header padding T/R/B/L · margin-top · height 0/8/0/8 · 2px · 40px 16/16/4/16 · 0 · 48px 0/8/0/8 · 2px · 40px
Title letter-spacing 0.56px normal 0.56px
List padding-top 8px 0px 8px
Row padding-left 14px 10px 14px
Row padding-right 12px 32px 32px — kept: the star button overlays the row's right padding
Row title weight · line-height · colour at rest 600 · 20px · text-text 500 · 20.15px · text-muted 600 · 20px · text-text
Row secondary line-height 16px 17.05px 16px
Row hover / selected bg bg-bg-hover / bg-accent-subtle same same
Grip: hit / bar / hover / active / inset 5px · 2px pill · bg-accent · bg-accent-hover · 12px 6px flat strip · bg-accent/30 · none · none 6px · 2px pill · bg-accent · bg-accent-hover · 12px
Grip geometry (boundingBox) strip y=43 h=848, bar y=55 h=824 strip y=42 h=850, no bar strip y=43 h=848, bar y=55 h=824
Search row SearchInput + clear button + 24px ListFilter menu trigger docked in the field bare SearchInput, no clear button, a wrapping row of 4 filter chips under it same field, clear button and docked trigger; filters in the menu

Kept differences, by design: the roster keeps its "N members" count line under the header (the sidebar's header has no count); the roster header leads with the page icon (it names a page, the sidebar title sits next to the rail toggle); rows are two lines (name + last message) where session rows carry a third meta line; presence rides the avatar and unread sits on the row's right edge (IM convention) instead of leading the secondary line.

How

  • components/listShell.ts (new)LIST_SHELL_CLS (sidebar sidebar-inner bg-bg-elevated border border-border rounded-xl shadow-sm: the sidebar theme-pack hook and the sidebar-inner kiro-light hook from feat(theme): white chat canvas with grey shell in kiro-light #8711 lead the string), LIST_HEADER_CLS, LIST_TITLE_CLS, LIST_BODY_CLS, ROW_BOX_CLS, ROW_IDLE_CLS, ROW_ACTIVE_CLS, ROW_META_CLS / ROW_TITLE_CLS / ROW_STATUS_CLS. These are the sidebar's own class strings, moved, not new values.
  • pages/ChatSidebar.tsx — consumes all of the above where it used to spell them (root card, header, title, both lane bodies, the session row's box/idle/active classes, the three type constants). Its inlined grip is replaced by <ResizeHandle inset={12} className="sidebar-resize-handle absolute top-0 -right-[3px] h-full z-10" …/> plus nudgeSidebar for arrow keys (same clamp + persist a drag applies). .sidebar-resize-handle stays as the hook the mobile overlay and split-pane host use to hide it.
  • pages/members/MembersPage.tsx — the roster card, header, list and rows consume the same recipes (cn() merges pr-8 over the box's pr-3 for the star). The grip moves from an in-flow flex column between the cards onto the roster card's right border (absolute, same 12px inset); the roster→thread gutter goes from 22px to the plain gap-2, and THREAD_MIN_RESERVE follows. Label becomes an action, pages.membersPage.resize_roster ("Resize member list"), in 13 locales + regenerated en-XA.
  • pages/chat/SessionFlyout.tsx — its copy of the title recipe becomes LIST_TITLE_CLS.
  • components/SearchFilterBar.tsx (new) — the sidebar's search row: SearchFilterBar (field, clear button once there is text, trailing-controls slot, and the sidebar's padding table — 36/56px with one/two 24px controls, +20px with text — so typed text never runs under the buttons) and FilterMenuButton (the 24px ListFilter trigger with the capped corner badge, forwardRef for DropdownMenuTrigger asChild), plus the menu's label/content class recipes. ChatSidebar mounts both and drops its inlined copies; the roster replaces its bare SearchInput + chip row with them.
  • pages/members/rosterFilter.ts (new) — the roster's filter/sort model as pure functions over the member array (sortRoster, the order-preserving narrowRoster, matchesStatus, countByFilter, queryNarrows, the storage parsers), plus matchesSource / parseSourceFilter, moved here from MembersPage (nothing else imported them; the one test that did now imports rosterFilter). The page hands in a RosterQuery and a signalsOf(member) resolver for the live facts (running from the slot frames, needs-you from tabStatus = permission | question, unread, active patrol) — nothing in the model reads React state or storage, so the data layer can move to React Query underneath without touching how a filter decides.
  • Members filter menu — FILTER: Starred, In progress, Needs your approval or answer, Unread, Scheduled check-ins (checkbox rows, OR across the live states like the sidebar's session filters, AND with star/origin/search; each row shows its count, so the zero-count row is visibly the one that would blank the list; rows keep the menu open). "In progress" and "Unread" reuse the sidebar menu's own keys (pages.chatSidebar.filter_running / filter_unread), so the same slot state cannot read as two words across the two menus. ORIGIN: Mine / Built-in / From packages (radio rows, choosing the active one clears it — the old chip semantics). SORT BY: Recent activity / A → Z (the A–Z row reuses the sidebar's pages.chatSidebar.sort_name_asc; the activity row keeps a member-specific word because the sidebar's "Newest" reads as "newest member" on a list of people). Status set and sort persist under mc-members-status / mc-members-sort next to the existing star/origin keys; the trigger's badge is the unread count — the same meaning the sidebar's trigger badge carries. New keys pages.membersPage.{sort_filter_members, filter_origin, filter_status_needs_you, filter_status_patrolling, filter_status_*_description, sort_recent, sort_name} in 13 locales + regenerated en-XA; the Patrolling tooltip is in task terms ("Members running a scheduled check-in loop").
  • components/ResizeHandle.tsx — renders the sidebar's visual: 6px transparent hit strip (group/drag) carrying a 2px rounded-full bar, group-hover:bg-accent / group-focus-visible:bg-accent / group-active:bg-accent-hover, resize-accent so the Kiro dark theme's retint applies. New props inset and className (tailwind-merged). Keyboard/ARIA contract unchanged; Issue Radar and Projects consumers inherit the look.
  • index.css comment + docs/theming-contract.md — the sidebar hook row now names both cards and the module that carries the hook.

Tests (written, not run locally — see below)

  • test/listShellParity.test.ts (new) — both pages import and use every shared recipe; neither spells out any of the replaced literals (the shell string, header, title, body, 'text-text-strong bg-accent-subtle', text-[13px] font-medium truncate, the three leading pairs); the recipes carry the sidebar's exact values; the Notes rail's inline RAIL_TYPE still mirrors the type scale.
  • test/ResizeHandle.parity.test.tsx (new) — both pages mount ResizeHandle and hand-roll no aria-orientation="vertical"; the bar recipe, insetcalc(100% - 24px), className merge, onNudge/tabIndex contract.
  • test/kiroLightShellHooks.test.ts — the sidebar-inner hook is now pinned in components/listShell.ts, and a new case requires both ChatSidebar.tsx and MembersPage.tsx to mount LIST_SHELL_CLS (and forbids the old literal), so the kiro-light step-back cannot reach one card and not the other again.
  • MembersPage.test.tsx — the DetailPanel separator assertion narrowed to /resize panel/i (the roster grip is now a second /resize/i separator on the page).
  • test/searchFilterBar.parity.test.tsx (new) — both pages import and mount SearchFilterBar + FilterMenuButton; neither re-inlines the list search field, the trigger markup or the roster chip row; the component contract (clear button appears with text and clears through onChange, the 36/56/+20 padding table, clear-button offsets, badge cap at 99+).
  • pages/members/rosterFilter.test.ts (new) — every dimension without a DOM: search, star, origin buckets, each status alone, OR across statuses, AND across dimensions, sort applied to filtered rows, counts, and the storage parsers rejecting junk. Expectations were dry-run against an esbuild bundle of the module.
  • MembersPage.filters.test.tsx — the chip interactions become menu interactions (open with Enter on the trigger, as the sidebar's own filter tests do; aria-pressedaria-checked); new cases for the status rows (OR, persistence, filtered-out notice), the sort rows, a persisted sort on mount, and the shared search row's clear button and trigger.

Mutation-verified by inspection: re-inlining any listed literal fails the negative pins; dropping a recipe's use fails the positive ones; dropping rounded-full or a hover/active class from the bar fails the recipe pin.

Screenshot Evidence

Two isolated pods, 1440×900 @2x. Left pair BEFORE, right pair AFTER; within each pair Sessions sidebar then Crew Members roster.

Light — the two cards side by side (BEFORE: roster white on the white canvas, sessions grey; header/title/list/row metrics differ. AFTER: same card colour, same header line, same row type.)
light panels before/after

Dark — the two cards side by side
dark panels before/after

Search row + filter menu open, dark (BEFORE: roster has a bare field and a wrapping chip row; AFTER: the sidebar's field, clear button and docked menu on both, the roster's menu holding star / live state / origin / sort — full height, through SORT BY)
dark search filter before/after

Grip — rest, hover, active (dragging) — both panels, AFTER (the BEFORE roster grip is the flat strip measured in the table above; these replace the narrow crops round 7 called unreadable.)
light grip states, members
dark grip states, sessions

Grip, light, top corner ×4 — the 12px inset spans exactly the straight segment of the roster card's border, as on the sidebar
light top corner before/after

Search row with text typed — the clear button, both panels, light + dark
search typed clear

Filters on, menu closed — the at-rest marker round 6 asked for (ONE aggregate chip in the sidebar's chip recipe — the shared FilterChip, which the sidebar's per-filter pills also render — whose visible text is the click's outcome — "Clear Starred (2), Mine (6) filter", the same sentence its accessible name carries, so nobody has to guess whether the one ✕ drops one filter or all. It wears the sidebar tag filter's NEUTRAL clear-all shape, not a filter colour: on both lists a coloured pill clears one filter, a neutral pill clears several. One control, not one per filter: AUTOSDE max-two-buttons-per-row.)
light roster chips
light sidebar chip

Origin row checked — "Mine ✓" with Starred, the state the chip below names
light origin menu

Status rows checked — lit glyphs and checks ("In progress" and "Unread" on: the Zap fills in its warn colour, the Unread dot fills accent; a checked row keeps its count.)
dark status menu

Filters applied — the states round 5 asked to see (Starred checked: lit star, check, every row's count right-aligned like ORIGIN's, 0 included; the count line reads "2 of 14 members"; a zero-count status on top empties the list into the "Clear filters" notice.)
light starred menu
dark starred roster
light filters hide all

More: light search row + menu, light grip hover, active (mid-drag) states, dark top corner

light search filter
light grip states, sessions
dark grip states, members
dark roster chips
light status menu
dark origin menu
dark starred menu
dark top corner

Pattern harvest

The Members page mirrors the chat surface, and every mirror drifts where it re-implements instead of reusing. Same family as #8852 (steer-only composer) and #8947 (Quote/Ask). This instance has two shapes of the same failure:

  1. Copied value. The roster re-typed the sidebar's header, row and type values; feat(theme): white chat canvas with grey shell in kiro-light #8711 then changed the sidebar's kiro-light background through a hook the copy did not carry, and the two cards split colour the same day. A copied value cannot receive a later change to its source. Fix shape: one module of whole class strings, both pages import it, a test forbids the literals.
  2. Shared-but-wrong-source. The roster's grip DID reuse a shared component — but ResizeHandle had been extracted from the app workspaces, not from the sidebar, so "shared" still meant "not the sidebar's". When a primitive is extracted it should absorb the oldest inlined copy, otherwise the extraction manufactures a second convention.

Rule candidate: when a page is built as a mirror of an existing surface (the Members roster of the Sessions sidebar, a pane of the chat page), it must import that surface's class recipes and components rather than copy their values — and a parity test naming both surfaces must forbid the literals — because a copied value cannot receive a later change to its source (here #8711's kiro-light hook). Corollary for extraction: when a primitive is pulled out of one surface, absorb the OLDEST inlined copy of it in the same PR, or the extraction manufactures a second convention.

Follow-ups (not in this PR)

  • Hand-rolled vertical grips that should adopt ResizeHandle (one-liners now that className/inset exist): apps/code-review-sage/Workspace.tsx (Splitter), apps/spec-builder/components/ColumnSplitter.tsx, apps/spec-builder/components/SpecDetail.tsx, pages/WebhooksPage.tsx; components/DetailPanel.tsx (left edge, flush — needs an edge prop).
  • Horizontal siblings — ChatSidebar history-pane splitter, SessionGridLayout divider, ChatInput composer grip — want a horizontal variant of the same recipe.
  • apps/md-notebook/constants.ts RAIL_TYPE mirrors the row type scale by value (it is styled inline); pinned by the parity test, still a copy.
  • The roster's Pause patrol badge glyph ("||" on the avatar) can read as a grip; if that is what the insider's crop pointed at, it is a glyph question, separate from this PR.

Review round 1 (on aace964b7)

  • CI: MembersPage.test.tsx roster-order selector followed the title weight (.font-medium.font-semibold); zh-CN terminology (回合轮次 for "turn"); hi tone (formal आपतुम in the three new strings).
  • UX CONCERNS, each implemented: Patrolling tooltip rewritten in task terms; "Working" → the sidebar's "In progress" by reusing its key (and "Unread" likewise); roster trigger badge = unread count like the sidebar's; full-height open-menu screenshots re-captured; a typed-text shot showing the clear button added. The needs-you badge no longer exists to screenshot.
  • First Principles subtractions, each taken: matchesSource / parseSourceFilter re-export dropped (the one importer, MembersPage.filters.test.tsx, imports rosterFilter directly); style prop dropped from ResizeHandle; EMPTY_QUERY export dropped (the test builds its base query). The Watch item on the status/sort dimensions is dispositioned in a comment: they are the user's ask, relayed by the conductor ("filter 菜单里放成员相关维度:状态 … crew 归属 … 排序"), not symmetry-inherited.

Review round 2 (on 5997f9877)

  • UX: the "Patrolling" row is now the task word Scheduled check-ins (13 locales); the sort rows reuse the sidebar's Newest / A → Z keys (pages.chatSidebar.sort_newest / sort_name_asc) instead of "Recent activity" / "Name", so one ordering has one name in both menus. The filters-behind-the-funnel item is the insider's ask ("把 filter 集成进去") and keeps the counts visible, as the review accepted. The lit unread badge is not screenshotted: it is the sidebar's own badge markup on the shared FilterMenuButton, exercised by searchFilterBar.parity.test.tsx (count and 99+ cap), and a pod has no unread member without a live agent turn — dispositioned in a comment.
  • First Principles repeats the "status/sort ride along" Watch; dispositioned again for this head — the dimensions are the user's request as relayed by the conductor, not symmetry.

Review round 3 (on 96cb0a3d9)

  • UX: "Needs you" → Needs your approval or answer (the sidebar's own status vocabulary, 13 locales); the sort row goes back to the member-specific Recent activity — round 1 asked to align it with the sidebar's "Newest", round 2's cold reader then read "Newest" as "newest member", so the row names what it sorts by (last activity) and only A → Z stays shared. Open-menu screenshots re-captured against the shipped strings; the rendered menu text was read back from the pod: FILTER | Starred | In progress | Needs your approval or answer | Unread | Scheduled check-ins | ORIGIN | Mine · Built-in · From packages | SORT BY | Recent activity | A → Z.
  • The lit unread badge remains dispositioned (shared sidebar markup + parity test; a pod cannot produce an unread member without a live agent turn).

Review round 4 (on c1ed4ef4b)

  • UX: the "Scheduled check-ins" tooltip defined check-in with check-in; it now reads "Members that run automatically on a schedule" (13 locales). The lit unread badge remains the one item for a human reviewer to accept — its disposition (shared sidebar markup + parity test; no honest way to light it on the capture pod) is on the thread.

Review round 5 (on 890dc4c82, after the rebase onto main @ 50cc659de)

  • Rebase: main moved the roster onto React Query (refactor(members): move the Crew Members data layer onto React Query #9442) and renamed the member editor deep link (fix(members): pencil right of the name opens the member editor; DM face stays a face #9434); the one conflict in MembersPage.tsx was resolved by keeping main's data layer (slotKeyOf, threadOutcome, the loadError dash) under this PR's filter model (the filter memo is now rosterFilterQuery, apart from main's rosterQuery read).
  • Bundle Size Gate: App over its ceiling by 67 B and t by 66 B. Measured: main's own tip builds them 373 B / ~520 B under the ceilings set on 2026-09-04, and this PR adds ~440 B / ~590 B (three small shared modules, ten catalog strings). Both entries re-based on the new measurement with the same 5% convention, in check-bundle-size.mjs, with the measurement in the comment.
  • UX suggestion taken: every filter row shows its count the same way — right-aligned, muted, 0 included — where the Starred and status rows used to hide a zero. Screenshots of the applied-filter states added above (Starred checked with the menu open, the narrowed roster with "2 of 14 members", the filters-hide-all notice).
  • First Principles subtraction taken: INPUT_PAD_RIGHT / CLEAR_RIGHT in SearchFilterBar.tsx are three entries (no mount reaches a third trailing control). Items 7–9 (status filters, sort, badge) stay: their provenance is the user's relayed request, dispositioned on the thread for this head.

Review round 6 (on b8aec806d)

  • Bundle Size Gate green with the re-based budgets, which also answers First Principles' item-10 Watch ("clears when CI's bundle-size check passes").
  • Rebased again onto main @ 5f2202429: main re-measured the App ceiling itself (chore(bundle): re-measure the App chunk ceiling (3360 -> 3530 KB) #9523, 3530 KB), so this PR now carries only the t entry's re-base; the App line is main's.
  • Rebased a third time onto main @ 20df2d7c3: main also re-measured t (fix(ci): re-measure the drifted t-chunk bundle ceiling #9529, 815 KB), so check-bundle-size.mjs is now off this PR's diff entirely. Main's fix(members): commit the roster display order per membership, not per refetch #9537 commits the roster display order per membership (a refetch never re-sorts rows under the cursor); this PR keeps that and adds the chosen sort to the commit key — orderedMembers re-sorts only when membership or the sort changes, and the filters narrow that committed order through a new order-preserving narrowRoster (pinned in rosterFilter.test.ts) instead of re-sorting.
  • UX Watch taken: a persisted filter had no at-rest marker once the menu closed. The roster now mounts the sidebar's chip row under the search field — ONE aggregate chip naming every active filter with its count, clearing them all in one click (round 7's GPT lane blocked the first cut, one pill per filter, on AUTOSDE max-two-buttons-per-row: up to six sibling buttons). The pill is a new shared recipe, FilterChip + FILTER_CHIP_ROW_CLS in components/SearchFilterBar.tsx; the sidebar's inline pill became that component (identical classes and colour recipe), so both lists draw the chip from one place. searchFilterBar.parity.test.tsx pins both pages to it and pins the pill's contract; MembersPage.filters.test.tsx covers show / clear / not-a-chip-for-search. Screenshots above.
  • UX evidence gap (lit unread badge): fourth round on the same item; the disposition stands and this is left to the human reviewer.
  • First Principles items 7–8 (status filters, sort): fourth round on the same item; the disposition stands (the dimensions are the user's relayed request).

Review round 7 (on ae5e3e5c1)

  • GPT BLOCK (max-two-buttons-per-row): the chip row could render up to six sibling buttons. Collapsed to one aggregate FilterChip naming every active filter ("Starred (2), Mine (6)") whose click clears them all — the same shape as the sidebar's tag filter chip, for the same rule. Test re-pinned: exactly one button in the row.
  • UX Watch: two "after" captures pre-dated the chip row — recaptured on this head, chip present. UX evidence gap: the four grip hover/active crops were unreadable — replaced by rest / hover / active strips per panel (light + dark), captured from the live pod. Both filter states and the grip strips come from one fixture (14 members, 2 starred, 6 mine).
  • Design Watch (splitters that still draw the old flat strip in Spec Builder and Code Review Sage): accepted and deferred to Mount the shared ResizeHandle on Spec Builder's and Code Review Sage's column splitters #9580 (assigned, due 2026-09-22); ResizeHandle's docstring now states the claim covers the surfaces that mount it. Design suggestion (split the filter/sort additions into their own feat commit): the repo's PR Hygiene gate requires one commit per PR.
  • First Principles: filterRoster had no consumer outside its own test — deleted; the test composes sortRoster + narrowRoster the way the page does. The false "re-exported from MembersPage" sentence above is corrected. Items 8–10 (status filters, sort, chips/badge): fifth head with the same item — see the disposition; this is the maintainer's call.
  • UX "Scheduled check-ins" label: fourth round of label churn on that row (Patrolling → tooltip → Scheduled check-ins → tooltip again); the current word was the UX lane's own round-2 suggestion, so the label stays and the maintainer decides.
  • Also rebased onto main @ c190fbe5a (+6, no conflicts).

Review round 8 (on 55f5c9b72)

  • GPT, Opus and Design Review all PASS. 63 of 65 checks green; the one red, Backend Tests (Windows) (4), is test/test_work_ledger.py:1403 failing on a Windows sharing violation / "Event loop is closed" — main's own run at a1db7b25f fails the same shard, so it is inherited, not re-run. Rebased onto main @ f036ab246 for a fresh round instead.
  • UX and First Principles repeat the three items already put to the maintainer (the "Scheduled check-ins" word, the unlit unread badge, the status/sort dimensions); each has a pointer disposition on this head. UX's new angle on the badge (a count on a filter icon can read as "N active filters") is answered: the badge is the sidebar's own on the shared trigger, and the aggregate chip beneath it names the active filters with their counts.

Review round 9 (on c171717aa)

  • GPT, Opus and Design PASS. The one red, E2E (stub ACP backend, offline), is the i18n-render gate's [vs-base] compare reporting 26 new findings on the app-detail surface (AppDetailPage.tsx, fixture description text) — a surface this PR does not touch, on a diff that passed the same gate on the previous head; the surface settles on a 400 ms timer, so base and head sampled different render states. Re-ran that one job.
  • UX (new, taken): one pill shape had two behaviours — the sidebar's per-filter pills clear one filter, the roster's chip cleared all. FilterChip now has an aggregate variant with the sidebar tag filter's neutral chrome, and the roster chip uses it, so the shapes say what the click does: coloured = this filter, neutral = all of them. Screenshots of the chip and the applied-filter states recaptured.
  • UX / First Principles otherwise repeat the maintainer-decision items (menu words, unlit badge, status/sort dimensions).
  • Rebased onto main @ ab3a5ac8d (+4, no conflicts).

Review round 10 (on c38a5a6a6)

  • GPT, Opus and Design PASS; E2E green (the previous round's app-detail [vs-base] red was the settle-timer flake it looked like). The three reds are one family: Backend Tests (3.12, 4) hung at 99 % until the 40-minute job timeout, Backend Tests (Windows) (4) failed test/test_work_ledger.py:1403 on a Windows sharing violation, and Coverage Gate cascaded from the cancelled shard. Frontend-only diff; main's own latest run passes both shards. Not re-run — rebased onto main @ d4e592979 for a fresh round.
  • UX evidence gap taken: a menu shot with status rows checked (lit glyphs + checks) is added above. The remaining UX / First Principles items are the ones already put to the maintainer.

Review round 11 (on aa7cb83b0)

  • GPT, Opus and Design PASS; every backend shard green this time (the previous rounds' test_work_ledger hang and Windows sharing violation did not recur). The one red is again the E2E i18n-render [vs-base] compare on app-detail — identical numbers to round 9 (44 → 70, +26) on a surface this PR does not touch, green on the head in between; the settle-timer race in the harness, not a defect in this diff.
  • UX (taken): the aggregate chip's clear-all meaning lived only in its tooltip/aria — its visible text is now that same sentence ("Clear Starred (2), Mine (6) filter"), through the existing clear_named_filter key with one {{var}}, no new strings. UX evidence gap (taken): a menu shot with an ORIGIN row checked ("Mine ✓") added above, from the same fixture.
  • Remaining UX / First Principles items are the maintainer-decision set. Rebased onto main @ 705541178.

Local gates

tsc -b clean, eslint on every touched file clean, i18n-check / check-i18n-keys / check-i18n-strings OK. Vitest deliberately left to CI per this session's rules; the parity assertions were dry-checked against the sources with a node one-liner.

@CrysisDeu
CrysisDeu requested a review from a team September 8, 2026 09:05
@CrysisDeu
CrysisDeu requested a review from a team as a code owner September 8, 2026 09:05
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

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

The refactor is faithful and well-shaped: sidebar classes moved (not re-typed) into listShell.ts, parity tests forbid re-inlined literals, ResizeHandle absorbed the oldest inlined grip, and docs (theming-contract.md, feature-map, index.css comment) updated in the same commit. The filter/sort menu addition is user-requested and dispositioned across eleven prior review rounds; the filter model is cleanly split as pure functions decoupled from the data layer. Remaining open items are UX-lane maintainer decisions, not design defects.

Design-Verdict: PASS

Root-cause fix for value-drift — shared recipes pinned by literal-forbidding parity tests — with kept differences named and the extraction absorbing the oldest copy.

[DESIGN-REVIEWED] 810ea8a

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of 810ea8ad8e9eb3c6d5330dfb4404e16787845f2a — 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.

I have what I need. Permission to fetch issue #9416 was declined, so I'll note the unverifiable provenance where it matters and finish the review with what the diff and repo establish. Counts I ran: temp-screenshots/ is an established convention (1,499 files, own README — not a rider); ResizeHandle has 6 pre-existing page mounts that keep the default props (grep <ResizeHandle, issue-radar ×4, ProjectsPage, MembersPage), so inset/className with 2 consumers each is not premature generalization; the shared recipes each have 2 page consumers except ROW_META_CLS (1, but it travels with its pinned type-scale trio).

First-Principles-Verdict: CONCERNS

Live-state filters and sort ride the drift fix on symmetry alone — "the roster's counterpart to the sidebar's session filters" — no quoted report asks for them.

Not justified as shipped

    1. Roster live-state filters — rides along, inherited: sole stated support is the sidebar's twin; the three insider reports cover grip, background, and integrating the existing filters, and "the audit found the rest" of the drifts — these are not a drift. Ships 2 persisted keys + 9 i18n keys ×13 locales.
    1. Roster sort — rides along, inherited: same symmetry provenance; nobody named who needed A→Z on the roster.
    1. Sidebar keyboard resize — rides along: derived, though — base ResizeHandle's own contract ("focusable, arrow-key operable") already called the inline pointer-only handle a gap.
    1. Unread badge on the roster's filter button — rides along: symmetry-only, harm-free.
    1. Aggregate clear-all chip — rides along: shape mandated by AUTOSDE max-two-buttons; existence is sidebar symmetry.
    1. Roster→thread gutter 22px→8px — rides along: mechanical consequence of the grip moving onto the card border.

What this change ships

Inventory (10 items, capped — SessionFlyout title, doc rows, i18n omitted) — 4 justified

Intent: make the Crew Members roster and the Sessions sidebar look and behave as one surface (reported drift) — a FIX.

  1. Roster card background, header, title, and row type scale now match the sidebar — justified
  2. Roster resize grip moved onto the card's right border with the sidebar's accent pill — justified
  3. Sessions sidebar grip is now keyboard-resizable (tab + arrows) — rides along
  4. Roster search field gains a clear button — justified
  5. Star/origin filter chips moved into a menu docked in the search field — justified (quoted user request)
  6. New roster filters: In progress / Needs you / Unread / Scheduled check-ins, persisted — rides along, inherited
  7. New roster sort: Recent activity / A→Z, persisted — rides along, inherited
  8. Roster filter button shows an unread-count badge — rides along
  9. One aggregate chip clears all active roster filters — rides along
  10. Roster→thread gap narrows from 22px to 8px — rides along

Watch

Items 6–7 rest on the symmetry form of an inherited requirement: the module comment justifies them as "the roster's counterpart to the sidebar's session filters", and the description names no report or failing user for either — while they add two persisted storage keys (mc-members-status, mc-members-sort), a 152-line filter model, and 9 locale keys ×13 languages. The fix (items 1–5) removes every reported and audited drift without them.
Clears when: the author confirms #9416 (which I could not fetch) actually requests live-state filtering/sorting, or names the independent harm — e.g. a counted roster where unread/needs-you members were unfindable.

Subtractions

  • Defer items 6–7: drop STATUS_FILTERS/SORT_OPTIONS from rosterFilter.ts, the two storage keys, and the 9 filter_status_*/sort_* i18n keys to their own PR — the menu with star + origin already delivers the requested "把 filter 集成进去" alignment.

[FIRST-PRINCIPLES-REVIEWED] 810ea8a

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — 🟡 CONCERNS

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

UX-Verdict: CONCERNS

Blind reader navigated every primary control correctly; the only friction is two filter labels whose meaning lives solely in hover tooltips, and one unshown badge state.

Watch

  • "Scheduled check-ins" (new filter_status_patrolling) and "From packages" read as unknowns to the blind reader — "no idea, honestly" / "I don't know what a package is here" — and their explanations (filter_status_patrolling_description, filter_source_package_description) are title tooltips, which never fire on touch and are invisible in a menu a keyboard user arrows through. Low frequency (secondary filters, correct guesses), but persistent for every first-time menu open. Smallest fix: render the description as a muted second line on those two rows, or fold the meaning into the label ("Runs on a schedule").

Evidence gaps

  • The roster filter trigger's unread-count badge (FilterMenuButton badge={filterCounts.status.unread}) appears in no screenshot — every capture has 0 unread, so the badge > 0 state on the new roster mount was never shown. One screenshot with an unread member would close it.
  • Checked states for "Needs your approval or answer" and "Scheduled check-ins" rows appear in no screenshot (only Starred, In progress, Unread checked states were captured).

[UX-REVIEWED] 810ea8a

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 810ea8ad8e9eb3c6d5330dfb4404e16787845f2a and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 810ea8a

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

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 810ea8ad8e9eb3c6d5330dfb4404e16787845f2a — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 810ea8a

Verdict parsed from the review's SHA-scoped output markers for commit 810ea8ad8e9eb3c6d5330dfb4404e16787845f2a.

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

@CrysisDeu
CrysisDeu force-pushed the fix/shared-drag-handle branch from e6a38f8 to 06be820 Compare September 8, 2026 09:29
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 8, 2026
@CrysisDeu CrysisDeu changed the title fix(dashboard): one column-resize grip for sidebar and members roster fix(dashboard): one list-shell recipe for sessions sidebar and members roster Sep 8, 2026
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 8, 2026
@CrysisDeu
CrysisDeu force-pushed the fix/shared-drag-handle branch from 06be820 to a3c8f53 Compare September 8, 2026 09:54
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 8, 2026
@CrysisDeu
CrysisDeu force-pushed the fix/shared-drag-handle branch from a3c8f53 to aace964 Compare September 8, 2026 10:00
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 8, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • The lit unread-count badge on FilterMenuButton appears in no screenshot

Disposition: needs-a-decision — sixth head, same answer; ruling stands as recorded in #issuecomment-5593381060.

Shared sidebar markup pinned by searchFilterBar.parity.test.tsx; lighting it needs a live agent turn the capture pod does not run. The maintainer accepts or overrides.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Items 5–7 (live-state filters, sort, badge) ride along on symmetry — defer them

Disposition: needs-a-decision — sixth head, same answer; ruling stands as recorded in #issuecomment-5593383467.

The dimensions are the user's relayed request ("状态(running / idle / 等你 needs_you / 有 active 巡逻)、crew 归属、排序(最近活动 / 名称)"), not the sidebar arguing for itself. This ruling covers every re-statement of the item, however numbered; the maintainer rules.

@CrysisDeu
CrysisDeu force-pushed the fix/shared-drag-handle branch from 55f5c9b to c171717 Compare September 9, 2026 00:26
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 9, 2026
@CrysisDeu
CrysisDeu force-pushed the fix/shared-drag-handle branch from c171717 to c38a5a6 Compare September 9, 2026 01:21
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: yes
mechanism: aggregate variant on the shared FilterChip (neutral clear-all chrome, the sidebar tag chip's)

  • Same pill, two behaviors: the sidebar's chips clear one filter each, the roster's single chip clears everything

Disposition: fixed in c38a5a6 — the roster chip now wears the sidebar's neutral clear-all shape.

The sidebar already has this vocabulary: a coloured pill clears the one filter it names, the neutral grey tag chip clears several at once. FilterChip gained an aggregate variant with that neutral chrome (no filter colour, border-border, text-muted), and the roster's clear-all chip uses it, so on both lists the shape says what the click does. Giving the sidebar an aggregate chip instead would have taken away its per-filter removal, which is the behaviour the cold reader expected from a coloured pill. Pinned in searchFilterBar.parity.test.tsx; screenshots recaptured.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Menu vocabulary: "Scheduled check-ins" and "From packages" are defined only in tooltips

Disposition: needs-a-decision — ruling stands as recorded in #issuecomment-5593837261; the maintainer names the words.

"Scheduled check-ins" is on its fifth round of label churn, each earlier word this lane's own suggestion; "From packages" is the pre-existing origin label this PR moved from a chip into the menu unchanged, with its tooltip. Both are one-key changes once the maintainer picks the wording.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • The lit unread-count badge on the roster's filter trigger appears in no committed screenshot

Disposition: needs-a-decision — seventh head, same answer; ruling stands as recorded in #issuecomment-5593381060.

Shared sidebar markup pinned by searchFilterBar.parity.test.tsx; lighting it needs a live agent turn the capture pod does not run. The maintainer accepts or overrides.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Items 7–8 (live-state filters, sort) are symmetry-justified riders — defer them

Disposition: needs-a-decision — seventh head, same answer; ruling stands as recorded in #issuecomment-5593383467.

The dimensions are the user's relayed request ("状态(running / idle / 等你 needs_you / 有 active 巡逻)、crew 归属、排序(最近活动 / 名称)"). This ruling covers every re-statement of the item, however numbered; the maintainer rules.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • No screenshot shows a status-filter menu row checked with its lit icon — one menu shot with e.g. Unread active would close it

Disposition: fixed in aa7cb83.

Added light + dark menu shots with "In progress" and "Unread" checked: the Zap fills in its warn colour, the Unread dot fills accent, both rows carry their check and keep their count. Captured from the same pod fixture as every other shot.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • "Scheduled check-ins" is opaque to a cold reader; its explanation lives only in a tooltip

Disposition: needs-a-decision — ruling stands as recorded in #issuecomment-5593837261; the maintainer names the word.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • The purple count bubble on the filter icon reads as "filters are active" rather than "N unread members"

Disposition: rebutted — ruling stands as recorded in #issuecomment-5593837437.

The badge is the sidebar's own unread bubble on the shared trigger; on both lists the neutral chip beneath the trigger now names the active filters explicitly, so "filters are active" has its own reading and the badge keeps the sidebar's.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • The roster trigger's unread-count badge (>0) appears in no screenshot

Disposition: needs-a-decision — eighth head, same answer; ruling stands as recorded in #issuecomment-5593381060.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Items 7–8 (live-state filters, sort) rest on symmetry — defer them

Disposition: needs-a-decision — eighth head, same answer; ruling stands as recorded in #issuecomment-5593383467.

The dimensions are the user's relayed request; this ruling covers every re-statement of the item, however numbered. The maintainer rules.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: yes

  • Aggregate chip scope is ambiguous: one ✕ on "Starred (2), Mine (6)" — put the outcome in the visible label

Disposition: fixed in 87fdc83.

The chip's visible text is now the click's outcome — "Clear Starred (2), Mine (6) filter" — the same sentence its accessible name already carried, so the reader no longer has to infer whether the ✕ drops one filter or all. It reuses the existing pages.chatSidebar.clear_named_filter key with one {{filter}} variable (13 locales already translated); no new strings, no glued fragments. MembersPage.filters.test.tsx pins the visible sentence. Third round on this chip: further wording changes go to the maintainer.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • No screenshot shows the filter menu with an ORIGIN row checked (e.g. "Mine ✓")

Disposition: fixed in 87fdc83.

Light + dark menu shots with Starred and "Mine" checked (tick and count on the row) are added, from the same pod fixture; the chip below them names exactly those two filters.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • "Scheduled check-ins" / "From packages" are hover-only definitions; consider "Runs on a schedule" as the row label

Disposition: needs-a-decision — ruling stands as recorded in #issuecomment-5593837261; the maintainer names the words.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • The roster trigger's unread-count badge is never shown non-zero

Disposition: needs-a-decision — ninth head, same answer; ruling stands as recorded in #issuecomment-5593381060.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Items 4–5 (live-state filters, sort) ship permanent surface for an unrequested capability — drop them

Disposition: needs-a-decision — ninth head, same answer; ruling stands as recorded in #issuecomment-5593383467.

The dimensions are the user's relayed request; this ruling covers every re-statement, however numbered. The maintainer rules.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Trigger badge means "unread members" but sits on a filter icon

Disposition: rebutted — ruling stands as recorded in #issuecomment-5593837437.

The badge is the sidebar's own unread bubble on the shared trigger; the neutral chip under it now states "Clear … filter" in full, so "filters are on" has its own explicit reading next to the badge.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Menu vocabulary fails the cold read: "From packages" and "Scheduled check-ins" carry their meaning only in tooltips

Disposition: needs-a-decision — ruling stands as recorded in #issuecomment-5593837261; the maintainer names the words (one key each, 13 locales).

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • The roster trigger's unread badge lit (unread > 0) appears in no committed screenshot

Disposition: needs-a-decision — tenth head, same answer; ruling stands as recorded in #issuecomment-5593381060.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

self-added: no

  • Items 5–7 (live-state filters, sort, unread badge) ride along on symmetry — delete the status and sort menu sections

Disposition: needs-a-decision — tenth head, same answer; ruling stands as recorded in #issuecomment-5593383467.

The dimensions are the user's relayed request ("状态(running / idle / 等你 needs_you / 有 active 巡逻)、crew 归属、排序(最近活动 / 名称)"); this ruling covers every re-statement, however numbered. The maintainer rules.

chenmingwei23
chenmingwei23 previously approved these changes Sep 9, 2026
…mbers roster

The Crew Members roster is the Sessions sidebar's twin, built from copied
values that drifted: in kiro-light its card stayed white (#fff) while the
sessions card stepped back to --panel (#fafafa), its header sat on its own
padding (16/16/4 vs an 8px 40px line), its row titles were 500 not 600 with
unset line-heights, its column-resize grip was a flat 6px strip where the
sidebar draws a 2px accent pill, and its search field carried a chip row
where the sidebar docks a sort/filter menu in the field (#9416).

components/listShell.ts now carries the sidebar's recipes — card (with the
`sidebar` theme-pack hook and the `sidebar-inner` kiro-light hook), header
line, title, list body, row box and states, row type scale — and both pages
consume them; SessionFlyout's title copy joins in. ResizeHandle renders the
sidebar's pill (2px rounded bar, accent on hover/focus, accent-hover while
dragging, `inset` for the card radius, `className` for absolute placement)
and keeps its window-splitter keyboard contract; the sidebar mounts it and
gains arrow-key resize, the roster mounts it on the card border with the same
12px inset. components/SearchFilterBar.tsx is the sidebar's search row
(field, clear button, trailing 24px filter trigger with badge, the padding
table that keeps text clear of it); the sidebar mounts it and the roster
replaces its SearchInput + chip row with it, the menu holding star, live
state (working / needs you / unread / patrolling), origin and sort. The
roster filter model is pure (pages/members/rosterFilter.ts) so the page's
state layer can move without touching how a filter decides. New labels in
all locales.

Parity tests pin both pages to the shared recipes and components and forbid
the literals; the kiro-light hook test follows the hook into listShell and
requires both cards to mount it; rosterFilter has its own unit pins.

Closes #9416
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

readiness: passed Eligible automated validation passed for the current revision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[insider] Crew Members roster diverges from the sessions sidebar (drag handle, background, row chrome, search + filter bar)

2 participants