Browse Source

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.
imccyu 2 months ago
parent
commit
40af20cafe
33 changed files with 406 additions and 135 deletions
  1. 4 1
      apps/cli/tests/web-agent-presets.e2e.ts
  2. 2 0
      apps/web/tests/pin-browse-picker.overlay.yml
  3. 4 1
      apps/web/tests/scaffold.ts
  4. 2 0
      packages/bundle/web-app/package.json
  5. 72 0
      packages/client/ui-directory-picker-native/package.json
  6. 0 0
      packages/client/ui-directory-picker-native/src/client/flow.ts
  7. 0 0
      packages/client/ui-directory-picker-native/src/client/index.ts
  8. 10 0
      packages/client/ui-directory-picker-native/src/index.ts
  9. 31 0
      packages/client/ui-directory-picker-native/src/invariant.ts
  10. 0 0
      packages/client/ui-directory-picker-native/tests/client-flow.spec.tsx
  11. 24 0
      packages/client/ui-directory-picker-native/tsconfig.json
  12. 3 0
      packages/client/ui-directory-picker-native/tsdown.config.ts
  13. 80 0
      packages/client/ui-directory-picker/package.json
  14. 0 0
      packages/client/ui-directory-picker/src/client/DirectoryBrowser.module.css
  15. 0 0
      packages/client/ui-directory-picker/src/client/DirectoryBrowser.tsx
  16. 0 0
      packages/client/ui-directory-picker/src/client/flow.ts
  17. 0 0
      packages/client/ui-directory-picker/src/client/index.ts
  18. 0 0
      packages/client/ui-directory-picker/src/css-modules.d.ts
  19. 10 0
      packages/client/ui-directory-picker/src/index.ts
  20. 31 0
      packages/client/ui-directory-picker/src/invariant.ts
  21. 0 0
      packages/client/ui-directory-picker/tests/client-flow.spec.tsx
  22. 0 0
      packages/client/ui-directory-picker/tests/directory-browser.spec.tsx
  23. 30 0
      packages/client/ui-directory-picker/tsconfig.json
  24. 3 0
      packages/client/ui-directory-picker/tsdown.config.ts
  25. 8 4
      packages/host/directory-picker-auto/package.json
  26. 38 21
      packages/host/directory-picker-auto/src/index.ts
  27. 2 32
      packages/host/directory-picker-browse/package.json
  28. 1 16
      packages/host/directory-picker-browse/tsconfig.json
  29. 14 2
      packages/host/directory-picker-browse/tsdown.config.ts
  30. 1 24
      packages/host/directory-picker-native/package.json
  31. 1 10
      packages/host/directory-picker-native/tsconfig.json
  32. 28 20
      packages/host/directory-picker-native/tsdown.config.ts
  33. 7 4
      scripts/verify-cordis-config.ts

+ 4 - 1
apps/cli/tests/web-agent-presets.e2e.ts

@@ -86,7 +86,10 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
     // host and so waits for the webserver disabled above; the browse variant
     // supplies `directoryPicker` without one.
     { id: 'directory-picker', disabled: true },
