Files
paseo/packages/app/src/sidebar-sessions/controller.ts
T
Mohamed Boudra a3c3d25592 sidebar history: group by project toggle with show more/less
Adds a group-by-project toggle to the Sessions header. When ON, the
flat session list becomes a grouped list — one section per project,
preview-capped at 6 agents per project with "Show N more" / "Show less"
controls per group. When OFF, the flat list stays as before.

Architecture preserves the Phase 7b perf model:

- Controller (useSidebarSessionsController) owns groupByProject and
  expandedProjects state. Expansion resets on grouping-off, host change,
  and filter change. Group-by persists across hosts as a UI preference.

- A new useOrderedAgentProjectShape selector reactively subscribes to
  per-agent cwd via useStoreWithEqualityFn(..., shallow). When project
  assignments don't change, the array reference stays stable; when an
  agent's project changes, the parent re-derives groups. Shape objects
  are reused across renders when project fields are unchanged.

- deriveGroupedSidebarSessions takes the resolved shape directly (no
  callback indirection) and returns ordered groups with visible/hidden
  ids based on per-project expansion state.

- Header and Footer components are React.memo'd with primitive props
  only, so they don't re-render on irrelevant agent updates. Per-row
  status indicators continue to subscribe via the shared tab descriptor
  path, isolated from parent renders.

- Project ordering: first-occurrence in the createdAt-desc id list,
  matching the user's mental model of "most-recent project first" and
  avoiding a per-render activity-timestamp computation.

Visual primitive shared between the workspace tree project row and the
new Sessions group header (packages/app/src/components/sidebar/
sidebar-project-row-visual.tsx) — no two systems for project visuals.
The workspace tree wraps it with its own chevron/status; the Sessions
header uses it bare.

Tests added: state-machine cases on the controller, pure-helper coverage
on grouping derivation, render-boundary stability for grouped data
references across irrelevant updates and reactivity on project changes,
and a footer click test.
2026-05-08 20:14:38 +07:00

57 lines
1.9 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
import type { SidebarSessionFilter, SidebarSessionViewMode } from "./types";
export function useSidebarSessionsController({ serverId }: { serverId: string | null }) {
const previousServerIdRef = useRef(serverId);
const [sidebarViewMode, setSidebarViewMode] = useState<SidebarSessionViewMode>("workspaces");
const [sidebarSessionFilter, setSidebarSessionFilter] = useState<SidebarSessionFilter>({
type: "all",
});
const [groupByProject, setGroupByProjectState] = useState(false);
const [expandedProjects, setExpandedProjects] = useState<ReadonlySet<string>>(() => new Set());
const setGroupByProject = useCallback((next: boolean) => {
setGroupByProjectState(next);
if (!next) {
setExpandedProjects((previous) => (previous.size === 0 ? previous : new Set()));
}
}, []);
const setSidebarSessionFilterWithReset = useCallback((next: SidebarSessionFilter) => {
setSidebarSessionFilter(next);
setExpandedProjects((previous) => (previous.size === 0 ? previous : new Set()));
}, []);
const toggleProjectExpanded = useCallback((projectKey: string) => {
setExpandedProjects((previous) => {
const next = new Set(previous);
if (next.has(projectKey)) {
next.delete(projectKey);
} else {
next.add(projectKey);
}
return next;
});
}, []);
useEffect(() => {
if (previousServerIdRef.current === serverId) {
return;
}
previousServerIdRef.current = serverId;
setSidebarSessionFilter({ type: "all" });
setExpandedProjects((previous) => (previous.size === 0 ? previous : new Set()));
}, [serverId]);
return {
sidebarViewMode,
setSidebarViewMode,
sidebarSessionFilter,
setSidebarSessionFilter: setSidebarSessionFilterWithReset,
groupByProject,
expandedProjects,
setGroupByProject,
toggleProjectExpanded,
};
}