feat(members): dock the chat SidePanel beside the DM thread (first tab: Crew summary) - #9437
feat(members): dock the chat SidePanel beside the DM thread (first tab: Crew summary)#9437CrysisDeu wants to merge 1 commit into
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Retires a drifting hand-copy by mounting the real component, with the hard edges (slot binding, withheld views, remount survival) reasoned and pinned by tests. The consolidation direction is right: the panel gap was previously closed by copying chat infrastructure, and the PR's own history (drag handle, steer badge, Quote/Ask) shows that class drifts. The new host surface on [DESIGN-REVIEWED] 925acb8 |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All checks done — the change reuses the existing SidePanel and deletes the hand-built drawer, the screenshots follow the repo's committed-deliverables convention, and every new prop has a real consumer. Final review: First-Principles-Verdict: PASS Verify one undeclared coupling: What this change shipsIntent: give the Crew Members page the same tabbed right-panel model as chat (first tab: Crew summary), per a quoted user request — an ADDITION whose core is a deletion (the hand-built
[FIRST-PRINCIPLES-REVIEWED] 925acb8 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of All evidence is reconciled. The blind reader correctly identified the primary Crew summary chip, the UX-Verdict: CONCERNS Reused chat panel reads coherently, but "Crew summary" summarizes one member — the blind reader flagged it — and the new error notice was never shown. Watch
Evidence gaps
[UX-REVIEWED] 925acb8 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
0282876 to
39bca34
Compare
39bca34 to
5a976ac
Compare
5a976ac to
95cce10
Compare
95cce10 to
b47fd1b
Compare
2f7200c to
7e4cfd1
Compare
7e4cfd1 to
fdeefd3
Compare
fdeefd3 to
574d78c
Compare
574d78c to
3793dbc
Compare
|
Re UX Review on
Design Review's Watch (hand-enumerated unfed set) is applied in the same push: |
…b Crew summary The Members page's right column was a closable DetailPanel drawer — a hand-built lookalike of the chat page's right column. It is now the chat page's own tabbed SidePanel, permanent on wide windows (no close control, no header toggle), whose first tab is the member's Crew summary carrying what the drawer showed (identity + status, counters, driving sessions, auto patrol, recent activity, wake sources, configuration, memory note, edit exits). The + menu is the chat panel's (Files / Artifacts / Terminal / Browser / Side chat …) against the member's DM slot; the strip is bucketed per member so it follows the roster selection. SidePanel gains `leadingTab` (a host-owned, non-closable, non-draggable tab ahead of the pinned block whose body the host renders), an optional `onClose` (absent ⇒ no close control, Escape inert) and `extraReserveW` (a sibling column's live width kept clear on drag). `usePanelTabs` takes `opts.leadingId` so a fresh strip opens on the leading tab and an emptied strip falls back to it. File / artifact open and file save move out of useChatPageResourcesController into the shared `usePanelDocumentActions` so both hosts run one implementation. Narrow windows (roster + thread + panel cannot seat) keep the panel as an overlay the header button opens. Closes #9432
3793dbc to
925acb8
Compare
Problem / Motivation
The Crew Members page's right column is a closable
DetailPaneldrawer (X in its header, a Details toggle in the thread header, one fixed body). The chat page's right column is the tabbedSidePanel: pinned Changes / Artifacts / Files, a+menu of further views (Summary, Pins, Issues, Links, Subagents, Workflows, Git, Side chat, Browser, per-chat Terminal — #8703), document tabs, reorder, resize, per-slot tab memory. A member DM thread is a chat slot, yet the two surfaces teach two different right-panel models. Zezhen: "这个侧边栏去掉关闭按钮,做成和 session 一样的 tab 形式,第一个 tab 是 crew summary".Why it matters
The Members page is the perpetual-agent roster: it is where a member's patrol (#8936), the sessions it drives (#8591) and its escalations (#8614) surface. Today that surface has none of the tools a chat has beside it — no Files browser, no Terminal in the member's project, no artifacts — and it is a hand-built copy of chat infrastructure, which is the drift class this page keeps producing (drag handle, list background, steer badge, Quote/Ask were each one PR).
What changed (motivation → approach → change)
Approach: reuse
SidePanel, do not build a second one. The only things the panel lacked were a way to host a page-owned first tab and a way to be permanent.pages/chat/SidePanel.tsxleadingTab?: { id, title, icon, render }— a host-owned tab rendered AHEAD of the pinned block as a pinned-style chip: non-closable, not aReorder.Item, never in the+menu, body rendered by the host only while active. Itsidmust not collide with aTabKind('summary'is the chat's session summary — a different thing; this one is'crew-summary', and its icon is the member's avatar, not a kind glyph).onClosebecomes optional. Absent ⇒ permanent: no close control in the strip (the trailing divider goes with it) and the views' Escape does nothing. ChatPage passes it as before; the Members docked column omits it; the Members narrow-window overlay passes it.extraReserveW?: number— a sibling column's live width kept clear on top ofmeasureSidePanelReservedW()(the Members roster), so a drag can never fold the thread to nothing — the contract the drawer'sreserveWidthcarried.TabChipaccepts aniconoverride and a kind-less tab for the leading chip.hooks/usePanelTabs.ts—usePanelTabs(slot, descriptors, { leadingId }): a fresh strip's focus is the leading tab (not the first pinned viewsyncPinnedwould otherwise pick),syncPinnednever "repairs" a leading-tab focus away for not being a stored tab, and closing the last tab /closeAlllands back on it. No change withoutleadingId.hooks/usePanelDocumentActions.ts(new, at the shared hooks layer — it is a two-host module from birth) — file open / artifact open / file save, extracted verbatim fromuseChatPageResourcesController(which now calls it withonOpened= reveal dock + close find pane). One implementation for both hosts, so the 404-vs-error read contract, the artifact involvement breadcrumb and the save-baseline reconcile cannot drift.pages/members/MembersPage.tsxDetailPaneldrawer,drawerOpen,DRAWER_*and themc-members-drawer-widthkey are gone. The panel is the chatSidePanelon the chat page's right-dock motion,canDockBottom={false},slot= the member's DM slot,projectDirfrom the live slot (Files root / Terminal cwd), tab strip bucketed per member (usePanelTabs(memberKey, …, { leadingId: CREW_SUMMARY_TAB_ID })) so switching members swaps the strip and switching back restores it.panelSitsBeside({ winW, rosterW, isMobile })(pure, exported) decides the placement: wide enough (shell reserve + roster + gaps + panel minimum) ⇒ permanent column, no header toggle, no close; narrower ⇒ an overlay the header button opens, sliding in over a full-bleed scrim (bg-bg/60+ blur) that dims the whole chat column — no sliver of thread peeking beside the panel — and dismissed by the panel's own close control or a click on the scrim, the drawer convention. A failed document read while the overlay is open closes the overlay as the notice appears above the thread, so the failure is never hidden behind the panel that caused it. Below md is always overlay.drawerOpen).ErrorNotice(askAgent; nothing there holds a draft).i18n: one new key
pages.membersPage.crew_summaryinen.json+ 12 locales,en-XAregenerated (pure-additive, 13 lines).docs/feature-map/README.md: Crew Members / Monitor loops rows updated.Views this host cannot feed are withheld, not shipped empty (
SidePanel.hiddenViews, applied to the pinned block and the+menu alike;MEMBERS_UNFED_VIEWS): Changes / Issues / Links / Pins read ChatPage-owned transcript indexes this page does not run, so an empty Changes chip on the monitoring page would assert "nothing changed" while a member edits. The chat page's sessionSummaryis withheld too — one click from the "Crew summary" chip it is an indistinguishable sibling label. The withdrawal is a RENDER-time filter over the bucket, never a deletion:syncPinnedstill receives the wholePINNED_VIEWSlist, a withheld pinned view (Changes) simply is not drawn, and tabs already in the slot's bucket (a member DM opened on the chat page can have stored Pins / Issues) are neither rendered nor focused here but stay in storage for the chat page; a stored focus on such a tab falls back to the leading tab in the STRIP only — the store keeps the focus (a withdrawal can be temporary: every slot view is withheld for the moment a re-open POST is in flight, and a stored focus on Files must come back as Files) — and the panel reports what it shows through a newonActiveTabChange, which is what the Members page gates its summary reads on. The unfed set is derived, not enumerated:usePanelTabs.VIEW_DATA_SOURCEclassifies everyViewKindasslotorchat-transcript(exhaustiveRecord, so a new view kind cannot exist unclassified) andMEMBERS_UNFED_VIEWS = [...CHAT_TRANSCRIPT_VIEWS, 'summary']. Body-owning tabs (Browser, Terminal, editors) stay MOUNTED and hidden while withheld — the body loop walks every stored tab and a withheld one is simply never active — so a temporary withdrawal cannot destroy a live WebContentsView. The persisted bucket keeps a leading-tab focus (serializeBucketonly refocuses when the focused tab was itself dropped), so a reload lands on Crew summary when that is what was selected. They return when the extraction moves below ChatPage. Files / Artifacts / Terminal / Browser / Side chat / Subagents / Workflows / Git work against the DM slot.The panel binds only the POST-confirmed slot. The tab bucket and
SidePanel.slotuseactiveSlot(set byPOST /api/members/{slug}/thread), never the roster's derivedslot_key— that key is a stale binding until confirmed, and an ordinary slot can occupy it after a restart. Until the thread is confirmed (or when the POST is refused), every slot-bound view is withheld; only the slot-free Crew summary (and Terminal, a PTY in a directory) is offered.One SidePanel instance across placements. Docked and overlay share one keyed motion element (only the wrapper class and motion axis differ), so a placement flip never remounts the panel; a closed overlay stays mounted-and-hidden while a live Browser / app tab exists (
shouldMountSidePanel/isSidePanelHidden, the chat page's rule).overlayOpenis reset whenever the panel docks, and a document open only reveals the overlay when the panel IS an overlay — so an open made while docked cannot pop the overlay over the thread on a later narrowing.The panel is unbound for the whole in-flight window of a re-open, not just after a refusal.
confirmedSlotis empty from the moment a thread POST is issued until it confirms (a request sequence makes only the NEWEST POST's answer count — an older answer, success or refusal, is dropped whole, so a stale success can never rebind a key the latest refusal unbound; nothing is lost because every open re-POSTs); the thread column keeps its cached key as a render hint, the panel offers only the Crew summary and its document actions carry no slot, so nothing can be recorded against a key the endpoint is about to refuse.A refused re-open unbinds.
activatealways re-POSTs (the endpoint is the idempotent creator/repairer); a refusal now clears the cached slot as well as showing the error, so a key confirmed by an earlier open cannot keep the thread or any panel view aimed at a session that is no longer this member's. Terminal additionally waits for the WSslotsframe that carries the confirmed slot's record (its project is the shell's cwd) — the POST answers before that frame, and a shell spawned in the gap would take the backend's HOME fallback with no re-rooting; a member with no project still gets its shell in the fallback cwd, as a project-less chat does. Until confirmation, Terminal and every app-contributed tab are withheld too (hiddenViewsaccepts'terminal'and the'app'switch) — a PTY or anAppHostopened into the provisional (no-slot) bucket would be orphaned / remounted when the strip re-keys. The feature map's App-contributed panel tabs row now names/membersas a second path.openFilecaptures the initiating slot before awaiting the read, likesaveFilealready did.Width is shared with the chat panel on purpose: the panel persists under the chat panel's
mc-side-panel-width, so resizing it on either page resizes both. It is the same panel; one width is the consistency the request asked for. The oldmc-members-drawer-widthkey is orphaned (a stale localStorage entry, nothing reads it).Tests
Written, not run locally (this session's rule: tsc / eslint / i18n gate only — all green; CI runs vitest).
src/test/sidePanelLeadingTab.test.tsx(new): leading chip renders first, ahead of the pinned block, with the host icon and no close control, and is not in theReordertablist; fresh bucket focuses it (body shows); it is absent from the+menu while Terminal is offered; closing the last dynamic tab refocuses the neighbour,closeAlllands on the leading tab; no close control withoutonClose, one with it; focus is per slot;hiddenViewswithdraws a view from the pinned block and the+menu alike.pages/members/MembersPage.test.tsx: the drawer cases now assert the docked SidePanel (permanent, no Details toggle, no close control, resize splitter present); Crew summary istabs[0],aria-selected, named, without nested buttons,CREW_SUMMARY_TAB_ID !== 'summary'; selecting Artifacts swaps the body and the chip brings the summary back; the+menu lists Terminal and not Crew summary; narrow window ⇒ closed overlay, toggle opens, close control dismisses;panelSitsBesideboundary at 1168px for a 264px roster, mobile never; the withheld set is pinned (MEMBERS_UNFED_VIEWS) and absent from strip + menu; a refused thread POST leaves only Crew summary on the strip and nothing slot-bound in the menu; an overlay opened narrow is closed again after dock → re-narrow.member-drawertest id →member-crew-summary; every existing summary-content case (config, memory disclosure, driving sessions, patrol, activity, wake sources, edit exits) is kept and re-pointed.sidePanelPinnedAlwaysPresent/sidePanelAddMenucontracts are unchanged (noleadingTab⇒ identical strip).Manual verification
Against a live pod (
kirocrew pod up kc-wt-members-sidepanel --seed <dir>with a seed config of four fictional crews; real gateway, real SPA dist, realPOST /api/members/{slug}/thread, no stubbed API): docked panel at 1440×900, Crew summary selected on open, no close / toggle,+menu offers Terminal, member switch swaps the strip and the summary body, Workflows opens from+, chip returns to the summary. Theme flipped through the pod's ownPUT /api/config/theme.Screenshots / video
recorded from 925acb8 · origin/kirodotdev/KiroCrew · mode: pod (
kirocrew pod up, dark/light viaPUT /api/config/theme) · real server + model, no fixturesBefore — closable drawer (X in header, Details toggle in the thread header):
After — the chat SidePanel docked, Crew summary as the permanent first tab, no close control:
+menu on a member DM — the chat panel's views, Terminal included:Switch member → strip and summary follow;
+→ a view opens; chip → back to the summary (6.7s):Narrow window (1000px): closed by default with the header toggle; the overlay carries the panel's own close control:
Docked → overlay → docked across the
panelSitsBesideboundary (window resized 1440 → 1000 → 1440; one keyed element, no remount; 6.9s):Not captured: the
member-panel-action-errornotice. I tried to force it (route-intercept/api/file-read→ 500 and open a file from the Files tab), but a member DM slot in the pod has no project directory, so the Files tab has no rows to open; its render is the sharedErrorNoticealready shown for the star-write failure on this page.Light theme + before/light
Related Issues
Closes #9432. Builds on #8936, #8591, #8614 (summary content), #8703 (Terminal in the
+menu), #8718 (state-transition continuity).Pattern harvest
Not generalizable as a rule — but it is the same class as the week's other Members-page fixes: the Members page was hand-built by copying chat surfaces instead of mounting them, and each copy drifts (drag handle, list background, steer badge, Quote/Ask, and this drawer). This PR retires the largest copy by mounting the real
SidePaneland extracting the document actions into a shared hook. The remaining copy is the left column; the follow-up "Members page: reuse ChatSidebar list infrastructure" (being filed by the React Query migration work) closes the class at its source.Checklist