-    { insert: [{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' }] },
+    { insert: [
+      { id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' },
+      { id: 'ui-directory-picker', name: '@deepseek-ai/dsh-client-ui-directory-picker' },
+    ] },
     // The roster AppCLIEntry would patch in; only the shipped root, so a
     // developer's own `~/.dsh/.preset` cannot change this test's outcome.
     // `default` here is the COMPOSITION default — the base layer the settings

+ 2 - 0
apps/web/tests/pin-browse-picker.overlay.yml

@@ -9,3 +9,5 @@
 - insert:
     - id: directory-picker-browse
       name: '@deepseek-ai/dsh-host-directory-picker-browse'
+    - id: ui-directory-picker
+      name: '@deepseek-ai/dsh-client-ui-directory-picker'

+ 4 - 1
apps/web/tests/scaffold.ts

@@ -439,7 +439,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
     // host: patch `name` is an assertion, not an override, hence the
     // disable+insert pair.
     { id: 'directory-picker', disabled: true },
-    { insert: [{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' }] },
+    { insert: [
+      { id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' },
+      { id: 'ui-directory-picker', name: '@deepseek-ai/dsh-client-ui-directory-picker' },
+    ] },
     ...options.agentPresets === undefined
       ? []
       : [{ id: 'agent-presets', config: options.agentPresets }],

+ 2 - 0
packages/bundle/web-app/package.json

@@ -56,6 +56,8 @@
     "@deepseek-ai/dsh-client-ui-command": "workspace:^",
     "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
     "@deepseek-ai/dsh-client-ui-deliverables": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-directory-picker": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-directory-picker-native": "workspace:^",
     "@deepseek-ai/dsh-client-ui-goal": "workspace:^",
     "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
     "@deepseek-ai/dsh-client-ui-model": "workspace:^",

+ 72 - 0
packages/client/ui-directory-picker-native/package.json

@@ -0,0 +1,72 @@
+{
+  "name": "@deepseek-ai/dsh-client-ui-directory-picker-native",
+  "description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the host's OS chooser",
+  "version": "0.0.1-rc.1",
+  "publishConfig": {
+    "access": "restricted"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
+    "directory": "packages/client/ui-directory-picker-native"
+  },
+  "type": "module",
+  "main": "lib/index.js",
+  "types": "lib/types/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./lib/types/index.d.ts",
+      "default": "./lib/index.js"
+    },
+    "./invariant": {
+      "types": "./lib/types/invariant.d.ts",
+      "default": "./lib/invariant.js"
+    },
+    "./client": {
+      "types": "./lib/types/client/index.d.ts",
+      "default": "./lib/client.js"
+    },
+    "./src/*": "./src/*",
+    "./package.json": "./package.json"
+  },
+  "dsh": {
+    "client": {
+      "inject": [
+        "@deepseek-ai/dsh-client-runtime",
+        "@deepseek-ai/dsh-client-ui-workspace"
+      ],
+      "platform": "web"
+    }
+  },
+  "scripts": {
+    "bundle": "tsdown",
+    "watch": "tsdown --watch"
+  },
+  "license": "BSD-3-Clause",
+  "peerDependencies": {
+    "@deepseek-ai/dsh-client-runtime": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
+    "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@deepseek-ai/cordis": "workspace:^",
+    "react": "^18.2.0"
+  },
+  "devDependencies": {
+    "@deepseek-ai/dsh-client-runtime": "workspace:^",
+    "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
+    "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@testing-library/react": "^16.1.0",
+    "@types/react": "~18.3.1",
+    "@deepseek-ai/cordis": "workspace:^",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
+  },
+  "files": [
+    "lib/index.js",
+    "lib/invariant.js",
+    "lib/client.js",
+    "lib/types/**/*.d.ts"
+  ]
+}

+ 0 - 0
packages/host/directory-picker-native/src/client/flow.ts → packages/client/ui-directory-picker-native/src/client/flow.ts


+ 0 - 0
packages/host/directory-picker-native/src/client/index.ts → packages/client/ui-directory-picker-native/src/client/index.ts


+ 10 - 0
packages/client/ui-directory-picker-native/src/index.ts

@@ -0,0 +1,10 @@
+/**
+ * Native directory-picker surface, node half. Pure UI plugin: the empty apply
+ * exists so the plugin appears in the host cordis.yml / Loader; the browser
+ * half ships via exports["./client"], discovered through the package.json
+ * dsh.client declaration. The OS chooser it drives lives in
+ * `@deepseek-ai/dsh-host-directory-picker-native`.
+ */
+
+/** Host plugin body — no host-side behavior for this surface plugin. */
+export function apply(): void {}

+ 31 - 0
packages/client/ui-directory-picker-native/src/invariant.ts

@@ -0,0 +1,31 @@
+/**
+ * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-directory-picker-native`.
+ * @module @deepseek-ai/dsh-client-ui-directory-picker-native/invariant
+ */
+
+/* jscpd:ignore-start */
+import type { Context } from '@deepseek-ai/cordis'
+import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
+
+const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-directory-picker-native'
+
+/** Cordis companion plugin name. */
+export const name = 'client-ui-directory-picker-native-invariant'
+/** Service required before the companion can reserve package ownership. */
+export const inject = ['invariants']
+
+/**
+ * No runtime invariant: the plugin registers a renderless flow occupant into
+ * two workspace holes as one transactional effect, whose disposal the
+ * HMR-safety spec proves, and it retains no state between picks.
+ */
+const install: InvariantInstaller = () => {}
+
+/**
+ * Register this package's invariant companion.
+ * @param ctx - Cordis context carrying the invariant service.
+ * @returns the installed registration's disposer after setup succeeds.
+ */
+export const apply = (ctx: Context): Promise<() => void> =>
+  Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
+/* jscpd:ignore-end */

+ 0 - 0
packages/host/directory-picker-native/tests/client-flow.spec.tsx → packages/client/ui-directory-picker-native/tests/client-flow.spec.tsx


+ 24 - 0
packages/client/ui-directory-picker-native/tsconfig.json

@@ -0,0 +1,24 @@
+{
+  "extends": "../../../tsconfig.base.client.json",
+  "compilerOptions": {
+    "rootDir": "src",
+    "outDir": "lib/types"
+  },
+  "include": [
+    "src"
+  ],
+  "references": [
+    {
+      "path": "../../support/invariants"
+    },
+    {
+      "path": "../ui-slots"
+    },
+    {
+      "path": "../runtime"
+    },
+    {
+      "path": "../ui-workspace"
+    }
+  ]
+}

+ 3 - 0
packages/client/ui-directory-picker-native/tsdown.config.ts

@@ -0,0 +1,3 @@
+import { clientBundle } from '../tsdown.client.ts'
+
+export default clientBundle('@deepseek-ai/dsh-client-ui-directory-picker-native', ['lib/types/index.js', 'lib/types/invariant.js'])

+ 80 - 0
packages/client/ui-directory-picker/package.json

@@ -0,0 +1,80 @@
+{
+  "name": "@deepseek-ai/dsh-client-ui-directory-picker",
+  "description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives",
+  "version": "0.0.1-rc.1",
+  "publishConfig": {
+    "access": "restricted"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
+    "directory": "packages/client/ui-directory-picker"
+  },
+  "type": "module",
+  "main": "lib/index.js",
+  "types": "lib/types/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./lib/types/index.d.ts",
+      "default": "./lib/index.js"
+    },
+    "./invariant": {
+      "types": "./lib/types/invariant.d.ts",
+      "default": "./lib/invariant.js"
+    },
+    "./client": {
+      "types": "./lib/types/client/index.d.ts",
+      "default": "./lib/client.js"
+    },
+    "./src/*": "./src/*",
+    "./package.json": "./package.json"
+  },
+  "dsh": {
+    "client": {
+      "inject": [
+        "@deepseek-ai/dsh-client-runtime",
+        "@deepseek-ai/dsh-client-ui-workspace",
+        "@deepseek-ai/dsh-client-locale"
+      ],
+      "platform": "web"
+    }
+  },
+  "scripts": {
+    "bundle": "tsdown",
+    "watch": "tsdown --watch"
+  },
+  "license": "BSD-3-Clause",
+  "dependencies": {
+    "clsx": "^2.0.0"
+  },
+  "peerDependencies": {
+    "@deepseek-ai/dsh-client-locale": "workspace:^",
+    "@deepseek-ai/dsh-client-runtime": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
+    "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@deepseek-ai/cordis": "workspace:^",
+    "react": "^18.2.0"
+  },
+  "devDependencies": {
+    "@deepseek-ai/dsh-client-locale": "workspace:^",
+    "@deepseek-ai/dsh-client-runtime": "workspace:^",
+    "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
+    "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@testing-library/react": "^16.1.0",
+    "@types/react": "~18.3.1",
+    "@deepseek-ai/cordis": "workspace:^",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
+  },
+  "files": [
+    "lib/index.js",
+    "lib/invariant.js",
+    "lib/client.js",
+    "lib/types/**/*.d.ts"
+  ]
+}

+ 0 - 0
packages/host/directory-picker-browse/src/client/DirectoryBrowser.module.css → packages/client/ui-directory-picker/src/client/DirectoryBrowser.module.css


+ 0 - 0
packages/host/directory-picker-browse/src/client/DirectoryBrowser.tsx → packages/client/ui-directory-picker/src/client/DirectoryBrowser.tsx


+ 0 - 0
packages/host/directory-picker-browse/src/client/flow.ts → packages/client/ui-directory-picker/src/client/flow.ts


+ 0 - 0
packages/host/directory-picker-browse/src/client/index.ts → packages/client/ui-directory-picker/src/client/index.ts


+ 0 - 0
packages/host/directory-picker-browse/src/css-modules.d.ts → packages/client/ui-directory-picker/src/css-modules.d.ts


+ 10 - 0
packages/client/ui-directory-picker/src/index.ts

@@ -0,0 +1,10 @@
+/**
+ * Directory-picker browsing surface, node half. Pure UI plugin: the empty
+ * apply exists so the plugin appears in the host cordis.yml / Loader; the
+ * browser half ships via exports["./client"], discovered through the
+ * package.json dsh.client declaration. The listing and creation primitives it
+ * drives live in `@deepseek-ai/dsh-host-directory-picker-browse`.
+ */
+
+/** Host plugin body — no host-side behavior for this surface plugin. */
+export function apply(): void {}

+ 31 - 0
packages/client/ui-directory-picker/src/invariant.ts

@@ -0,0 +1,31 @@
+/**
+ * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-directory-picker`.
+ * @module @deepseek-ai/dsh-client-ui-directory-picker/invariant
+ */
+
+/* jscpd:ignore-start */
+import type { Context } from '@deepseek-ai/cordis'
+import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
+
+const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-directory-picker'
+
+/** Cordis companion plugin name. */
+export const name = 'client-ui-directory-picker-invariant'
+/** Service required before the companion can reserve package ownership. */
+export const inject = ['invariants']
+
+/**
+ * No runtime invariant: the plugin registers one workspace directory-flow
+ * owner whose disposal the HMR-safety spec proves, and every listing it shows
+ * is re-read from the Host on demand rather than held here.
+ */
+const install: InvariantInstaller = () => {}
+
+/**
+ * Register this package's invariant companion.
+ * @param ctx - Cordis context carrying the invariant service.
+ * @returns the installed registration's disposer after setup succeeds.
+ */
+export const apply = (ctx: Context): Promise<() => void> =>
+  Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
+/* jscpd:ignore-end */

+ 0 - 0
packages/host/directory-picker-browse/tests/client-flow.spec.tsx → packages/client/ui-directory-picker/tests/client-flow.spec.tsx


+ 0 - 0
packages/host/directory-picker-browse/tests/directory-browser.spec.tsx → packages/client/ui-directory-picker/tests/directory-browser.spec.tsx


+ 30 - 0
packages/client/ui-directory-picker/tsconfig.json

@@ -0,0 +1,30 @@
+{
+  "extends": "../../../tsconfig.base.client.json",
+  "compilerOptions": {
+    "rootDir": "src",
+    "outDir": "lib/types"
+  },
+  "include": [
+    "src"
+  ],
+  "references": [
+    {
+      "path": "../../support/invariants"
+    },
+    {
+      "path": "../ui-slots"
+    },
+    {
+      "path": "../ui-primitives"
+    },
+    {
+      "path": "../locale"
+    },
+    {
+      "path": "../runtime"
+    },
+    {
+      "path": "../ui-workspace"
+    }
+  ]
+}

+ 3 - 0
packages/client/ui-directory-picker/tsdown.config.ts

@@ -0,0 +1,3 @@
+import { clientBundle } from '../tsdown.client.ts'
+
+export default clientBundle('@deepseek-ai/dsh-client-ui-directory-picker', ['lib/types/index.js', 'lib/types/invariant.js'])

+ 8 - 4
packages/host/directory-picker-auto/package.json

@@ -32,21 +32,25 @@
   ],
   "license": "BSD-3-Clause",
   "peerDependencies": {
+    "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-directory-picker": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-directory-picker-native": "workspace:^",
     "@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^",
     "@deepseek-ai/dsh-host-directory-picker-native": "workspace:^",
     "@deepseek-ai/dsh-host-webserver": "workspace:^",
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/dsh-invariants": "workspace:^"
   },
   "devDependencies": {
+    "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/cordis-plugin-include": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-directory-picker": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-directory-picker-native": "workspace:^",
     "@deepseek-ai/dsh-host-directory-picker": "workspace:^",
     "@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^",
     "@deepseek-ai/dsh-host-directory-picker-native": "workspace:^",
     "@deepseek-ai/dsh-host-webserver": "workspace:^",
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/dsh-invariants": "workspace:^"
   }
 }

+ 38 - 21
packages/host/directory-picker-auto/src/index.ts

@@ -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] })
+    // 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 }))
+    }
     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)
