diff --git a/packages/app/src/components/sidebar-workspace-list.test.tsx b/packages/app/src/components/sidebar-workspace-list.test.tsx index 0e1d7d28ab..fc57dc2dce 100644 --- a/packages/app/src/components/sidebar-workspace-list.test.tsx +++ b/packages/app/src/components/sidebar-workspace-list.test.tsx @@ -2,6 +2,7 @@ * @vitest-environment jsdom */ import { act, renderHook } from "@testing-library/react"; +import { createWorkspaceVisibilityPolicy } from "@/components/sidebar/sidebar-group-visibility"; import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import type { WorkspaceScriptPayload } from "@getpaseo/protocol/messages"; @@ -486,6 +487,20 @@ describe("sidebar group visibility", () => { }, ); + it("treats placements without a hydrated entry as Done", () => { + const items = entries([...Array(8).fill("done"), "running", "done"]); + const hydrated = new Map(items.slice(0, 9).map((item) => [item.workspaceKey, item])); + for (const entriesByKey of [new Map(), hydrated]) { + const { result } = renderHook(() => + useLimitedSidebarGroup(items, createWorkspaceVisibilityPolicy(entriesByKey)), + ); + expect(result.current.canToggle).toBe(true); + expect(names(result.current.visibleItems)).toEqual( + entriesByKey.size === 0 ? names(items.slice(0, 7)) : [...names(items.slice(0, 7)), "row-9"], + ); + } + }); + it("shows eight active rows once with no redundant toggle", () => { const items = entries([ "running", diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index c3048e6d9a..16294e0a03 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -99,6 +99,7 @@ import { import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction"; import { SidebarSectionHeader } from "@/components/sidebar/sidebar-section-header"; import { SidebarGroupToggleRow } from "@/components/sidebar/sidebar-group-toggle-row"; +import { createWorkspaceVisibilityPolicy } from "@/components/sidebar/sidebar-group-visibility"; import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group"; import { SidebarWorkspaceRowFrame, @@ -1593,11 +1594,7 @@ function ProjectBlock({ onToggleWorkspacePin: ToggleSidebarWorkspacePin; }) { const visibilityPolicy = useMemo( - () => ({ - kind: "workspaces" as const, - getStatusBucket: (workspace: SidebarWorkspacePlacement) => - workspaceEntriesByKey.get(workspace.workspaceKey)!.statusBucket, - }), + () => createWorkspaceVisibilityPolicy(workspaceEntriesByKey), [workspaceEntriesByKey], ); const { diff --git a/packages/app/src/components/sidebar/sidebar-group-visibility.ts b/packages/app/src/components/sidebar/sidebar-group-visibility.ts index ce9f47671c..1bc3018e9a 100644 --- a/packages/app/src/components/sidebar/sidebar-group-visibility.ts +++ b/packages/app/src/components/sidebar/sidebar-group-visibility.ts @@ -4,6 +4,18 @@ export type SidebarGroupVisibilityPolicy = | { kind: "pinned" } | { kind: "workspaces"; getStatusBucket: (item: T) => SidebarStateBucket }; +// A placement can outrun its entry: a retained sidebar that is inactive (compact layout with the +// sidebar closed) or still waiting for sessions keeps an empty entry map. A missing entry renders +// no row, so it counts as Done and claims no slot past the limit. +export function createWorkspaceVisibilityPolicy( + entriesByKey: ReadonlyMap, +): SidebarGroupVisibilityPolicy { + return { + kind: "workspaces", + getStatusBucket: (item) => entriesByKey.get(item.workspaceKey)?.statusBucket ?? "done", + }; +} + export function selectSidebarGroupVisibility( items: readonly T[], expanded: boolean,