From 0d7643b65beb3d793bdecba5529cc13640dbf977 Mon Sep 17 00:00:00 2001 From: bk-agent-01 Date: Tue, 13 Oct 2026 12:31:17 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(web):=20Running=20shelf=20and=20"askin?= =?UTF-8?q?g=20=C2=B7=20unread=20=C2=B7=20running"=20words=20for=20custom?= =?UTF-8?q?=20groups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In custom grouping, a small "Running" switch beside the group-by caption moves sessions that run with nothing waiting on a human out of their groups into a Running shelf, docked above Snoozed. The shelf is one flat list, newest activity first, and each row keeps a pill naming its group. A session with a question, approval or plan anywhere in its subtree stays in its group with its highlight. Every custom group header now says in words what it holds, open or closed: "1 asking · 2 unread · 3 running". The running count includes the sessions the shelf took, so the header still tells you which group they belong to. The switch is a per-device preference (off by default) in the grouping store; mobile ignores it. Co-Authored-By: Claude Opus 5.5 --- .../src/components/PhaseGroupedSidebar.tsx | 165 ++++++++++++++---- .../PhaseSidebarRunningShelfSwitch.tsx | 32 ++++ .../PhaseSidebarShelfDock.expbkt3.test.tsx | 39 ++++- .../sidebar/PhaseSidebarShelfDock.tsx | 137 +++++++++------ apps/web/src/phaseSidebarGroupingStore.ts | 5 + .../src/state/phaseSidebarGrouping.test.ts | 71 ++++++++ .../src/state/phaseSidebarGrouping.ts | 80 ++++++++- 7 files changed, 439 insertions(+), 90 deletions(-) create mode 100644 apps/web/src/components/sidebar/PhaseSidebarRunningShelfSwitch.tsx diff --git a/apps/web/src/components/PhaseGroupedSidebar.tsx b/apps/web/src/components/PhaseGroupedSidebar.tsx index b5a5c8bb0440d..a5277be9e29b7 100644 --- a/apps/web/src/components/PhaseGroupedSidebar.tsx +++ b/apps/web/src/components/PhaseGroupedSidebar.tsx @@ -244,6 +244,7 @@ import { buildPhaseSidebarSections, isPhaseSidebarSectionCollapsed, listPhaseSidebarCustomGroups, + PHASE_SIDEBAR_RUNNING_SHELF_SECTION, PHASE_SIDEBAR_SHELF_SECTIONS, PHASE_SIDEBAR_UNGROUPED_ID, phaseSidebarCustomGroupIdForRow, @@ -256,6 +257,7 @@ import { } from "@t3tools/client-runtime/state/phase-sidebar-grouping"; import { usePhaseSidebarGroupingStore } from "../phaseSidebarGroupingStore"; import { PhaseSidebarGroupByPopover } from "./sidebar/PhaseSidebarGroupByPopover"; +import { PhaseSidebarRunningShelfSwitch } from "./sidebar/PhaseSidebarRunningShelfSwitch"; import { PhaseSidebarGroupNameDialog } from "./sidebar/PhaseSidebarGroupNameDialog"; import { PHASE_SIDEBAR_SECTION_ACTION_BUTTON_CLASS_NAME, @@ -2888,7 +2890,7 @@ export function PhaseGroupedSidebar() { null, [environments], ); - const { sections, forcedExpansionKeys } = useMemo( + const { sections, forcedExpansionKeys, runningNodes } = useMemo( () => buildPhaseSidebarSections({ rows: activeRows, @@ -3114,12 +3116,42 @@ export function PhaseGroupedSidebar() { }), [routeThreadKey, settledRows, settledShelfCollapsed, settledVisibleCount], ); + // The Running shelf (custom mode, switched on) holds live session trees, so + // an open shelf flattens them like any section for traversal. + const runningShelfCollapsed = isPhaseSidebarSectionCollapsed( + PHASE_SIDEBAR_RUNNING_SHELF_SECTION, + collapsedSectionKeys, + ); + const toggleRunningShelf = useCallback( + () => toggleSectionCollapsed(PHASE_SIDEBAR_RUNNING_SHELF_SECTION.key), + [toggleSectionCollapsed], + ); + const runningShelfRows = useMemo( + () => + runningShelfCollapsed + ? [] + : flattenPhaseSidebarTree(runningNodes, isTreeKeyExpanded).map((node) => node.row), + [isTreeKeyExpanded, runningNodes, runningShelfCollapsed], + ); + const runningShelfHoldsRoutedThread = useMemo( + () => + routeThreadKey !== null && + flattenPhaseSidebarTree(runningNodes, () => true).some( + (node) => phaseSidebarRowKey(node.row) === routeThreadKey, + ), + [routeThreadKey, runningNodes], + ); // T3-CUSTOM(expbkt3): END // Traversal, jump labels and shift-range selection operate on what is // actually on screen, in screen order: the list, then the docked shelves. const visibleRows = useMemo( - () => [...activeVisibleRows, ...snoozedShelf.rendered, ...settledShelf.rendered], - [activeVisibleRows, snoozedShelf.rendered, settledShelf.rendered], + () => [ + ...activeVisibleRows, + ...runningShelfRows, + ...snoozedShelf.rendered, + ...settledShelf.rendered, + ], + [activeVisibleRows, runningShelfRows, snoozedShelf.rendered, settledShelf.rendered], ); // T3-CUSTOM(expbkt3): BEGIN — environment markers are conditional on what is // actually rendered, not on how many environments are configured. With one @@ -4024,6 +4056,8 @@ export function PhaseGroupedSidebar() { // clean while silently leaving every treeXxx prop undefined, which reads as // "the feature is off" rather than as a build error. tree?: PhaseThreadRowTreeProps, + // T3-CUSTOM(expbkt3): Running shelf rows left their group, so they name it. + showGroup = false, ) => { const key = scopedThreadKey(scopeThreadRef(row.thread.environmentId, row.thread.id)); const project = @@ -4078,7 +4112,9 @@ export function PhaseGroupedSidebar() { // T3-CUSTOM(expbkt3): custom groups. {...(groupActions ? { groupActions } : {})} customGroupId={phaseSidebarCustomGroupIdForRow(row)} - customGroupLabel={grouping.groupBy === "custom" ? null : (row.customGroup ?? null)} + customGroupLabel={ + grouping.groupBy === "custom" && !showGroup ? null : (row.customGroup ?? null) + } customGroupColor={customGroupColors.get(phaseSidebarCustomGroupIdForRow(row) ?? "") ?? null} linearIssueStatuses={linearStatusesForRow(row)} {...(tree ?? {})} @@ -4088,22 +4124,27 @@ export function PhaseGroupedSidebar() { // T3-CUSTOM(expbkt3): BEGIN — a session tree renders as nested