+      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: backend entry')
+  }, 'directory-picker-auto: interaction entries')
 }

+ 2 - 32
packages/host/directory-picker-browse/package.json

@@ -22,55 +22,25 @@
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"
     },
-    "./client": {
-      "types": "./lib/types/client/index.d.ts",
-      "default": "./lib/client.js"
-    },
     "./src/*": "./src/*",
     "./package.json": "./package.json"
   },
   "files": [
     "lib/index.js",
     "lib/invariant.js",
-    "lib/client.js",
     "lib/types/**/*.d.ts"
   ],
   "license": "BSD-3-Clause",
   "dependencies": {
     "@deepseek-ai/dsh-host-directory-picker": "workspace:^",
-    "clsx": "^2.0.0",
     "@deepseek-ai/schemastery": "workspace:^"
   },
   "peerDependencies": {
-    "@deepseek-ai/dsh-client-locale": "workspace:^",
-    "@deepseek-ai/dsh-client-runtime": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
-    "@deepseek-ai/dsh-client-locale": "workspace:^",
-    "@deepseek-ai/dsh-client-runtime": "workspace:^",
-    "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@types/react": "~18.3.1",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
-  },
-  "dsh": {
-    "client": {
-      "inject": [
-        "@deepseek-ai/dsh-client-runtime",
-        "@deepseek-ai/dsh-client-ui-workspace",
-        "@deepseek-ai/dsh-client-locale"
-      ],
-      "platform": "web"
-    }
+    "@deepseek-ai/cordis": "workspace:^"
   }
 }

