refactor(picker): split the directory-picker faces into their own packages

The browse and native backends were dual-face packages: a Node backend plus a
browser surface under one tsconfig that referenced Client packages. That put
Client projects — and through them the Client runtime — inside the Host
compiler aggregate, which builds before the generated Remote contributions
exist. Each browser half moves to its own Client package, and both backends
become Node-only.

The interaction is still one choice: the adaptive chooser mounts the backend
and its surface as a pair of Loader entries and tears both down in reverse, so
a resolved kind still swaps both faces. Compositions that pin an interaction
directly now pin the pair, and the chooser's runtime-string package list keeps
naming everything a composing app must resolve.
This commit is contained in:
imccyu
2026-08-11 19:10:50 +08:00
parent 070a2a7f1e
commit 40af20cafe
33 changed files with 407 additions and 136 deletions

View File

@@ -1,12 +1,13 @@
/**
* Adaptive chooser of the directory-picker seam: resolves the host's
* situation once at boot (bind host, SSH launch, display session, Linux
* chooser binary) and mounts the matching dual-face backend — `-native` or
* `-browse` — as a real Loader entry in the in-memory root tree. Because the
* backend arrives as an ordinary entry, its browser half is discovered
* exactly as a config-row's would be, so the seam's one-row-swaps-both-faces
* invariant holds for the resolved choice; pinning an interaction remains
* composing that backend row directly instead of this one.
* chooser binary) and mounts the matching interaction — `native` or `browse`
* — as real Loader entries in the in-memory root tree. Each interaction is a
* pair: the Host backend serving the seam capability and the client surface
* occupying ui-workspace's directory-flow holes. Both arrive as ordinary
* entries, so the surface is discovered exactly as a config-row's would be
* and one resolved choice still swaps both faces; pinning an interaction
* remains composing that pair directly instead of this row.
* @module @deepseek-ai/dsh-host-directory-picker-auto
*/
@@ -28,7 +29,7 @@ export const name = 'directory-picker-auto'
export const inject = ['httpServer', 'loader']
/**
* Backend package per resolved kind — fixed composition vocabulary, not a
* Host backend package per resolved kind — fixed composition vocabulary, not a
* tunable. Exported because the reference is a runtime string the static
* config gate cannot see in a yml row: `verify-cordis-config` requires every
* app composing this chooser to declare both values as dependencies.
@@ -39,10 +40,20 @@ export const BACKEND_PACKAGES: Record<DirectoryPickerBackendKind, string> = {
}
/**
* Resolve the backend from one boot-time sample and mount it as a Loader
* entry; the effect's disposer removes the entry and joins the backend
* fiber's teardown, so unloading this plugin returns only after both faces
* of the mounted backend (and their dependents) quiesced.
* Client surface package per resolved kind, mounted with its backend so one
* resolved interaction still composes both faces. Declared as dependencies by
* every composing app for the same reason as {@link BACKEND_PACKAGES}.
*/
export const SURFACE_PACKAGES: Record<DirectoryPickerBackendKind, string> = {
native: '@deepseek-ai/dsh-client-ui-directory-picker-native',
browse: '@deepseek-ai/dsh-client-ui-directory-picker',
}
/**
* Resolve the interaction from one boot-time sample and mount its backend and
* surface as Loader entries; the effect's disposer removes both entries and
* joins their fibers' teardown, so unloading this plugin returns only after
* both faces of the mounted interaction (and their dependents) quiesced.
* @param ctx - cordis context carrying the injected `httpServer` and `loader`.
*/
export async function apply(ctx: Context): Promise<void> {
@@ -54,16 +65,22 @@ export async function apply(ctx: Context): Promise<void> {
})
await ctx.effect(async () => {
// Root-tree create: the Loader root is in-memory (write() is a no-op), so
// the mounted row can never be persisted back into a config file.
const id = await ctx.loader.create({ name: BACKEND_PACKAGES[backend] })
return async () => {
// Tree teardown (group.stop) can have removed the entry already;
// nothing is left to unmount or await then.
const entry = ctx.loader.store[id]
if (entry === undefined) return
// remove() disposes the entry transactionally, so the chooser's unload
// signals completion only after the backend quiesced.
await ctx.loader.remove(id)
// the mounted rows can never be persisted back into a config file. The
// backend lands first: the surface's browser half drives the capability
// the backend registers.
const ids: string[] = []
for (const name of [BACKEND_PACKAGES[backend], SURFACE_PACKAGES[backend]]) {
ids.push(await ctx.loader.create({ name }))
}
}, 'directory-picker-auto: backend entry')
return async () => {
for (const id of ids.reverse()) {
// Tree teardown (group.stop) can have removed the entry already;
// nothing is left to unmount or await then.
if (ctx.loader.store[id] === undefined) continue
// remove() disposes the entry transactionally, so the chooser's unload
// signals completion only after that face quiesced.
await ctx.loader.remove(id)
}
}
}, 'directory-picker-auto: interaction entries')
}