fix(host,client): default-export the picker seam; invalidate stale kind reads
ds-review-bot round 3. The seam package broke the service-package export contract (named export only), so the config catalog filed it under Other libraries and default imports failed; it now default-exports DirectoryPicker like every abstract seam, and the regenerated catalog lists it as one. The picker-kind effect also let a settlement from a superseded flow open leak into the current one (close/reopen mid-describe, or a reconnect that swaps the backend): the read now resets the affordance on every open and a cleanup-toggled flag discards obsolete settlements, both directions pinned by jsdom races.
This commit is contained in:
@@ -2211,6 +2211,7 @@ Abstract service classes — a deployment loads a concrete implementation packag
|
|||||||
- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
|
- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-compact` — abstract `CompactService` ([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts))
|
- `@deepseek-ai/dsh-compact` — abstract `CompactService` ([`packages/compact/compact/src/index.ts`](../packages/compact/compact/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
|
- `@deepseek-ai/dsh-fs` — abstract `FileSystem` ([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
|
||||||
|
- `@deepseek-ai/dsh-host-directory-picker` — abstract `DirectoryPicker` ([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-sandbox` — abstract `SandboxProvider` ([`packages/sandbox/sandbox/src/index.ts`](../packages/sandbox/sandbox/src/index.ts))
|
- `@deepseek-ai/dsh-sandbox` — abstract `SandboxProvider` ([`packages/sandbox/sandbox/src/index.ts`](../packages/sandbox/sandbox/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-session-persistence` — abstract `SessionPersistence` ([`packages/session-persistence/session-persistence/src/index.ts`](../packages/session-persistence/session-persistence/src/index.ts))
|
- `@deepseek-ai/dsh-session-persistence` — abstract `SessionPersistence` ([`packages/session-persistence/session-persistence/src/index.ts`](../packages/session-persistence/session-persistence/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-session-query` — abstract `SessionQueryService` ([`packages/session-query/session-query/src/index.ts`](../packages/session-query/session-query/src/index.ts))
|
- `@deepseek-ai/dsh-session-query` — abstract `SessionQueryService` ([`packages/session-query/session-query/src/index.ts`](../packages/session-query/session-query/src/index.ts))
|
||||||
@@ -2234,7 +2235,6 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
|
|||||||
- `@deepseek-ai/dsh-client-web-react` ([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
|
- `@deepseek-ai/dsh-client-web-react` ([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-helper` ([`packages/sdk/helper/src/index.ts`](../packages/sdk/helper/src/index.ts))
|
- `@deepseek-ai/dsh-helper` ([`packages/sdk/helper/src/index.ts`](../packages/sdk/helper/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
|
- `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-host-directory-picker` ([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts))
|
|
||||||
- `@deepseek-ai/dsh-jsonrpc-demo` ([`packages/examples/jsonrpc-demo/src/index.ts`](../packages/examples/jsonrpc-demo/src/index.ts))
|
- `@deepseek-ai/dsh-jsonrpc-demo` ([`packages/examples/jsonrpc-demo/src/index.ts`](../packages/examples/jsonrpc-demo/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-llm-mock-server` ([`packages/support/llm-mock-server/src/index.ts`](../packages/support/llm-mock-server/src/index.ts))
|
- `@deepseek-ai/dsh-llm-mock-server` ([`packages/support/llm-mock-server/src/index.ts`](../packages/support/llm-mock-server/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-loader-smoke` ([`packages/support/loader-smoke/src/index.ts`](../packages/support/loader-smoke/src/index.ts))
|
- `@deepseek-ai/dsh-loader-smoke` ([`packages/support/loader-smoke/src/index.ts`](../packages/support/loader-smoke/src/index.ts))
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ Abstract directory-picking service. Subclass, implement `capability()`, and load
|
|||||||
abstract capability(): DirectoryPickerCapability
|
abstract capability(): DirectoryPickerCapability
|
||||||
```
|
```
|
||||||
|
|
||||||
Source: [`packages/host/directory-picker/src/index.ts:118`](../../packages/host/directory-picker/src/index.ts)
|
Source: [`packages/host/directory-picker/src/index.ts:120`](../../packages/host/directory-picker/src/index.ts)
|
||||||
|
|
||||||
## `ctx.fs` — `FileSystem` (abstract seam)
|
## `ctx.fs` — `FileSystem` (abstract seam)
|
||||||
|
|
||||||
|
|||||||
@@ -80,12 +80,18 @@ export function WorkspaceCreateFlow({
|
|||||||
const [dialogPicker, setDialogPicker] = useState(false)
|
const [dialogPicker, setDialogPicker] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
|
// Reset before each read: a reconnect can change the composed backend, so
|
||||||
|
// a previous open's answer must not leak into this one; and a settlement
|
||||||
|
// from a superseded open (flow closed, or a newer read started) is
|
||||||
|
// discarded via the cleanup-toggled flag.
|
||||||
|
setDialogPicker(false)
|
||||||
|
let stale = false
|
||||||
void directoryPickerKind()
|
void directoryPickerKind()
|
||||||
.then((kind) => { setDialogPicker(kind === 'dialog') })
|
.then((kind) => { if (!stale) setDialogPicker(kind === 'dialog') })
|
||||||
// A failed describe hides the entry too: the same Host that cannot
|
// A failed describe hides the entry too: the same Host that cannot
|
||||||
// answer describe cannot serve pickDirectory. (Post-unmount settlement
|
// answer describe cannot serve pickDirectory.
|
||||||
// is safe: React 18 no-ops setState on unmounted components.)
|
.catch(() => { if (!stale) setDialogPicker(false) })
|
||||||
.catch(() => { setDialogPicker(false) })
|
return () => { stale = true }
|
||||||
}, [open, directoryPickerKind])
|
}, [open, directoryPickerKind])
|
||||||
|
|
||||||
const items: MenuEntry[] = [
|
const items: MenuEntry[] = [
|
||||||
|
|||||||
@@ -268,4 +268,50 @@ describe('WorkspacePicker', () => {
|
|||||||
)
|
)
|
||||||
expect(directoryPickerKind).not.toHaveBeenCalled()
|
expect(directoryPickerKind).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** Render the picker with an owner-controlled `open` and a scripted kind read. */
|
||||||
|
function togglable(directoryPickerKind: () => Promise<string>) {
|
||||||
|
const anchorRef = anchor()
|
||||||
|
const props = (open: boolean) => (
|
||||||
|
<WorkspacePicker
|
||||||
|
open={open} anchorRef={anchorRef} useSessions={hook(sessions)} useWorkspaces={hook(workspaceState([]))}
|
||||||
|
onPick={vi.fn()} onClose={vi.fn()} createWorkspace={vi.fn()} pickDirectory={vi.fn()}
|
||||||
|
directoryPickerKind={directoryPickerKind}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const view = render(props(true))
|
||||||
|
return { setOpen: (open: boolean) => { view.rerender(props(open)) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
it('discards a kind settlement from a superseded flow open', async () => {
|
||||||
|
let resolveFirst!: (kind: string) => void
|
||||||
|
const first = new Promise<string>((settle) => { resolveFirst = settle })
|
||||||
|
const directoryPickerKind = vi.fn<() => Promise<string>>()
|
||||||
|
.mockImplementationOnce(() => first)
|
||||||
|
.mockImplementation(async () => 'browse')
|
||||||
|
const t = togglable(directoryPickerKind)
|
||||||
|
// Close while the first read is in flight, then let it answer 'dialog':
|
||||||
|
// the settlement is stale and must not leak into the next open.
|
||||||
|
t.setOpen(false)
|
||||||
|
await act(async () => { resolveFirst('dialog') })
|
||||||
|
t.setOpen(true)
|
||||||
|
await screen.findByRole('menuitem', { name: 'Create a new workspace' })
|
||||||
|
await waitFor(() => { expect(directoryPickerKind).toHaveBeenCalledTimes(2) })
|
||||||
|
expect(screen.queryByRole('menuitem', { name: 'Open local folder…' })).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('discards a stale describe failure after a newer open already answered', async () => {
|
||||||
|
let rejectFirst!: (reason: Error) => void
|
||||||
|
const first = new Promise<string>((_settle, reject) => { rejectFirst = reject })
|
||||||
|
const directoryPickerKind = vi.fn<() => Promise<string>>()
|
||||||
|
.mockImplementationOnce(() => first)
|
||||||
|
.mockImplementation(async () => 'dialog')
|
||||||
|
const t = togglable(directoryPickerKind)
|
||||||
|
t.setOpen(false)
|
||||||
|
t.setOpen(true)
|
||||||
|
await screen.findByRole('menuitem', { name: 'Open local folder…' })
|
||||||
|
// The superseded read failing late must not hide the freshly shown entry.
|
||||||
|
await act(async () => { rejectFirst(new Error('late loss')); await first.catch(() => {}) })
|
||||||
|
expect(screen.getByRole('menuitem', { name: 'Open local folder…' })).toBeTruthy()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -128,3 +128,5 @@ export abstract class DirectoryPicker extends Service {
|
|||||||
*/
|
*/
|
||||||
abstract capability(): DirectoryPickerCapability
|
abstract capability(): DirectoryPickerCapability
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default DirectoryPicker
|
||||||
|
|||||||
Reference in New Issue
Block a user