+ 1 - 16
packages/host/directory-picker-browse/tsconfig.json

@@ -1,5 +1,5 @@
 {
-  "extends": "../../../tsconfig.base.client.json",
+  "extends": "../../../tsconfig.base.json",
   "compilerOptions": {
     "rootDir": "src",
     "outDir": "lib/types",
@@ -16,21 +16,6 @@
     },
     {
       "path": "../../support/invariants"
-    },
-    {
-      "path": "../../client/ui-slots"
-    },
-    {
-      "path": "../../client/ui-primitives"
-    },
-    {
-      "path": "../../client/locale"
-    },
-    {
-      "path": "../../client/runtime"
-    },
-    {
-      "path": "../../client/ui-workspace"
     }
   ]
 }

+ 14 - 2
packages/host/directory-picker-browse/tsdown.config.ts

@@ -1,3 +1,15 @@
-import { clientBundle } from '../../client/tsdown.client.ts'
+import { defineConfig } from 'tsdown'
 
-export default clientBundle('@deepseek-ai/dsh-host-directory-picker-browse', ['lib/types/index.js', 'lib/types/invariant.js'])
+/** Node-only backend: listing and creation primitives over the host filesystem. */
+export default defineConfig([
+  {
+    entry: ['lib/types/index.js', 'lib/types/invariant.js'],
+    outDir: 'lib',
+    format: ['esm'],
+    platform: 'node',
+    target: 'es2024',
+    fixedExtension: false,
+    dts: false,
+    clean: false,
+  },
+])

