diff --git a/docs/assets/sidebar-category-header-after.png b/docs/assets/sidebar-category-header-after.png new file mode 100644 index 00000000000..c6f6f0a2c10 Binary files /dev/null and b/docs/assets/sidebar-category-header-after.png differ diff --git a/docs/assets/sidebar-category-header-before.png b/docs/assets/sidebar-category-header-before.png new file mode 100644 index 00000000000..e74e9b9f6a2 Binary files /dev/null and b/docs/assets/sidebar-category-header-before.png differ diff --git a/docs/developers/daemon-client-adapters/web-shell.md b/docs/developers/daemon-client-adapters/web-shell.md index 1d65066594d..d89967afa76 100644 --- a/docs/developers/daemon-client-adapters/web-shell.md +++ b/docs/developers/daemon-client-adapters/web-shell.md @@ -37,6 +37,22 @@ The sidebar trigger, dialog heading, and non-browse submit button all use The Recent heading and its region name continue to share the same translation key; a separate descriptive region label is outside this copy-only update. +## Sidebar category headers + +Pinned, Workspaces, No-workspace sessions, and Recent are peer collapsible +categories. Their active sidebar headers use the same height, spacing, muted +text, and trailing expansion chevron, including hover and keyboard-focus reveal +rules. No-workspace sessions retains its independent saved expansion preference +and search behavior. Its header in Archived settings keeps the workspace-entry +presentation. + +Chromium comparison with simulated daemon responses (hovering the collapsed +No-workspace sessions category): + +| Before | After | +| -------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| ![Workspace-entry header](../../assets/sidebar-category-header-before.png) | ![Peer category header](../../assets/sidebar-category-header-after.png) | + ## Shared UI Contract Use the TypeScript SDK daemon UI exports as the common boundary: diff --git a/packages/web-shell/client/components/sidebar/StandaloneRecents.test.tsx b/packages/web-shell/client/components/sidebar/StandaloneRecents.test.tsx index 69d5dda25a4..42a83ad4a29 100644 --- a/packages/web-shell/client/components/sidebar/StandaloneRecents.test.tsx +++ b/packages/web-shell/client/components/sidebar/StandaloneRecents.test.tsx @@ -194,7 +194,9 @@ describe('StandaloneRecents', () => { const newSession = container.querySelector( '[data-testid="standalone-active-group"] button[aria-label="sidebar.newTask"]', ); - expect(newSession?.parentElement?.className).toContain('invisible'); + expect(newSession?.parentElement?.className).toContain( + 'projectsHeaderActions', + ); const sessions = container.querySelector( '[data-testid="standalone-active-group"] > div:nth-child(2)', ); @@ -202,6 +204,11 @@ describe('StandaloneRecents', () => { const header = container.querySelector( '[data-testid="standalone-active-group"] > div > button', )!; + expect(header.parentElement?.className).toContain('projectsHeader'); + expect(header.className).toContain('projectsHeaderToggle'); + expect(header.querySelector('span + svg')?.classList).toContain( + 'lucide-chevron-down', + ); header.focus(); await act(async () => header.dispatchEvent( @@ -209,6 +216,9 @@ describe('StandaloneRecents', () => { ), ); expect(document.activeElement).not.toBe(header); + expect(header.querySelector('span + svg')?.classList).toContain( + 'lucide-chevron-right', + ); await act(async () => header.dispatchEvent( new MouseEvent('click', { bubbles: true, detail: 1 }), @@ -313,6 +323,16 @@ describe('StandaloneRecents', () => { it('loads only the requested archived group', async () => { const { renderSession } = await render({ archiveState: 'archived' }); + const group = container.querySelector( + '[data-testid="standalone-archived-group"]', + )!; + expect(group.classList).toContain('mb-3'); + const header = group.querySelector('button[aria-expanded]')!; + expect(header.parentElement?.className).toContain('headerRow'); + expect(header.querySelector('svg')?.classList).toContain( + 'lucide-circle-dashed', + ); + expect(header.querySelector('span + svg')).toBeNull(); expect(container.textContent).toContain('Archived chat'); expect(container.textContent).not.toContain('Active chat'); expect(renderSession).toHaveBeenCalledWith( diff --git a/packages/web-shell/client/components/sidebar/StandaloneRecents.tsx b/packages/web-shell/client/components/sidebar/StandaloneRecents.tsx index 33a5923f623..ccf9903682a 100644 --- a/packages/web-shell/client/components/sidebar/StandaloneRecents.tsx +++ b/packages/web-shell/client/components/sidebar/StandaloneRecents.tsx @@ -5,7 +5,12 @@ import { useState, type ReactNode, } from 'react'; -import { CircleDashedIcon, SquarePenIcon } from 'lucide-react'; +import { + ChevronDownIcon, + ChevronRightIcon, + CircleDashedIcon, + SquarePenIcon, +} from 'lucide-react'; import { STANDALONE_SESSIONS_CAPABILITY, type DaemonSessionArchiveState, @@ -469,7 +474,11 @@ export function StandaloneRecents({ return ( <>
-
+
{archiveState === 'active' && onNewSession && ( -
+