feat(web): open workspace picker from composer

This commit is contained in:
NI0317
2026-08-07 14:33:18 +08:00
parent 23a4c0a51e
commit c44c6c93c8
16 changed files with 165 additions and 33 deletions
@@ -189,12 +189,20 @@ describe('resident composer', () => {
runtime.slots.installLocale(locale)
await runtime.root.declare(LAYOUT_CHILDREN, AppRoot)
await runtime.mount({ inject: [...inject], apply })
runtime.slots.register({ name: 'conversation.hero.workspace' }, WorkspaceProbe)
const view = runtime.renderRoot()
// No session entity: the inert twin renders (disabled textarea), and the
// workspace picker chip is the only live control.
const textarea = view.container.querySelector('textarea')
expect(textarea).not.toBeNull()
expect(textarea!.disabled).toBe(true)
expect(textarea!.disabled).toBe(false)
expect(textarea!.readOnly).toBe(true)
expect(textarea!.getAttribute('aria-haspopup')).toBe('menu')
expect(view.getByTestId('workspace-probe').textContent).toBe('false:0')
fireEvent.click(textarea!)
expect(view.getByTestId('workspace-probe').textContent).toBe('true:0')
expect(textarea!.getAttribute('aria-expanded')).toBe('true')
fireEvent.click(view.getByRole('button', { name: '选择工作区' }))
fireEvent.keyDown(textarea!, { key: 'Enter' })
expect(view.getByTestId('workspace-probe').textContent).toBe('true:0')
expect(view.getByRole('button', { name: '选择工作区' })).toBeTruthy()
await runtime.dispose()
})
@@ -219,7 +227,8 @@ describe('resident composer', () => {
const textarea = view.container.querySelector('textarea')!
const workspaceChip = view.getByRole('button', { name: '选择工作区' })
const workspaceProbe = view.getByTestId('workspace-probe')
expect(textarea.disabled).toBe(true)
expect(textarea.disabled).toBe(false)
expect(textarea.readOnly).toBe(true)
fireEvent.click(workspaceChip)
fireEvent.click(workspaceProbe)
@@ -239,6 +248,7 @@ describe('resident composer', () => {
expect(view.getByTestId('workspace-probe')).toBe(workspaceProbe)
expect(workspaceProbe.textContent).toBe('true:1')
expect(textarea.disabled).toBe(false)
expect(textarea.readOnly).toBe(false)
await runtime.dispose()
})
@@ -55,6 +55,9 @@ interface BenchOptions {
running?: boolean
subagent?: Exclude<ConversationSnapshot['subagent'], null>
disabled?: boolean
inert?: boolean
workspacePickerOpen?: boolean
onRequestWorkspace?: () => void
promptError?: ConversationSnapshot['promptError']
variant?: 'hero' | 'composer'
placeholder?: string
@@ -135,6 +138,9 @@ function bench(over?: BenchOptions) {
t: over?.t ?? makeTranslate(zh, commonZh),
renderSlot,
variant: over?.variant ?? 'composer',
...(over?.inert === true ? { disabled: true } : {}),
...(over?.workspacePickerOpen !== undefined ? { workspacePickerOpen: over.workspacePickerOpen } : {}),
...(over?.onRequestWorkspace !== undefined ? { onRequestWorkspace: over.onRequestWorkspace } : {}),
...(over?.placeholder !== undefined ? { placeholder: over.placeholder } : {}),
...(over?.accessory !== undefined ? { accessory: over.accessory } : {}),
...(over?.overlay !== undefined ? { overlay: over.overlay } : {}),
@@ -539,6 +545,26 @@ describe('running and lock semantics (queue cut 1)', () => {
expect(custom.textarea.placeholder).toBe('Custom placeholder')
})
it('the inert textarea opens the Workspace picker by pointer or keyboard', () => {
const onRequestWorkspace = vi.fn()
const { view, textarea } = bench({
inert: true,
workspacePickerOpen: false,
onRequestWorkspace,
placeholder: '选择一个工作区开始',
})
expect(textarea.disabled).toBe(false)
expect(textarea.readOnly).toBe(true)
expect(textarea.getAttribute('aria-haspopup')).toBe('menu')
expect(textarea.getAttribute('aria-expanded')).toBe('false')
expect((view.getByLabelText('命令') as HTMLButtonElement).disabled).toBe(true)
fireEvent.click(textarea)
fireEvent.keyDown(textarea, { key: 'Enter' })
fireEvent.keyDown(textarea, { key: ' ' })
expect(onRequestWorkspace).toHaveBeenCalledTimes(3)
})
it('the plan projection swaps the placeholder while its effective target is plan mode', () => {
const active = bench({ plan: { active: true, pending: false } })
expect(active.textarea.placeholder).toBe('描述你的任务以生成计划')