fix(web): address the review round on the one-route add flow
- lifecycle-chrome's second scaffold staged its workspace under the OUTER scaffold's temp root, coupling two supposedly independent worlds and leaving the aria scrub root wrong; it now uses its own workspaceCwd. - The direct-open path now carries the same `flowBusy` gate that disables the equivalent menu entry, so an occupant re-registering mid-adoption cannot raise a second flow. - A composition with no directory-picker no longer opens a zero-entry popover on the hero anchor: with nothing to pick and nothing to add, the gesture shows nothing. Both behaviors gain a unit test. - Brought three partially superseded Agent Notes current (the native picker, the workspace UI product flow, and the sidebar browsing split), cross-linked to this decision, both languages re-recorded. - Corrected this Note's own Testing section: the shared e2e helper stages and adopts its directory, it does not create one in-dialog — only workspace-management does. Named the client-seam and CLI-README residue in the follow-up TODO alongside the wire branch.
This commit is contained in:
@@ -114,6 +114,10 @@ export function WorkspacePickFlow({
|
||||
disabled: flowBusy,
|
||||
}))
|
||||
: addEntries
|
||||
// Nothing listed and nothing to add with (a composition that mounts this
|
||||
// package without any directory-picker): an empty popover would claim a
|
||||
// choice that does not exist, so the anchor gesture shows nothing at all.
|
||||
const menuIsEmpty = items.length === 0
|
||||
|
||||
const closeModal = (): void => {
|
||||
setErrorOpen(false)
|
||||
@@ -153,9 +157,11 @@ export function WorkspacePickFlow({
|
||||
// made unnecessary; the add-only surface lists nothing and never waits.
|
||||
const listSettled = addOnly || workspaceSnapshot.phase === 'ready'
|
||||
const addIsTheOnlyEntry = !pinAdd && listSettled && addEntries.length === 1
|
||||
// `flowBusy` gates this exactly as it disables the equivalent menu entry: a
|
||||
// pick still being adopted owns the surface until it settles.
|
||||
useEffect(() => {
|
||||
if (open && addIsTheOnlyEntry) openDirectoryFlow()
|
||||
}, [open, addIsTheOnlyEntry, openDirectoryFlow])
|
||||
if (open && addIsTheOnlyEntry && !flowBusy) openDirectoryFlow()
|
||||
}, [open, addIsTheOnlyEntry, flowBusy, openDirectoryFlow])
|
||||
|
||||
/** Owner side of the flow conversation: adopt keeps the flow open (busy) until the Host answers. */
|
||||
const flowOwner: DirectoryFlowOwnerProps = {
|
||||
@@ -185,7 +191,7 @@ export function WorkspacePickFlow({
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
open={open && !addIsTheOnlyEntry}
|
||||
open={open && !addIsTheOnlyEntry && !menuIsEmpty}
|
||||
anchor={null}
|
||||
items={items}
|
||||
{...pinAdd ? { footer: addEntries } : {}}
|
||||
@@ -196,7 +202,7 @@ export function WorkspacePickFlow({
|
||||
portal
|
||||
getAnchorRect={getAnchorRect}
|
||||
/>
|
||||
{open && !addIsTheOnlyEntry && workspaceSnapshot.phase === 'pending' && <div className={css.menuStatus} role="status">{t('picker.loading')}</div>}
|
||||
{open && !addIsTheOnlyEntry && !menuIsEmpty && workspaceSnapshot.phase === 'pending' && <div className={css.menuStatus} role="status">{t('picker.loading')}</div>}
|
||||
{renderDirectoryFlow(flowOwner)}
|
||||
<Modal
|
||||
open={errorOpen}
|
||||
|
||||
@@ -253,6 +253,34 @@ describe('WorkspacePicker', () => {
|
||||
expect(screen.getByRole('menuitem', { name: '添加工作区…' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows no popover at all when nothing is listed and nothing can be added', () => {
|
||||
// A composition mounting this package without any directory-picker: the
|
||||
// hero anchor has neither a Workspace to pick nor a way to add one, so it
|
||||
// must not claim a choice with an empty menu.
|
||||
const b = mount([], vi.fn(), occupancySource(false))
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
expect(screen.queryByTestId('directory-flow')).toBeNull()
|
||||
expect(b.createWorkspace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('holds the anchor gesture while an adoption is still settling', async () => {
|
||||
// The auto-open path obeys the same busy rule as the disabled menu entry:
|
||||
// an occupant that re-registers mid-adoption must not raise a second flow.
|
||||
let resolve!: (workspace: WorkspaceView) => void
|
||||
const pending = new Promise<WorkspaceView>((settle) => { resolve = settle })
|
||||
const created = workspace('adopted')
|
||||
const b = mount([workspace('alpha', 'Alpha')], vi.fn(() => pending))
|
||||
chooseAdd()
|
||||
act(() => { b.probe.owner!.onPicked('/tmp/project') })
|
||||
expect(b.probe.owner!.busy).toBe(true)
|
||||
// The list empties under the still-settling adoption (the workspace was
|
||||
// deleted elsewhere), which would otherwise make add the only entry.
|
||||
act(() => { b.rerenderItems([]) })
|
||||
expect(b.createWorkspace).toHaveBeenCalledTimes(1)
|
||||
await act(async () => { resolve(created); await pending })
|
||||
expect(b.probe.owner!.busy).toBe(false)
|
||||
})
|
||||
|
||||
it('hides the add entry while the directory-flow hole is empty', () => {
|
||||
mount([workspace('alpha', 'Alpha')], vi.fn(), occupancySource(false))
|
||||
expect(screen.getByRole('menuitem', { name: 'Alpha' })).toBeTruthy()
|
||||
|
||||
Reference in New Issue
Block a user