+ 1 - 24
packages/host/directory-picker-native/package.json

@@ -22,10 +22,6 @@
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"
     },
-    "./client": {
-      "types": "./lib/types/client/index.d.ts",
-      "default": "./lib/client.js"
-    },
     "./worker": {
       "types": "./lib/types/win32-dialog-worker.d.ts",
       "default": "./lib/worker.cjs"
@@ -37,7 +33,6 @@
     "lib/index.js",
     "lib/invariant.js",
     "lib/worker.cjs",
-    "lib/client.js",
     "lib/types/**/*.d.ts"
   ],
   "license": "BSD-3-Clause",
@@ -47,30 +42,12 @@
     "koffi": "^3.1.0"
   },
   "peerDependencies": {
-    "@deepseek-ai/dsh-client-runtime": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
-    "@deepseek-ai/dsh-client-runtime": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@types/react": "~18.3.1",
     "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0",
     "tsx": "^4.19.2"
-  },
-  "dsh": {
-    "client": {
-      "inject": [
-        "@deepseek-ai/dsh-client-runtime",
-        "@deepseek-ai/dsh-client-ui-workspace"
-      ],
-      "platform": "web"
-    }
   }
 }

+ 1 - 10
packages/host/directory-picker-native/tsconfig.json

@@ -1,5 +1,5 @@
 {
-  "extends": "../../../tsconfig.base.client.json",
+  "extends": "../../../tsconfig.base.json",
   "compilerOptions": {
     "rootDir": "src",
     "outDir": "lib/types",
@@ -19,15 +19,6 @@
     },
     {
       "path": "../../util/native-command"
-    },
-    {
-      "path": "../../client/ui-slots"
-    },
-    {
-      "path": "../../client/runtime"
-    },
-    {
-      "path": "../../client/ui-workspace"
     }
   ]
 }

