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()
|
||||
|
||||
@@ -1594,8 +1594,12 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
// TODO: the create-by-name branch lost its last product consumer when
|
||||
// the Web picker collapsed onto the directory flow
|
||||
// (.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.md).
|
||||
// Delete it with the wire schema's `name` member, this `defaults.workspaceRoot`,
|
||||
// and the `dsh web --workspace-root` flag that exists only to feed it.
|
||||
// Delete it with the wire schema's `name` member, this
|
||||
// `defaults.workspaceRoot`, the client seam that carried the name
|
||||
// (`WorkspaceCreateInput`, `WorkspacesService.create`'s `{ name }` arm,
|
||||
// `intentName`'s name branch, the manager's "name under workspaceRoot"
|
||||
// contract), and the `dsh web --workspace-root` flag plus its apps/cli
|
||||
// README lines, which exist only to feed it.
|
||||
async create(request) {
|
||||
const { payload } = request
|
||||
let path: string
|
||||
|
||||
Reference in New Issue
Block a user