From 810581a86fdc124cfd30a5bec236597426d69cd7 Mon Sep 17 00:00:00 2001 From: Bersabel Tadesse Date: Wed, 26 Aug 2026 12:34:41 -0700 Subject: [PATCH 1/4] Polish sidebar hierarchy and section actions --- .../src/components/sidebar/ProjectList.tsx | 74 ++++++++++++++---- .../sidebar/ProjectListSectionHeader.test.tsx | 40 ++++++++++ .../sidebar/ProjectRow.interactions.test.tsx | 75 ++++++++++++++++++- .../app/src/components/sidebar/ProjectRow.tsx | 24 +++++- .../sidebar/SidebarChildToggleChevron.tsx | 2 +- .../src/components/sidebar/ThreadRow.test.tsx | 18 ++++- apps/app/src/components/sidebar/ThreadRow.tsx | 2 +- .../sidebar/TopLevelSidebarSection.tsx | 2 +- 8 files changed, 212 insertions(+), 25 deletions(-) diff --git a/apps/app/src/components/sidebar/ProjectList.tsx b/apps/app/src/components/sidebar/ProjectList.tsx index 0458b87996..b5b2c3c8ea 100644 --- a/apps/app/src/components/sidebar/ProjectList.tsx +++ b/apps/app/src/components/sidebar/ProjectList.tsx @@ -229,6 +229,12 @@ interface ProjectListThreadsSectionActionsProps { onNewThread: () => void; } +interface ProjectListManualPinnedSectionActionsProps { + displayOptions: ReactNode; + isCreatingSection: boolean; + onNewSection?: () => void; +} + interface SidebarDisplayOptionsMenuProps { activeCount?: number; draftCount?: number; @@ -590,20 +596,10 @@ function ProjectListThreadsSectionActions({ }: ProjectListThreadsSectionActionsProps) { return ( <> - {onNewSection ? ( - - } - onClick={onNewSection} - /> - ) : null} + ) { + return onNewSection ? ( + + } + onClick={onNewSection} + /> + ) : null; +} + +export function ProjectListManualPinnedSectionActions({ + displayOptions, + isCreatingSection, + onNewSection, +}: ProjectListManualPinnedSectionActionsProps) { + return ( + <> + {displayOptions} + + + ); +} + // "Manually" is the user-facing name for chronological/drag-ordered mode; // the stored atom value stays "chronological". const SIDEBAR_ORGANIZE_OPTIONS = [ @@ -1375,6 +1407,7 @@ interface SectionModeSectionsProps extends BuiltInSectionRenderState { compareThreads: ThreadComparator; sections: readonly SidebarSectionDefinition[]; isReady: boolean; + onCreateSection: () => void; onCreateThreadInSection: (sectionId: string) => void; onProjectSelect?: () => void; onRemoveSection: (section: SidebarSectionDefinition) => void; @@ -1406,6 +1439,7 @@ function SectionModeSections({ effectivePinnedThreadIds, sections, isReady, + onCreateSection, onCreateThreadInSection, onProjectSelect, onRemoveSection, @@ -1455,6 +1489,7 @@ function SectionModeSections({ collapsedThreadIds={collapsedThreadIds} collapsedEnvironmentIds={collapsedEnvironmentIds} onProjectSelect={onProjectSelect} + onCreateSection={onCreateSection} onCreateThreadInSection={onCreateThreadInSection} onRenameSection={onRenameSection} onRemoveSection={onRemoveSection} @@ -2146,7 +2181,15 @@ function ProjectListComponent({ collapsedThreads: pinnedSectionThreads, label: "Pinned", content: pinnedSectionContent, - actions: renderSectionDisplayOptions("pinned"), + actions: ( + + ), actionsOpen: isSectionDisplayOptionsOpen("pinned"), }; const threadsSection = { @@ -2273,6 +2316,7 @@ function ProjectListComponent({ collapsedEnvironmentIds={collapsedEnvironmentIds} compareThreads={sidebarThreadComparator} onProjectSelect={onProjectSelect} + onCreateSection={handleOpenCreateSectionDialog} onCreateThreadInSection={handleCreateThreadInSection} onRenameSection={handleOpenRenameThreadSection} onRemoveSection={handleRemoveThreadSection} diff --git a/apps/app/src/components/sidebar/ProjectListSectionHeader.test.tsx b/apps/app/src/components/sidebar/ProjectListSectionHeader.test.tsx index 60c4707ddf..a75aa508c9 100644 --- a/apps/app/src/components/sidebar/ProjectListSectionHeader.test.tsx +++ b/apps/app/src/components/sidebar/ProjectListSectionHeader.test.tsx @@ -18,6 +18,7 @@ import { setPluginThreadRowStatus, } from "@/lib/plugin-thread-row-status"; import { + ProjectListManualPinnedSectionActions, ProjectListSectionIconButton, TopLevelSidebarSection, } from "./ProjectList"; @@ -76,6 +77,38 @@ describe("ProjectListSectionIconButton", () => { }); }); +describe("ProjectListManualPinnedSectionActions", () => { + it("offers the shared New section action only in Manual view", () => { + const onNewSection = vi.fn(); + const result = render( + + Display options} + isCreatingSection={false} + onNewSection={onNewSection} + /> + , + ); + + expect(screen.getByText("Display options")).not.toBeNull(); + const newSection = screen.getByRole("button", { name: "New section" }); + expect(newSection.querySelector('[data-icon="SectionAdd"]')).not.toBeNull(); + + fireEvent.click(newSection); + expect(onNewSection).toHaveBeenCalledOnce(); + + result.rerender( + + Display options} + isCreatingSection={false} + /> + , + ); + expect(screen.queryByRole("button", { name: "New section" })).toBeNull(); + }); +}); + describe("TopLevelSidebarSection", () => { it("exposes stable identity only for persisted sections", () => { const result = render( @@ -144,6 +177,13 @@ describe("TopLevelSidebarSection", () => { label.compareDocumentPosition(disclosure) & Node.DOCUMENT_POSITION_FOLLOWING, ).not.toBe(0); + expect(disclosure.classList.contains("text-subtle-foreground/75")).toBe( + true, + ); + expect(disclosure.classList.contains("text-subtle-foreground")).toBe(false); + expect( + disclosure.classList.contains("hover:text-sidebar-accent-foreground"), + ).toBe(true); }); it("rolls a hidden split thread up to a collapsed top-level section", () => { diff --git a/apps/app/src/components/sidebar/ProjectRow.interactions.test.tsx b/apps/app/src/components/sidebar/ProjectRow.interactions.test.tsx index 09e37a21c0..6fd4ef8490 100644 --- a/apps/app/src/components/sidebar/ProjectRow.interactions.test.tsx +++ b/apps/app/src/components/sidebar/ProjectRow.interactions.test.tsx @@ -208,7 +208,7 @@ describe("ProjectRow interactions", () => { }); it("shows generic runtime activity before a named workflow rollup", () => { - renderProjectRow( + const result = renderProjectRow( vi.fn(), { status: "ready", @@ -252,6 +252,22 @@ describe("ProjectRow interactions", () => { ).not.toBeNull(); expect(screen.getByLabelText("Thread working")).not.toBeNull(); expect(screen.queryByLabelText("Workflow running")).toBeNull(); + const folderIcon = result.container.querySelector( + '[data-icon="FolderGit"]', + ); + const folderIconContainer = folderIcon?.parentElement; + expect(folderIconContainer).not.toBeNull(); + expect( + folderIconContainer?.classList.contains("text-subtle-foreground/75"), + ).toBe(true); + expect( + folderIconContainer?.classList.contains("text-subtle-foreground"), + ).toBe(false); + expect( + folderIconContainer?.nextElementSibling?.classList.contains( + "text-subtle-foreground/80", + ), + ).toBe(true); }); it("shows a working draft before named work for a collapsed environment", () => { @@ -412,6 +428,63 @@ describe("ProjectRow interactions", () => { expect(screen.queryByLabelText("Plan mode active")).toBeNull(); }); + it("offers New section from a Manual-view section menu", async () => { + const store = createStore(); + const queryClient = new QueryClient(); + const sectionId = "sec_actions"; + const onCreateSection = vi.fn(); + const onRenameSection = vi.fn(); + const onRemoveSection = vi.fn(); + + render( + + + + + 0} + sections={[{ id: sectionId, name: "Actionable" }]} + collapsedThreadIds={new Set()} + collapsedEnvironmentIds={new Set()} + onCreateSection={onCreateSection} + onRenameSection={onRenameSection} + onRemoveSection={onRemoveSection} + onToggleThreadCollapsed={vi.fn()} + onToggleEnvironmentCollapsed={vi.fn()} + topLevelSectionOrder={[ + buildSidebarEntitySectionId("section", sectionId), + ]} + onTopLevelSectionOrderChange={vi.fn()} + pinnedReorderPending={false} + pinnedThreads={[]} + onReorderPinnedThread={vi.fn()} + /> + + + + , + ); + + fireEvent.pointerDown( + screen.getByRole("button", { name: "Actionable section actions" }), + { button: 0 }, + ); + + const newSection = await screen.findByRole("menuitem", { + name: "New section", + }); + expect(newSection.querySelector('[data-icon="SectionAdd"]')).not.toBeNull(); + expect(screen.getByRole("menuitem", { name: "Rename" })).not.toBeNull(); + expect(screen.getByRole("menuitem", { name: "Remove" })).not.toBeNull(); + + fireEvent.click(newSection); + expect(onCreateSection).toHaveBeenCalledOnce(); + }); + it("surfaces named activity when the project is collapsed", () => { renderProjectRow( vi.fn(), diff --git a/apps/app/src/components/sidebar/ProjectRow.tsx b/apps/app/src/components/sidebar/ProjectRow.tsx index c59648f214..e8080ca0a1 100644 --- a/apps/app/src/components/sidebar/ProjectRow.tsx +++ b/apps/app/src/components/sidebar/ProjectRow.tsx @@ -199,6 +199,7 @@ interface SectionThreadTreeProps { collapsedThreadIds: Set; collapsedEnvironmentIds: Set; onProjectSelect?: () => void; + onCreateSection?: () => void; onCreateThreadInSection?: (sectionId: string) => void; onRenameSection?: (section: SidebarSectionDefinition) => void; onRemoveSection?: (section: SidebarSectionDefinition) => void; @@ -300,6 +301,7 @@ interface ThreadTreeItemRowProps { collapsedEnvironmentIds: Set; variant: ProjectThreadTreeVariant; onProjectSelect?: () => void; + onCreateSection?: () => void; onCreateThreadInSection?: (sectionId: string) => void; onRenameSection?: (section: SidebarSectionDefinition) => void; onRemoveSection?: (section: SidebarSectionDefinition) => void; @@ -322,6 +324,7 @@ interface SectionTreeItemRowProps { collapsedEnvironmentIds: Set; variant: ProjectThreadTreeVariant; onProjectSelect?: () => void; + onCreateSection?: () => void; onCreateThreadInSection?: (sectionId: string) => void; onRenameSection?: (section: SidebarSectionDefinition) => void; onRemoveSection?: (section: SidebarSectionDefinition) => void; @@ -960,7 +963,7 @@ function EnvironmentThreadGroupHeader({ )} } - isCreatingSection={false} - onNewSection={onNewSection} - /> - , - ); - - expect(screen.getByText("Display options")).not.toBeNull(); - const newSection = screen.getByRole("button", { name: "New section" }); - expect(newSection.querySelector('[data-icon="SectionAdd"]')).not.toBeNull(); - - fireEvent.click(newSection); - expect(onNewSection).toHaveBeenCalledOnce(); - - result.rerender( - - Display options} - isCreatingSection={false} - /> - , - ); - expect(screen.queryByRole("button", { name: "New section" })).toBeNull(); - }); -}); - describe("TopLevelSidebarSection", () => { it("exposes stable identity only for persisted sections", () => { const result = render( diff --git a/apps/app/src/components/sidebar/ProjectRow.interactions.test.tsx b/apps/app/src/components/sidebar/ProjectRow.interactions.test.tsx index aefc6fbcee..db50319db8 100644 --- a/apps/app/src/components/sidebar/ProjectRow.interactions.test.tsx +++ b/apps/app/src/components/sidebar/ProjectRow.interactions.test.tsx @@ -428,63 +428,6 @@ describe("ProjectRow interactions", () => { expect(screen.queryByLabelText("Plan mode active")).toBeNull(); }); - it("offers New section from a Manual-view section menu", async () => { - const store = createStore(); - const queryClient = new QueryClient(); - const sectionId = "sec_actions"; - const onCreateSection = vi.fn(); - const onRenameSection = vi.fn(); - const onRemoveSection = vi.fn(); - - render( - - - - - 0} - sections={[{ id: sectionId, name: "Actionable" }]} - collapsedThreadIds={new Set()} - collapsedEnvironmentIds={new Set()} - onCreateSection={onCreateSection} - onRenameSection={onRenameSection} - onRemoveSection={onRemoveSection} - onToggleThreadCollapsed={vi.fn()} - onToggleEnvironmentCollapsed={vi.fn()} - topLevelSectionOrder={[ - buildSidebarEntitySectionId("section", sectionId), - ]} - onTopLevelSectionOrderChange={vi.fn()} - pinnedReorderPending={false} - pinnedThreads={[]} - onReorderPinnedThread={vi.fn()} - /> - - - - , - ); - - fireEvent.pointerDown( - screen.getByRole("button", { name: "Actionable section actions" }), - { button: 0 }, - ); - - const newSection = await screen.findByRole("menuitem", { - name: "New section", - }); - expect(newSection.querySelector('[data-icon="SectionAdd"]')).not.toBeNull(); - expect(screen.getByRole("menuitem", { name: "Rename" })).not.toBeNull(); - expect(screen.getByRole("menuitem", { name: "Remove" })).not.toBeNull(); - - fireEvent.click(newSection); - expect(onCreateSection).toHaveBeenCalledOnce(); - }); - it("surfaces named activity when the project is collapsed", () => { renderProjectRow( vi.fn(), diff --git a/apps/app/src/components/sidebar/ProjectRow.tsx b/apps/app/src/components/sidebar/ProjectRow.tsx index c544419b3b..f280c5bca1 100644 --- a/apps/app/src/components/sidebar/ProjectRow.tsx +++ b/apps/app/src/components/sidebar/ProjectRow.tsx @@ -201,7 +201,6 @@ interface SectionThreadTreeProps { collapsedThreadIds: Set; collapsedEnvironmentIds: Set; onProjectSelect?: () => void; - onCreateSection?: () => void; onCreateThreadInSection?: (sectionId: string) => void; onRenameSection?: (section: SidebarSectionDefinition) => void; onRemoveSection?: (section: SidebarSectionDefinition) => void; @@ -303,7 +302,6 @@ interface ThreadTreeItemRowProps { collapsedEnvironmentIds: Set; variant: ProjectThreadTreeVariant; onProjectSelect?: () => void; - onCreateSection?: () => void; onCreateThreadInSection?: (sectionId: string) => void; onRenameSection?: (section: SidebarSectionDefinition) => void; onRemoveSection?: (section: SidebarSectionDefinition) => void; @@ -326,7 +324,6 @@ interface SectionTreeItemRowProps { collapsedEnvironmentIds: Set; variant: ProjectThreadTreeVariant; onProjectSelect?: () => void; - onCreateSection?: () => void; onCreateThreadInSection?: (sectionId: string) => void; onRenameSection?: (section: SidebarSectionDefinition) => void; onRemoveSection?: (section: SidebarSectionDefinition) => void; @@ -1196,7 +1193,6 @@ const ThreadTreeItemRow = memo(function ThreadTreeItemRow({ collapsedEnvironmentIds, variant, onProjectSelect, - onCreateSection, onCreateThreadInSection, onRenameSection, onRemoveSection, @@ -1220,7 +1216,6 @@ const ThreadTreeItemRow = memo(function ThreadTreeItemRow({ collapsedEnvironmentIds={collapsedEnvironmentIds} variant={variant} onProjectSelect={onProjectSelect} - onCreateSection={onCreateSection} onCreateThreadInSection={onCreateThreadInSection} onRenameSection={onRenameSection} onRemoveSection={onRemoveSection} @@ -1342,7 +1337,6 @@ const SectionTreeItemRow = memo(function SectionTreeItemRow({ collapsedEnvironmentIds, variant, onProjectSelect, - onCreateSection, onCreateThreadInSection, onRenameSection, onRemoveSection, @@ -1426,7 +1420,6 @@ const SectionTreeItemRow = memo(function SectionTreeItemRow({ collapsedEnvironmentIds={collapsedEnvironmentIds} variant={variant} onProjectSelect={onProjectSelect} - onCreateSection={onCreateSection} onCreateThreadInSection={onCreateThreadInSection} onRenameSection={onRenameSection} onRemoveSection={onRemoveSection} @@ -1455,9 +1448,7 @@ const SectionTreeItemRow = memo(function SectionTreeItemRow({ if (variant === "section" && depthOffset === 0) { const externalHeaderActions = renderTopLevelSectionHeaderActions?.(section); - const hasMenuActions = Boolean( - onCreateSection || onRenameSection || onRemoveSection, - ); + const hasMenuActions = Boolean(onRenameSection || onRemoveSection); const hasTopLevelActions = Boolean( externalHeaderActions?.actions || hasMenuActions || @@ -1488,12 +1479,6 @@ const SectionTreeItemRow = memo(function SectionTreeItemRow({ - {onCreateSection ? ( - - - ) : null} {onRenameSection ? ( onRenameSection(section)}>