+ 28 - 20
packages/host/directory-picker-native/tsdown.config.ts

@@ -1,23 +1,31 @@
-import { clientBundle } from '../../client/tsdown.client.ts'
+import { defineConfig } from 'tsdown'
 
-// The Win32 dialog worker builds as its own CJS entry (mirroring
-// dsh-workflow-workerthread's worker): path-loaded by the driver, inlining
-// the dialog logic while koffi stays an external native require.
-export default clientBundle(
-  '@deepseek-ai/dsh-host-directory-picker-native',
-  ['lib/types/index.js', 'lib/types/invariant.js'],
+/**
+ * Node-only backend. The Win32 dialog worker builds as its own CJS entry
+ * (mirroring dsh-workflow-workerthread's worker): path-loaded by the driver,
+ * inlining the dialog logic while koffi stays an external native require.
+ */
+export default defineConfig([
   {
-    companions: [{
-      // The artifact is lib/worker.cjs (the ./worker export the workspace
-      // constraint keys on), bundled from the descriptive source entry.
-      entry: { worker: 'lib/types/win32-dialog-worker.js' },
-      outDir: 'lib',
-      format: ['cjs'],
-      platform: 'node',
-      target: 'es2024',
-      fixedExtension: false,
-      dts: false,
-      clean: false,
-    }],
+    entry: ['lib/types/index.js', 'lib/types/invariant.js'],
+    outDir: 'lib',
+    format: ['esm'],
+    platform: 'node',
+    target: 'es2024',
+    fixedExtension: false,
+    dts: false,
+    clean: false,
   },
-)
+  {
+    // The artifact is lib/worker.cjs (the ./worker export the workspace
+    // constraint keys on), bundled from the descriptive source entry.
+    entry: { worker: 'lib/types/win32-dialog-worker.js' },
+    outDir: 'lib',
+    format: ['cjs'],
+    platform: 'node',
+    target: 'es2024',
+    fixedExtension: false,
+    dts: false,
+    clean: false,
+  },
+])

+ 7 - 4
scripts/verify-cordis-config.ts

@@ -42,14 +42,17 @@ const metadataFields = ['id', 'name', 'group', 'disabled', 'inject', 'intercept'
 const CHOOSER_PACKAGE = '@deepseek-ai/dsh-host-directory-picker-auto'
 
 /**
- * The backends the chooser mounts by runtime string (mirror of its exported
- * `BACKEND_PACKAGES`), invisible to yml-row scanning: a composition mounting
- * the chooser must resolve both, or keyless Linux CI (which only ever
- * resolves `browse`) hides a dropped `-native` dependency until a macOS boot.
+ * The packages the chooser mounts by runtime string (mirror of its exported
+ * `BACKEND_PACKAGES` and `SURFACE_PACKAGES`), invisible to yml-row scanning: a
+ * composition mounting the chooser must resolve every one, or keyless Linux CI
+ * (which only ever resolves `browse`) hides a dropped `-native` dependency
+ * until a macOS boot.
  */
 const CHOOSER_BACKEND_PACKAGES = [
   '@deepseek-ai/dsh-host-directory-picker-native',
   '@deepseek-ai/dsh-host-directory-picker-browse',
+  '@deepseek-ai/dsh-client-ui-directory-picker',
+  '@deepseek-ai/dsh-client-ui-directory-picker-native',
 ]
 const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
   kind: 'scalar',