Sfoglia il codice sorgente

feat(client): simplify and exercise sidebar panel entries

imccyu 3 settimane fa
parent
commit
bc840b483e

+ 2 - 2
packages/client/ui-sidebar/src/client/SidebarRoot.tsx

@@ -53,7 +53,7 @@ type PanelRowProps =
   & Pick<SidebarSectionOwnerProps, 'wide'>
   & Pick<PropsRuntime<'sidebar'>, 'usePanelInfo'>
   & Pick<InjectFace<SidebarRootInjected>, 'selectPanel'>
-  & PropsRenderSlots<'sidebar.panellist' | 'sidebar.panellist.title'>
+  & PropsRenderSlots<'sidebar.panellist'>
 
 /** Each panel row subscribes only to its own selection state. */
 function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }: PanelRowProps) {
@@ -72,7 +72,7 @@ function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }: Pa
         </span>
         {wide && (
           <span className={clsx(css.panelTitle, css.wide)}>
-            {renderSlot('sidebar.panellist.title', { active }, { entryKey: id, fallback: label })}
+            {label}
           </span>
         )}
       </button>

+ 3 - 12
packages/client/ui-sidebar/src/client/contract/slots.ts

@@ -30,8 +30,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
      * the sidebar owns the button and resolves its label from list metadata.
      */
     'sidebar.panellist': { kind: 'list'; scope: 'root'; owner: SidebarPanelIconOwnerProps }
-    /** Global panel titles keyed by list id, with the list label as fallback. */
-    'sidebar.panellist.title': { kind: 'keyed'; scope: 'root'; owner: SidebarPanelTitleOwnerProps }
     /**
      * The workspace/session browsing region: section header, search, the
      * grouped/flat session list, and every workspace dialog. Declared by this
@@ -73,19 +71,13 @@ export interface SidebarPanelIconOwnerProps {
   active: boolean
 }
 
-/** Title presentation supplied by the global panel row. */
-export interface SidebarPanelTitleOwnerProps {
-  /** Whether this panel is selected in the main column. */
-  active: boolean
-}
-
 /** Serializable metadata for one active global panel list registration. */
 export interface SidebarPanelMetadata {
   /** List id and matching main panel key. */
   id: MainPanelId
   /** Ascending row order; ties retain registration order. */
   order: number
-  /** Localized accessible name and fallback title. */
+  /** Row title and accessible name: resolved label, or the id when omitted. */
   label: string
 }
 
@@ -128,8 +120,8 @@ export type SidebarRootInjected = {
   startSession: (workspaceId?: WorkspaceId) => void
   /** Toggle the sidebar column through the layout service. */
   toggleSidebar: () => void
-  /** Select a global panel, or the conversation when null. */
-  selectPanel: (id: MainPanelId | null) => void
+  /** Select the global panel addressed by a sidebar row. */
+  selectPanel: (id: MainPanelId) => void
   /** Private reactive sources bound to framework selector hooks. */
   hooks: { panels: ObservableSnapshot<readonly SidebarPanelMetadata[]> }
 }
@@ -145,7 +137,6 @@ export type SidebarRootComponentProps =
     | 'sidebar.brand.mark'
     | 'sidebar.brand.name'
     | 'sidebar.panellist'
-    | 'sidebar.panellist.title'
     | 'sidebar.workspaces'
     | 'sidebar.settings'
     | 'sidebar.footer.action'

+ 1 - 2
packages/client/ui-sidebar/src/client/index.ts

@@ -15,7 +15,7 @@ import { en, zh, type SidebarKey } from './locales.ts'
 
 export type {
   SidebarBrandMarkOwnerProps, SidebarBrandNameOwnerProps, SidebarFooterActionOwnerProps,
-  SidebarPanelIconOwnerProps, SidebarPanelMetadata, SidebarPanelTitleOwnerProps,
+  SidebarPanelIconOwnerProps, SidebarPanelMetadata,
   SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps, SidebarSettingsOwnerProps,
 } from './contract/slots.ts'
 export type { SidebarKey } from './locales.ts'
