Merge remote-tracking branch 'origin/feat/directory-picker' into feat/workspace-directory-browser

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.i18n.yaml
#	packages/host/directory-picker-browse/README.i18n.yaml
#	packages/host/directory-picker-browse/package.json
#	pnpm-lock.yaml
This commit is contained in:
creatixchu
2026-07-29 04:12:44 +08:00
16 changed files with 155 additions and 35 deletions

View File

@@ -119,7 +119,7 @@ export class TestWorkspaces implements IWorkspaces {
this.calls.push({ method: 'listDirectory', args: [path] })
const stub = this.stubs.get('listDirectory')
if (stub !== undefined) return await (stub(path) as Promise<DirectoryListing>)
return { path: '/home/test', home: '/home/test', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [] }
return { path: '/home/test', home: '/home/test', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false }
}
/**

View File

@@ -98,9 +98,12 @@ export function WorkspaceCreateFlow({
const flowAvailable = useDirectoryFlow(occupied => occupied)
// An occupant that unloads mid-interaction leaves nobody to cancel: an
// open flow over an empty hole withdraws so the menu actions come back.
// flowOpen is a dependency because the flow can also OPEN over an already
// empty hole (Choose again after the occupant unloaded with the error
// dialog up) — that transition must snap back too, not just occupancy loss.
useEffect(() => {
if (!flowAvailable) setFlowOpen(false)
}, [flowAvailable])
if (flowOpen && !flowAvailable) setFlowOpen(false)
}, [flowOpen, flowAvailable])
const createEntries: MenuEntry[] = [
...(flowAvailable
? [{ id: OPEN_LOCAL_FOLDER, label: 'Open local folder…', icon: <IconFolderClose16 size={16} />, disabled: flowBusy }]
@@ -224,7 +227,9 @@ export function WorkspaceCreateFlow({
footer={(
<>
<Button variant="outline" className={css.modalAction} onClick={closeModal}>Cancel</Button>
<Button variant="primary" className={css.modalAction} onClick={openLocalFolder}>Choose again</Button>
{/* Retrying needs an occupant to serve the flow; without one the
* button would open a flow nobody can answer or cancel. */}
<Button variant="primary" className={css.modalAction} disabled={!flowAvailable} onClick={openLocalFolder}>Choose again</Button>
</>
)}
>

View File

@@ -303,6 +303,20 @@ describe('WorkspacePicker', () => {
expect(screen.getByRole('menuitem', { name: 'Open local folder…' })).toBeTruthy()
})
it('keeps Choose again inert while the flow occupant is gone, and snaps back a flow opened over an empty hole', async () => {
const b = mount([], vi.fn(async () => { throw new Error('adoption failed') }))
chooseItem('Open local folder…')
await act(async () => { b.probe.owner!.onPicked('/one/project') })
await waitFor(() => { expect(screen.getByRole('dialog', { name: 'Couldnt open folder' })).toBeTruthy() })
// The occupant unloads while the error dialog is up: retrying would open
// a flow nobody can serve or cancel, so the button goes inert.
act(() => { b.occupancy.flip(false) })
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Choose again' }).disabled).toBe(true)
// Cancel stays the way out, and the menu actions are usable again.
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
expect(screen.getByRole<HTMLButtonElement>('menuitem', { name: 'Create a new workspace' }).disabled).toBe(false)
})
it('withdraws an open flow when its occupant unloads, re-enabling the menu actions', () => {
const b = mount([])
chooseItem('Open local folder…')