@@ -75,7 +75,6 @@ export function apply(ctx: ClientContext): void {
       'sidebar.brand.mark': { kind: 'single', scope: 'root' },
       'sidebar.brand.name': { kind: 'single', scope: 'root' },
       'sidebar.panellist': { kind: 'list', scope: 'root' },
-      'sidebar.panellist.title': { kind: 'keyed', scope: 'root' },
       'sidebar.workspaces': { kind: 'single', scope: 'root' },
       'sidebar.settings': { kind: 'single', scope: 'root' },
       'sidebar.footer.action': { kind: 'list', scope: 'root' },

+ 22 - 4
packages/client/ui-sidebar/tests/apply.client.spec.tsx

@@ -1,19 +1,34 @@
 /** Sidebar shell slot registration and its Session/layout callbacks. */
-import { Context } from '@deepseek-ai/cordis'
-import { describe, expect, it, vi } from 'vitest'
+import { Context, type Fiber } from '@deepseek-ai/cordis'
+import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
 import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
 import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
 import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
 import { apply, inject } from '@deepseek-ai/dsh-client-ui-sidebar/client'
 import type { SidebarRootInjected } from '@deepseek-ai/dsh-client-ui-sidebar/client'
+import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client'
 import { apply as hostApply } from '../src/index.ts'
 
+const owners = new Set<Fiber>()
+afterEach(async () => {
+  try {
+    for (const owner of owners) await owner.dispose()
+  } finally {
+    owners.clear()
+  }
+})
+
 function SidebarFrame({ renderSlot }: PropsRenderSlots<'sidebar'>) {
   return renderSlot('sidebar', { collapsed: false, width: 300 })
 }
 
 async function bench(declare = true) {
-  const ctx = new Context()
+  const root = new Context()
+  let ctx: Context | undefined
+  const owner = root.plugin((owned: Context) => { ctx = owned })
+  owners.add(owner)
+  await owner.await()
+  if (ctx === undefined) throw new Error('the sidebar fixture owner did not activate')
   await ctx.plugin(SlotRegistry).await()
   const layout = { toggleSidebar: vi.fn(), selectPanel: vi.fn() }
   const uiWorkspace = { startSession: vi.fn() }
@@ -52,7 +67,6 @@ describe('ui-sidebar apply', () => {
     expect(b.slots.spec('sidebar.settings')).toEqual({ kind: 'single', scope: 'root' })
     expect(b.slots.spec('sidebar.footer.action')).toEqual({ kind: 'list', scope: 'root' })
     expect(b.slots.spec('sidebar.panellist')).toEqual({ kind: 'list', scope: 'root' })
-    expect(b.slots.spec('sidebar.panellist.title')).toEqual({ kind: 'keyed', scope: 'root' })
     // Copy rides the standard locale seat, not the inject face.
     expect(b.slots.entries('sidebar')[0]!.locale).toBe('sidebar')
     const injected = (b.slots.entries('sidebar')[0]!.inject as () => SidebarRootInjected)()
@@ -66,6 +80,10 @@ describe('ui-sidebar apply', () => {
     expect(b.uiWorkspace.startSession).toHaveBeenLastCalledWith(undefined)
     injected.toggleSidebar()
     expect(b.layout.toggleSidebar).toHaveBeenCalledOnce()
+    const panelId = 'custom-panel' as MainPanelId
+    injected.selectPanel(panelId)
+    expect(b.layout.selectPanel).toHaveBeenCalledExactlyOnceWith(panelId)
+    expectTypeOf<Parameters<SidebarRootInjected['selectPanel']>[0]>().toEqualTypeOf<MainPanelId>()
   })
 
   it('waits for the sidebar declaration before registering', async () => {

+ 276 - 0
packages/client/ui-sidebar/tests/panel-list.client.spec.tsx

@@ -0,0 +1,276 @@
+// @vitest-environment jsdom
+/** Global panel rows and DOM focus through the production slot renderer. */
+import type { Context } from '@deepseek-ai/cordis'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { act, cleanup, fireEvent, waitFor, within } from '@testing-library/react'
+import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
+import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
+import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
+import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
+import { IconGlobeOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
+import type { ILayout, MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client'
+import type { PropsRenderSlots, PropsRuntime, SlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
+import { apply, inject } from '../src/client/index.ts'
+
+declare module '@deepseek-ai/dsh-client-ui-slots' {
+  interface LocaleNamespaceMap {
+    'sidebar-panel-test': 'alpha'
+  }
+}
+
+const ALPHA = 'sidebar-test-alpha' as MainPanelId
+const BETA = 'sidebar-test-beta' as MainPanelId
+const runtimes = new Set<SlotTestRuntime>()
+
+afterEach(async () => {
+  try {
+    for (const runtime of runtimes) await runtime.dispose()
+  } finally {
+    runtimes.clear()
+    cleanup()
+  }
+})
+
+async function bench(collapsed = false) {
+  const runtime = await SlotTestRuntime.create()
+  runtimes.add(runtime)
+  const locale = new LocaleRuntime(runtime.ctx)
+  locale.setLocale('en')
+  const layout = {
+    beginNavigation: vi.fn(() => new AbortController().signal),
+    toggleSidebar: vi.fn(),
+    selectPanel: vi.fn((activePanelId: MainPanelId | null) => { runtime.panelInfo.set({ activePanelId }) }),
+    openRightbar: vi.fn(),
+    closeRightbar: vi.fn(),
+  } satisfies ILayout
+  await runtime.mount({
+    inject: ['slots'],
+    apply(ctx: Context) {
+      ctx.provide('layout', layout)
+      ctx.provide('uiWorkspace', { startSession: vi.fn() } as never)
+      ctx.provide('locale', locale)
+      ctx.effect(() => locale.register('common', { zh: commonZh, en: commonEn }), 'panel test: common locale')
+      ctx.effect(() => locale.register('sidebar-panel-test', {
+        zh: { alpha: '甲面板' }, en: { alpha: 'Alpha panel' },
+      }), 'panel test: panel locale')
+      ctx.slots.installLocale(locale)
+      ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'conversation' }, () => (
+        <p>Conversation content</p>
+      )))
+    },
+  })
+  function Frame({ usePanelInfo, renderSlot }: PropsRuntime<'root'> & PropsRenderSlots<'sidebar' | 'main'>) {
+    const activePanelId = usePanelInfo(info => info.activePanelId)
+    return (
+      <>
+        <aside>{renderSlot('sidebar', { collapsed, width: collapsed ? 56 : 300 })}</aside>
+        <main>{renderSlot('main', {}, { entryKey: activePanelId ?? 'conversation' })}</main>
+      </>
+    )
+  }
+  await runtime.root.declare({
+    sidebar: { kind: 'single', scope: 'root' },
+    main: { kind: 'keyed', scope: 'root' },
+  }, Frame)
+  const sidebar = await runtime.mount({ inject: [...inject], apply })
+  const view = runtime.renderRoot()
+  return { runtime, locale, layout, sidebar, view }
+}
+
+interface TestPanel {
+  id: MainPanelId
+  heading: string
+  order?: number
+  label?: SlotLabel
+}
+
+async function mountPanel(runtime: SlotTestRuntime, { heading, ...metadata }: TestPanel) {
+  function Icon({ size, active }: PropsRuntime<'sidebar.panellist'>) {
+    return (
+      <span data-testid={`${metadata.id}-icon`} data-active={active}>
+        <IconGlobeOutline14 size={size} />
+      </span>
+    )
+  }
+  function Body() {
+    return (
+      <section>
+        <h1>{heading}</h1>
+        <input aria-label={`${heading} input`} />
+        <button type="button">{heading} action</button>
+      </section>
+    )
+  }
+  return runtime.mount({
+    inject: ['slots'],
+    apply(ctx: Context) {
+      ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: metadata.id }, Body))
+      ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({ name: 'sidebar.panellist', ...metadata }, Icon))
+    },
+  })
+}
+
+async function mountPanels(runtime: SlotTestRuntime, locale: LocaleRuntime, betaOrder = 10) {
+  const t = locale.bind('sidebar-panel-test')
+  const alpha = await mountPanel(runtime, { id: ALPHA, heading: 'Alpha content', label: () => t('alpha'), order: 20 })
+  const beta = await mountPanel(runtime, { id: BETA, heading: 'Beta content', label: 'Beta panel', order: betaOrder })
+  return { alpha, beta }
+}
+
+describe('sidebar global panels', () => {
+  it('adds late registrations, removes each plugin contribution, and leaves no empty panel-list DOM', async () => {
+    const { runtime, locale, view } = await bench()
+    expect(runtime.slots.entries('sidebar.panellist')).toEqual([])
+    expect(view.queryByRole('navigation')).toBeNull()
+    expect(view.container.querySelector('[data-slot="sidebar.panellist"]')).toBeNull()
+    expect(view.getByText('Conversation content')).toBeTruthy()
+
+    const { alpha, beta } = await mountPanels(runtime, locale)
+    const navigation = await view.findByRole('navigation', { name: 'Global panels' })
+    expect(within(navigation).getAllByRole('button')).toHaveLength(2)
+    fireEvent.click(within(navigation).getByRole('button', { name: 'Alpha panel' }))
+    expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy()
+
+    await alpha.dispose()
+    await waitFor(() => {
+      expect(view.queryByRole('button', { name: 'Alpha panel' })).toBeNull()
+      expect(view.queryByRole('heading', { name: 'Alpha content' })).toBeNull()
+    })
+    expect(runtime.slots.entries('main').map(entry => entry.options.key)).toEqual(['conversation', BETA])
+    fireEvent.click(within(navigation).getByRole('button', { name: 'Beta panel' }))
+    expect(view.getByRole('heading', { name: 'Beta content' })).toBeTruthy()
+
+    await beta.dispose()
+    await waitFor(() => { expect(view.queryByRole('navigation')).toBeNull() })
+    expect(view.queryByRole('heading', { name: 'Beta content' })).toBeNull()
+    expect(view.container.querySelector('[data-slot="sidebar.panellist"]')).toBeNull()
+    expect(runtime.slots.entries('sidebar.panellist')).toEqual([])
+    expect(runtime.slots.entries('main').map(entry => entry.options.key)).toEqual(['conversation'])
+  })
+
+  it.each([false, true])('renders ordered rows, icon sizes, and tooltip labels with collapsed=%s', async (collapsed) => {
+    const { runtime, locale, view } = await bench(collapsed)
+    await mountPanels(runtime, locale)
+    const navigation = await view.findByRole('navigation', { name: 'Global panels' })
+    const rows = within(navigation).getAllByRole('button')
+    expect(rows.map(row => row.getAttribute('aria-label'))).toEqual(['Beta panel', 'Alpha panel'])
+    for (const [id, label] of [[BETA, 'Beta panel'], [ALPHA, 'Alpha panel']] as const) {
+      const row = within(navigation).getByRole('button', { name: label })
+      expect(row.getAttribute('aria-current')).toBeNull()
+      expect(row.textContent).toBe(collapsed ? '' : label)
+      expect(row.querySelectorAll('svg')).toHaveLength(1)
+      const icon = within(row).getByTestId(`${id}-icon`)
+      expect(icon.getAttribute('data-active')).toBe('false')
+      expect(icon.querySelector('svg')?.getAttribute('width')).toBe(collapsed ? '18' : '16')
+      expect(icon.querySelector('svg')?.getAttribute('height')).toBe(collapsed ? '18' : '16')
+      act(() => { row.focus() })
+      expect(document.activeElement).toBe(row)
+      if (collapsed) expect(view.getByRole('tooltip').textContent).toBe(label)
+      else expect(view.queryByRole('tooltip')).toBeNull()
+      act(() => { row.blur() })
+      expect(document.activeElement).not.toBe(row)
+      expect(view.queryByRole('tooltip')).toBeNull()
+    }
+  })
+
+  it('keeps registration order when panel rows have equal order', async () => {
+    const { runtime, locale, view } = await bench()
+    await mountPanels(runtime, locale, 20)
+    const navigation = await view.findByRole('navigation', { name: 'Global panels' })
+    expect(within(navigation).getAllByRole('button').map(row => row.textContent)).toEqual(['Alpha panel', 'Beta panel'])
+  })
+
+  it('selects registered main content and keeps a repeated selection active', async () => {
+    const { runtime, locale, layout, view } = await bench()
+    await mountPanels(runtime, locale)
+    const navigation = await view.findByRole('navigation', { name: 'Global panels' })
+    const alpha = within(navigation).getByRole('button', { name: 'Alpha panel' })
+    const beta = within(navigation).getByRole('button', { name: 'Beta panel' })
+    fireEvent.click(alpha)
+    expect(layout.selectPanel).toHaveBeenLastCalledWith(ALPHA)
+    expect(runtime.panelInfo.getSnapshot()).toEqual({ activePanelId: ALPHA })
+    expect(alpha.getAttribute('aria-current')).toBe('page')
+    expect(beta.getAttribute('aria-current')).toBeNull()
+    expect(within(alpha).getByTestId(`${ALPHA}-icon`).getAttribute('data-active')).toBe('true')
+    expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy()
+    expect(view.queryByRole('heading', { name: 'Beta content' })).toBeNull()
+
+    fireEvent.click(alpha)
+    expect(layout.selectPanel).toHaveBeenLastCalledWith(ALPHA)
+    expect(alpha.getAttribute('aria-current')).toBe('page')
+    expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy()
+
+    fireEvent.click(beta)
+    expect(layout.selectPanel).toHaveBeenLastCalledWith(BETA)
+    expect(runtime.panelInfo.getSnapshot()).toEqual({ activePanelId: BETA })
+    expect(alpha.getAttribute('aria-current')).toBeNull()
+    expect(beta.getAttribute('aria-current')).toBe('page')
+    expect(within(alpha).getByTestId(`${ALPHA}-icon`).getAttribute('data-active')).toBe('false')
+    expect(within(beta).getByTestId(`${BETA}-icon`).getAttribute('data-active')).toBe('true')
+    expect(view.queryByRole('heading', { name: 'Alpha content' })).toBeNull()
+    expect(view.getByRole('heading', { name: 'Beta content' })).toBeTruthy()
+  })
+
+  it('refreshes locale labels without re-registering icons or changing plain labels', async () => {
+    const { runtime, locale, view } = await bench()
+    await mountPanels(runtime, locale)
+    const navigation = await view.findByRole('navigation', { name: 'Global panels' })
+    const entries = runtime.slots.entries('sidebar.panellist')
+    act(() => { locale.setLocale('zh') })
+    await waitFor(() => {
+      expect(within(navigation).getByRole('button', { name: '甲面板' }).textContent).toBe('甲面板')
+    })
+    expect(within(navigation).getByRole('button', { name: 'Beta panel' }).textContent).toBe('Beta panel')
+    expect(runtime.slots.entries('sidebar.panellist')).toBe(entries)
+  })
+
+  it('uses an omitted label and order as the panel id and order zero', async () => {
+    const { runtime, view } = await bench()
+    await mountPanel(runtime, { id: ALPHA, heading: 'Alpha content', label: 'Alpha panel', order: 20 })
+    await mountPanel(runtime, { id: BETA, heading: 'Beta content' })
+    const navigation = await view.findByRole('navigation', { name: 'Global panels' })
+    expect(within(navigation).getAllByRole('button').map(row => row.textContent)).toEqual([BETA, 'Alpha panel'])
+    fireEvent.click(within(navigation).getByRole('button', { name: BETA }))
+    expect(view.getByRole('heading', { name: 'Beta content' })).toBeTruthy()
+  })
+
+  it.each([false, true])('keeps main selection while DOM focus moves between controls with collapsed=%s', async (collapsed) => {
+    const { runtime, locale, layout, view } = await bench(collapsed)
+    await mountPanels(runtime, locale)
+    const navigation = await view.findByRole('navigation', { name: 'Global panels' })
+    const alpha = within(navigation).getByRole('button', { name: 'Alpha panel' })
+    const beta = within(navigation).getByRole('button', { name: 'Beta panel' })
+    act(() => { alpha.focus() })
+    expect(document.activeElement).toBe(alpha)
+    expect(runtime.panelInfo.getSnapshot()).toEqual({ activePanelId: null })
+    expect(alpha.getAttribute('aria-current')).toBeNull()
+    expect(layout.selectPanel).not.toHaveBeenCalled()
+    fireEvent.click(alpha)
+    const selected = runtime.panelInfo.getSnapshot()
+    const textbox = view.getByRole('textbox', { name: 'Alpha content input' })
+    const action = view.getByRole('button', { name: 'Alpha content action' })
+    for (const target of [textbox, action, beta, alpha]) {
+      act(() => { target.focus() })
+      expect(document.activeElement).toBe(target)
+      expect(runtime.panelInfo.getSnapshot()).toBe(selected)
+      expect(alpha.getAttribute('aria-current')).toBe('page')
+      expect(beta.getAttribute('aria-current')).toBeNull()
+      expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy()
+      expect(view.queryByRole('heading', { name: 'Beta content' })).toBeNull()
+    }
+    expect(layout.selectPanel).toHaveBeenCalledExactlyOnceWith(ALPHA)
+  })
+
+  it('disposes the sidebar while retaining independently owned main panel bodies', async () => {
+    const { runtime, locale, sidebar, view } = await bench()
+    await mountPanels(runtime, locale)
+    fireEvent.click(await view.findByRole('button', { name: 'Alpha panel' }))
+    await sidebar.dispose()
+    expect(view.queryByRole('navigation')).toBeNull()
+    expect(runtime.slots.spec('sidebar.panellist')).toBeUndefined()
+    expect(runtime.slots.entries('sidebar.panellist')).toEqual([])
+    expect(view.getByRole('heading', { name: 'Alpha content' })).toBeTruthy()
+    act(() => { locale.setLocale('zh') })
+    expect(view.queryByRole('navigation')).toBeNull()
+  })
+})

+ 25 - 0
packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx

@@ -9,6 +9,7 @@ import type {
 } from '@deepseek-ai/dsh-api-workspace-controller/client'
 import type { SessionPendingInteractionSnapshot } from '@deepseek-ai/dsh-client-ui-session/client'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
+import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client'
 import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
 import type { WorkspaceBrowserProps } from '../src/client/contract/slots.ts'
 import { createWorkspaceViewStore, FLAT_SESSION_ORDER_KEY } from '../src/client/stores.ts'
@@ -112,6 +113,30 @@ function rerender(b: ReturnType<typeof mount>, overrides: Partial<WorkspaceBrows
 }
 
 describe('WorkspaceBrowser', () => {
+  it('moves focus into Workspace controls without selecting a Session while a main panel is active', () => {
+    const panelInfo = { activePanelId: 'panel-a' as MainPanelId }
+    const b = mount({
+      usePanelInfo: hook(panelInfo),
+      useSessions: hook(sessionState([summary('current', 1)], { current: sid('current') })),
+      useWorkspaces: hook(workspaceState([workspace('alpha', ['current'])])),
+    })
+    fireEvent.click(screen.getByText('alpha'))
+    const current = screen.getByText('current').closest('[role="treeitem"]')
+    expect(current?.getAttribute('aria-selected')).toBe('false')
+    fireEvent.click(screen.getByRole('button', { name: '搜索会话' }))
+    const input = screen.getByPlaceholderText('搜索会话…')
+    expect(document.activeElement).toBe(input)
+    fireEvent.click(screen.getByRole('button', { name: '清除搜索' }))
+    const add = screen.getByRole('button', { name: '添加工作区' })
+    add.focus()
+    fireEvent.click(add)
+    expect(document.activeElement).toBe(add)
+    expect(screen.getByTestId('directory-flow')).toBeTruthy()
+    expect(panelInfo.activePanelId).toBe('panel-a')
+    expect(b.props.open).not.toHaveBeenCalled()
+    expect(b.props.startSession).not.toHaveBeenCalled()
+  })
+
   it('workspace hover card shows a POSIX home descendant as ~', () => {
     vi.useFakeTimers()
     try {