Переглянути джерело

Merge commit 'e25ae41263f1ce4066aa8352f76d3d6d077b7959' into schedule-web-catalog

# Conflicts:
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	packages/client/ui-workspace/package.json
pku-xht 1 місяць тому
батько
коміт
a5330ecf99
100 змінених файлів з 896 додано та 754 видалено
  1. 2 2
      .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.i18n.yaml
  2. 0 1
      .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md
  3. 0 1
      .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.zh.md
  4. 16 3
      apps/web/tests/preview-boot.e2e.ts
  5. 17 0
      apps/web/tests/settings-chrome.e2e.ts
  6. 2 2
      docs/capability-seams.i18n.yaml
  7. 5 2
      docs/capability-seams.md
  8. 5 2
      docs/capability-seams.zh.md
  9. 1 1
      docs/config-catalog.i18n.yaml
  10. 1 1
      docs/config-catalog.md
  11. 2 2
      docs/module-graph.i18n.yaml
  12. 9 5
      docs/module-graph.md
  13. 9 5
      docs/module-graph.zh.md
  14. 2 2
      docs/subsystems/workspace.i18n.yaml
  15. 34 0
      docs/subsystems/workspace.md
  16. 34 0
      docs/subsystems/workspace.zh.md
  17. 0 5
      knip.json
  18. 1 1
      packages/api/remotes/src/client/index.ts
  19. 0 17
      packages/api/session-controller/tests/fake-api.client.ts
  20. 2 2
      packages/api/workspace-controller/README.i18n.yaml
  21. 1 1
      packages/api/workspace-controller/README.md
  22. 1 1
      packages/api/workspace-controller/README.zh.md
  23. 2 0
      packages/api/workspace-controller/package.json
  24. 171 0
      packages/api/workspace-controller/src/directory-picker.ts
  25. 7 0
      packages/api/workspace-controller/src/index.ts
  26. 28 1
      packages/api/workspace-controller/src/types.ts
  27. 166 0
      packages/api/workspace-controller/tests/directory-picker.host.spec.ts
  28. 1 0
      packages/api/workspace-controller/tsconfig.client.json
  29. 2 0
      packages/api/workspace-controller/tsconfig.host.json
  30. 2 0
      packages/client/connection/package.json
  31. 0 1
      packages/client/connection/src/client/api.ts
  32. 56 39
      packages/client/connection/src/client/fixture.ts
  33. 0 1
      packages/client/connection/src/client/index.ts
  34. 0 17
      packages/client/connection/tests/fake-api.client.ts
  35. 26 10
      packages/client/connection/tests/fixture.client.spec.ts
  36. 2 2
      packages/client/connection/tests/http-bridge.host.spec.ts
  37. 2 2
      packages/client/connection/tests/node-half.host.spec.ts
  38. 3 0
      packages/client/connection/tsconfig.client.json
  39. 6 6
      packages/client/ui-chat/src/client/chat/ChatView.module.css
  40. 2 2
      packages/client/ui-chat/src/client/chat/ContextInjectionRow.module.css
  41. 1 1
      packages/client/ui-chat/src/client/chat/GenericCommandCard.module.css
  42. 11 11
      packages/client/ui-chat/src/client/chat/MessageItem.module.css
  43. 7 6
      packages/client/ui-chat/src/client/chat/ReasoningRow.module.css
  44. 5 4
      packages/client/ui-chat/src/client/chat/StatsLine.module.css
  45. 2 2
      packages/client/ui-chat/src/client/chat/TurnUsageDisclosure.module.css
  46. 7 7
      packages/client/ui-chat/tests/chat-font-axis-styles.client.spec.ts
  47. 3 3
      packages/client/ui-directory-picker-browse/package.json
  48. 1 1
      packages/client/ui-directory-picker-browse/src/client/DirectoryBrowser.tsx
  49. 1 1
      packages/client/ui-directory-picker-browse/src/client/flow.ts
  50. 5 4
      packages/client/ui-directory-picker-browse/src/client/index.ts
  51. 1 1
      packages/client/ui-directory-picker-browse/tests/client-flow.client.spec.tsx
  52. 1 1
      packages/client/ui-directory-picker-browse/tests/directory-browser.client.spec.tsx
  53. 1 1
      packages/client/ui-directory-picker-browse/tsconfig.json
  54. 1 1
      packages/client/ui-directory-picker-native/src/client/index.ts
  55. 1 1
      packages/client/ui-message-feedback/src/client/MessageFeedbackActions.module.css
  56. 7 5
      packages/client/ui-primitives/src/DisclosureRow.module.css
  57. 2 2
      packages/client/ui-primitives/tests/disclosure-row-styles.client.spec.ts
  58. 2 2
      packages/client/ui-theme/README.i18n.yaml
  59. 2 2
      packages/client/ui-theme/README.md
  60. 2 2
      packages/client/ui-theme/README.zh.md
  61. 14 8
      packages/client/ui-theme/src/styles/gradient-shadow-text.css
  62. 8 6
      packages/client/ui-tool/src/client/tool/components/ToolRow.module.css
  63. 2 2
      packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css
  64. 4 3
      packages/client/ui-tool/tests/tool-row-styles.client.spec.ts
  65. 16 13
      packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css
  66. 8 6
      packages/client/ui-workflow-run/tests/panel-styles.client.spec.ts
  67. 5 0
      packages/client/ui-workspace/package.json
  68. 5 2
      packages/client/ui-workspace/src/client/index.ts
  69. 15 15
      packages/client/ui-workspace/src/client/navigation.ts
  70. 7 2
      packages/client/ui-workspace/tests/apply.client.spec.ts
  71. 6 1
      packages/client/ui-workspace/tests/rename-assembly.client.spec.tsx
  72. 45 20
      packages/client/ui-workspace/tests/workspaces-service.client.spec.ts
  73. 3 0
      packages/client/ui-workspace/tsconfig.json
  74. 34 1
      packages/extensions/tool-cordis/src/api-catalog.ts
  75. 2 2
      packages/host/apiproxy/README.i18n.yaml
  76. 1 2
      packages/host/apiproxy/README.md
  77. 1 2
      packages/host/apiproxy/README.zh.md
  78. 0 75
      packages/host/apiproxy/src/api-proxy.ts
  79. 0 44
      packages/host/apiproxy/src/api/host.schema.ts
  80. 0 58
      packages/host/apiproxy/src/api/host.ts
  81. 1 1
      packages/host/apiproxy/src/api/index.ts
  82. 0 3
      packages/host/apiproxy/src/api/rpc-map.ts
  83. 0 4
      packages/host/apiproxy/src/api/rpc.schema.ts
  84. 0 4
      packages/host/apiproxy/src/api/rpc.ts
  85. 8 29
      packages/host/apiproxy/src/fetch/client.ts
  86. 1 6
      packages/host/apiproxy/src/fetch/handler.ts
  87. 1 0
      packages/host/apiproxy/src/index.ts
  88. 4 133
      packages/host/apiproxy/tests/api-proxy-host.spec.ts
  89. 0 3
      packages/host/apiproxy/tests/client-handler.spec.ts
  90. 7 70
      packages/host/apiproxy/tests/fetch-carrier.spec.ts
  91. 1 30
      packages/host/apiproxy/tests/rpc-schemas.spec.ts
  92. 2 2
      packages/host/directory-picker-browse/README.i18n.yaml
  93. 1 1
      packages/host/directory-picker-browse/README.md
  94. 1 1
      packages/host/directory-picker-browse/README.zh.md
  95. 2 2
      packages/host/directory-picker-browse/src/index.ts
  96. 2 2
      packages/host/directory-picker-native/README.i18n.yaml
  97. 1 1
      packages/host/directory-picker-native/README.md
  98. 1 1
      packages/host/directory-picker-native/README.zh.md
  99. 2 2
      packages/host/directory-picker/README.i18n.yaml
  100. 2 2
      packages/host/directory-picker/README.md

+ 2 - 2
.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md
-2026-08-18-settings-font-size-control.md: 34ee7f28f58a0a5a5dc8cd39bc4cecf5e081359f
-2026-08-18-settings-font-size-control.zh.md: 2c2ea714d50ca1f934d78ede6175935338488331
+2026-08-18-settings-font-size-control.md: 778893a2c0943f1d3af8905a27efb526071e073b
+2026-08-18-settings-font-size-control.zh.md: 71ff3b556622e7091891f8e18f5eefcbda2b0851

Різницю між файлами не показано, бо вона завелика
+ 0 - 1
.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md


Різницю між файлами не показано, бо вона завелика
+ 0 - 1
.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.zh.md


+ 16 - 3
apps/web/tests/preview-boot.e2e.ts

@@ -316,7 +316,6 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
     const exercised = await page.evaluate(async () => {
       type Result<T> = { result: { ok: true; value: T } | { ok: false; error: { code: string; message: string } } }
       interface PreviewApi {
-        host: { createDirectory(payload: { path: string; name: string }): Promise<Result<{ path: string }>> }
         skills: { list(payload: { sessionId: string }): Promise<Result<{ skills: unknown[] }>> }
         settings: {
           describe(payload: object): Promise<Result<{ namespaces: Array<{ ns: string; revision: number }> }>>
@@ -349,12 +348,26 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
       const sessionId = sessions.result.value.items[0]?.sessionId
       if (sessionId === undefined) throw new Error('workspace adoption created no Session')
 
+      // Remote namespaces answer over the same unary carrier; the args object
+      // keys every wire parameter by its name.
+      const remote = async <T>(endpoint: string, args: object): Promise<T> => {
+        const answered = await transport.fetch(`/api/${endpoint}`, {
+          method: 'POST',
+          headers: { 'content-type': 'application/json' },
+          body: JSON.stringify({
+            type: 'client-request', rpcId: `preview-${endpoint.replace('/', '-')}`,
+            method: endpoint, payload: { args },
+          }),
+        })
+        const body = await answered.json() as Result<T>
+        if (!body.result.ok) throw new Error(`${endpoint} failed: ${body.result.error.message}`)
+        return body.result.value
+      }
       const api = transport.createApiClient()
       const skills = await api.skills.list({ sessionId })
       if (!skills.result.ok) throw new Error(`skill.list failed: ${skills.result.error.message}`)
       const createDirectory = async (path: string, name: string): Promise<void> => {
-        const created = await api.host.createDirectory({ path, name })
-        if (!created.result.ok) throw new Error(`host.createDirectory failed: ${created.result.error.message}`)
+        await remote<string>('directoryPicker/createDirectory', { path, name })
         await new Promise((resolve) => { setTimeout(resolve, 250) })
         const refreshed = await api.skills.list({ sessionId })
         if (!refreshed.result.ok) throw new Error(`skill.list refresh failed: ${refreshed.result.error.message}`)

+ 17 - 0
apps/web/tests/settings-chrome.e2e.ts

@@ -351,7 +351,19 @@ describe('web e2e: settings modal and General preferences', () => {
     const readFontSize = async (target: Page = page): Promise<string> => await target.evaluate(
       () => document.body.style.getPropertyValue('--dsh-content-font-size'),
     )
+    // The secondary tier resolved by the real engine: a probe element's
+    // font-size forces min/max/calc evaluation, which the CSS-text specs
+    // cannot exercise. Setting −1 at ≤14, setting −2 above.
+    const readSecondaryFontSize = async (): Promise<string> => await page.evaluate(() => {
+      const probe = document.createElement('div')
+      probe.style.fontSize = 'var(--dsh-content-font-size-secondary, 13px)'
+      document.body.appendChild(probe)
+      const size = getComputedStyle(probe).fontSize
+      probe.remove()
+      return size
+    })
     expect(await readFontSize()).toBe('14px')
+    expect(await readSecondaryFontSize()).toBe('13px')
     await page.getByRole('button', { name: '设置', exact: true }).click()
     const dialog = page.getByRole('dialog', { name: '设置' })
     await dialog.waitFor({ timeout: 10_000 })
@@ -360,9 +372,13 @@ describe('web e2e: settings modal and General preferences', () => {
     const increase = dialog.getByRole('button', { name: '增大字号' })
     await increase.click()
     await dialog.getByText('15', { exact: true }).waitFor({ timeout: 5_000 })
+    // 15 is the piecewise boundary: the secondary tier holds at 13px (−2)
+    // where the ≤14 branch would have given 14px (−1).
+    await expect.poll(readSecondaryFontSize, { timeout: 5_000 }).toBe('13px')
     await increase.click()
     await dialog.getByText('16', { exact: true }).waitFor({ timeout: 5_000 })
     await expect.poll(readFontSize, { timeout: 5_000 }).toBe('16px')
+    await expect.poll(readSecondaryFontSize, { timeout: 5_000 }).toBe('14px')
     await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
       .toMatch(/ui-theme:\n(?:\s+\w+: .*\n)*?\s+fontSize: 16/)
     await page.keyboard.press('Escape')
@@ -375,6 +391,7 @@ describe('web e2e: settings modal and General preferences', () => {
     acknowledgeReloadConnectionLoss(tripwire, warningStart)
     await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
     await expect.poll(readFontSize, { timeout: 5_000 }).toBe('16px')
+    expect(await readSecondaryFontSize()).toBe('14px')
 
     // Restore the default for the specs that follow (and the dialog golden).
     await page.getByRole('button', { name: '设置', exact: true }).click()

+ 2 - 2
docs/capability-seams.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/capability-seams.md
-capability-seams.md: 2ee2d1b989c4eb25b245cfd528b2fa1ba3bb0272
-capability-seams.zh.md: 7ecda568ef4df4d360414ee872d2b0241ebe1dce
+capability-seams.md: 7cf93600cbf0d2deef586135f299058629732685
+capability-seams.zh.md: 3d79717d811fc3cd4e51c19c34c02a4e51d5da21

+ 5 - 2
docs/capability-seams.md

@@ -40,6 +40,7 @@ flowchart LR
   svc_sessionController["ctx.sessionController<br/>Host Session Remote controller"]
   pkg_api_workspace_controller["api-workspace-controller"]
   svc_workspaceController["ctx.workspaceController<br/>Host Workspace Remote controller"]
+  svc_directoryPickerController["ctx.directoryPickerController<br/>Host directory-picking Remote controller"]
   svc_invariants["ctx.invariants<br/>Package-owned invariant registry"]
   pkg_scope["scope"]
   pkg_typert_registry["typert-registry"]
@@ -217,6 +218,7 @@ flowchart LR
   pkg_agent_presets --> svc_agentPresets
   pkg_api_gateway --> svc_typertGateway
   pkg_api_session_controller --> svc_sessionController
+  pkg_api_workspace_controller --> svc_directoryPickerController
   pkg_api_workspace_controller --> svc_workspaceController
   pkg_attachment --> svc_attachments
   pkg_attachment_local --> svc_attachments
@@ -348,7 +350,7 @@ flowchart LR
   svc_credentials --> pkg_llm_deepseek
   svc_credentials --> pkg_llm_pi_ai
   svc_deepseekLlmApiExtensions --> pkg_llm_deepseek
-  svc_directoryPicker --> pkg_host_apiproxy
+  svc_directoryPicker --> pkg_api_workspace_controller
   svc_dynamicCordisRunner --> pkg_tool_cordis
   svc_e2b --> pkg_fs_e2b
   svc_e2b --> pkg_subprocess_e2b
@@ -459,6 +461,7 @@ flowchart LR
 | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. |
 | `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Owns Session commands, cold reads, durable-event following, live control state, and Agent activation policy; apiProxy reuses its inspection and Agent-resolution operations for Session-aware domains. |
 | `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Owns Workspace commands and reconnect-safe Workspace state delivery through the generated Remote namespace. |
+| `ctx.directoryPickerController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Carries the picking seam onto the wire: capability gating, cancellation, and the seam-coded failures a browser directory flow discriminates on. |
 | `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
 | `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. |
 | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. |
@@ -506,7 +509,7 @@ flowchart LR
 | `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry. |
 | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
 | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |
-| `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`host-apiproxy`](../packages/host/apiproxy) | - | Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement). |
+| `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`api-workspace-controller`](../packages/api/workspace-controller) | - | Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement). |
 | `ctx.webServer` | `core` | [`host-webserver`](../packages/host/webserver) | - | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-hmr`](../packages/client/hmr) | - | Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes. |
 | `ctx.clientModules` | `core` | [`client-modules`](../packages/client/modules) | - | [`client-hmr`](../packages/client/hmr) | - | Composes the __DSH_BOOT__ entry graph from an incremental dsh.client scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers. |
 | `ctx.workflowEngine` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | One engine per context, as in bash, with no named-provider registry; the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents. |

+ 5 - 2
docs/capability-seams.zh.md

@@ -42,6 +42,7 @@ flowchart LR
   svc_sessionController["ctx.sessionController<br/>Host Session Remote controller"]
   pkg_api_workspace_controller["api-workspace-controller"]
   svc_workspaceController["ctx.workspaceController<br/>Host Workspace Remote controller"]
+  svc_directoryPickerController["ctx.directoryPickerController<br/>Host directory-picking Remote controller"]
   svc_invariants["ctx.invariants<br/>Package-owned invariant registry"]
   pkg_scope["scope"]
   pkg_typert_registry["typert-registry"]
@@ -219,6 +220,7 @@ flowchart LR
   pkg_agent_presets --> svc_agentPresets
   pkg_api_gateway --> svc_typertGateway
   pkg_api_session_controller --> svc_sessionController
+  pkg_api_workspace_controller --> svc_directoryPickerController
   pkg_api_workspace_controller --> svc_workspaceController
   pkg_attachment --> svc_attachments
   pkg_attachment_local --> svc_attachments
@@ -350,7 +352,7 @@ flowchart LR
   svc_credentials --> pkg_llm_deepseek
   svc_credentials --> pkg_llm_pi_ai
   svc_deepseekLlmApiExtensions --> pkg_llm_deepseek
-  svc_directoryPicker --> pkg_host_apiproxy
+  svc_directoryPicker --> pkg_api_workspace_controller
   svc_dynamicCordisRunner --> pkg_tool_cordis
   svc_e2b --> pkg_fs_e2b
   svc_e2b --> pkg_subprocess_e2b
@@ -461,6 +463,7 @@ flowchart LR
 | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
 | `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | 负责 Session 命令、冷读取、持久事件跟随、实时控制状态与 Agent 激活策略;apiProxy 在需要 Session 上下文的领域中复用其检查和 Agent 解析操作。 |
 | `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 通过生成的 Remote namespace 负责 Workspace 命令和可在重连后收敛的 Workspace 状态投递。 |
+| `ctx.directoryPickerController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 把选目录 seam 送上线:能力门禁、取消传播,以及浏览器目录流程用于分支判断的 seam 错误码。 |
 | `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | 配套子路径注册所属包本地的检查;该服务负责选择、唯一性、子 fiber,以及标明所属包的失败。 |
 | `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | 插件直接或通过 dsh-typert-loader 注册实时 zod 贡献;API 网关消费调用描述符和提供方,其他运行时消费方则在各自边界查询 schema 与反射元数据。 |
 | `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | 将生成的 Remote 描述符与实时 Cordis 服务关联,解析已注册的身份,并通过共享的 Connection RPC 载体提供一元调用。 |
@@ -508,7 +511,7 @@ flowchart LR
 | `ctx.jobs` | `seam` | [`jobs`](../packages/jobs/jobs) | [`jobs-local`](../packages/jobs/jobs-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | - | 生产方(后台 bash、PTY 发送和 subagent 委派)登记正在运行的工作;tool-jobs 是面向模型的控制器,用于读取、列出和终止这些工作;jobs-local 是进程本地注册表。 |
 | `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-http`](../packages/web/web-fetch-http) | [`tool-web`](../packages/web/tool-web) | - | 搜索和抓取提供方注册到同一个 ctx.web seam;tool-web 负责稳定的面向模型名称。 |
 | `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | 后端保存过大的工具文本,并返回面向模型的定位信息和取回提示;spill-policy 是 tools/post-execute 消费方,负责决定何时 spill。 |
-| `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`host-apiproxy`](../packages/host/apiproxy) | - | 带判别标记的交互能力:原生后端在 Host 显示设备上打开一个操作系统选择器,浏览后端为应用内浏览器提供列表与创建原语;双端后端通过其浏览器侧填充 ui-workspace 目录流程的 slot(不通过协议发布)。 |
+| `ctx.directoryPicker` | `seam` | [`host-directory-picker`](../packages/host/directory-picker) | [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | [`api-workspace-controller`](../packages/api/workspace-controller) | - | 带判别标记的交互能力:原生后端在 Host 显示设备上打开一个操作系统选择器,浏览后端为应用内浏览器提供列表与创建原语;双端后端通过其浏览器侧填充 ui-workspace 目录流程的 slot(不通过协议发布)。 |
 | `ctx.webServer` | `core` | [`host-webserver`](../packages/host/webserver) | - | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-hmr`](../packages/client/hmr) | - | 普通的 node:http 载体:具名路由注册表、索引转换 tap,以及静态 dist 回退;Web 传输插件注册自己的路由。 |
 | `ctx.clientModules` | `core` | [`client-modules`](../packages/client/modules) | - | [`client-hmr`](../packages/client/hmr) | - | 通过增量 `dsh.client` 扫描组合 __DSH_BOOT__ 入口图,提供插件组合包,并通知重建/图变更订阅方。 |
 | `ctx.workflowEngine` | `seam` | [`workflow`](../packages/workflow/workflow) | [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | [`tool-workflow`](../packages/workflow/tool-workflow), [`tool-ralph`](../packages/workflow/tool-ralph) | - | 每个上下文使用一个引擎,与 bash 相同,且没有具名提供方注册表;通用工作流与固定 Ralph 消费方启动运行,其中的 agent() 调用通过 ctx.subagents 扇出。 |

+ 1 - 1
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 8b11d157f15d58c0a2193b51a76bbfb0b877b2ec
+config-catalog.md: 7c5361358bf66d02857fe91bd58bcca428c426a6
 config-catalog.zh.md: 60ed9270483f1e207e9af9295d050948fcd85518

+ 1 - 1
docs/config-catalog.md

@@ -796,7 +796,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/host/apiproxy/src/index.ts:41`](../packages/host/apiproxy/src/index.ts)
+Source: [`packages/host/apiproxy/src/index.ts:42`](../packages/host/apiproxy/src/index.ts)
 
 <a id="deepseek-aidsh-host-directory-picker-browse"></a>
 

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: 3a1aa6d3ba0000cf16ac38587d244c44bb49212a
-module-graph.zh.md: e83f239ddae34f30d8385e57bbc36454a5901ad9
+module-graph.md: db76f0f64025535b06440cf1b58d037be8085d70
+module-graph.zh.md: fd8e25d450b6e58bfc6a20c9e160858db0e6151a

+ 9 - 5
docs/module-graph.md

@@ -1085,6 +1085,7 @@ flowchart TD
   pkg_client_connection --> pkg_commands
   pkg_client_connection --> pkg_credentials
   pkg_client_connection --> pkg_host_apiproxy
+  pkg_client_connection --> pkg_host_directory_picker
   pkg_client_connection --> pkg_host_webserver
   pkg_client_connection --> pkg_invariants
   pkg_client_connection --> pkg_llm
@@ -1258,6 +1259,7 @@ flowchart TD
   pkg_api_session_controller --> pkg_workspace
   pkg_api_workspace_controller --> pkg_api_gateway
   pkg_api_workspace_controller --> pkg_client_connection
+  pkg_api_workspace_controller --> pkg_host_directory_picker
   pkg_api_workspace_controller --> pkg_invariants
   pkg_api_workspace_controller --> pkg_session
   pkg_api_workspace_controller --> pkg_storage_domain
@@ -1390,6 +1392,7 @@ flowchart TD
   pkg_client_ui_sidebar --> pkg_client_ui_session
   pkg_client_ui_sidebar --> pkg_client_ui_workspace
   pkg_client_ui_sidebar --> pkg_invariants
+  pkg_client_ui_workspace --> pkg_api_remotes
   pkg_client_ui_workspace --> pkg_api_session_controller
   pkg_client_ui_workspace --> pkg_api_workspace_controller
   pkg_client_ui_workspace --> pkg_client_connection
@@ -1401,6 +1404,7 @@ flowchart TD
   pkg_client_ui_workspace --> pkg_invariants
   pkg_client_ui_workspace --> pkg_schedule
   pkg_client_ui_workspace --> pkg_session
+  pkg_client_ui_workspace --> pkg_typert_protocol
   pkg_client_ui_workspace --> pkg_util_workspace_path
   pkg_client_ui_agent_preset --> pkg_agent_presets
   pkg_client_ui_agent_preset --> pkg_api_remotes
@@ -1428,7 +1432,7 @@ flowchart TD
   pkg_client_ui_brand_official --> pkg_client_ui_renderer
   pkg_client_ui_brand_official --> pkg_client_ui_sidebar
   pkg_client_ui_brand_official --> pkg_invariants
-  pkg_client_ui_directory_picker_browse --> pkg_client_connection
+  pkg_client_ui_directory_picker_browse --> pkg_api_remotes
   pkg_client_ui_directory_picker_browse --> pkg_client_locale
   pkg_client_ui_directory_picker_browse --> pkg_client_ui_renderer
   pkg_client_ui_directory_picker_browse --> pkg_client_ui_workspace
@@ -1864,7 +1868,7 @@ flowchart TD
 | [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) |
+| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-directory-picker`](../packages/host/directory-picker), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
 | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
@@ -1887,7 +1891,7 @@ flowchart TD
 | [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
-| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
+| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
 | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
 | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
@@ -1904,11 +1908,11 @@ flowchart TD
 | [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-workspace`](../packages/client/ui-workspace), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`util-crypto`](../packages/util/crypto), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
 | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`api-workspace-controller`](../packages/api/workspace-controller), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`schedule`](../packages/schedule/schedule), [`session`](../packages/core/session), [`util-workspace-path`](../packages/util/workspace-path) |
+| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`schedule`](../packages/schedule/schedule), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) |
 | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`agent-presets`](../packages/preset/agent-presets), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`client-ui-approval`](../packages/client/ui-approval) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`client-ui-brand-official`](../packages/client/ui-brand-official) | `client` | [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`client-ui-jobs`](../packages/client/ui-jobs) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants) |

+ 9 - 5
docs/module-graph.zh.md

@@ -1087,6 +1087,7 @@ flowchart TD
   pkg_client_connection --> pkg_commands
   pkg_client_connection --> pkg_credentials
   pkg_client_connection --> pkg_host_apiproxy
+  pkg_client_connection --> pkg_host_directory_picker
   pkg_client_connection --> pkg_host_webserver
   pkg_client_connection --> pkg_invariants
   pkg_client_connection --> pkg_llm
@@ -1260,6 +1261,7 @@ flowchart TD
   pkg_api_session_controller --> pkg_workspace
   pkg_api_workspace_controller --> pkg_api_gateway
   pkg_api_workspace_controller --> pkg_client_connection
+  pkg_api_workspace_controller --> pkg_host_directory_picker
   pkg_api_workspace_controller --> pkg_invariants
   pkg_api_workspace_controller --> pkg_session
   pkg_api_workspace_controller --> pkg_storage_domain
@@ -1392,6 +1394,7 @@ flowchart TD
   pkg_client_ui_sidebar --> pkg_client_ui_session
   pkg_client_ui_sidebar --> pkg_client_ui_workspace
   pkg_client_ui_sidebar --> pkg_invariants
+  pkg_client_ui_workspace --> pkg_api_remotes
   pkg_client_ui_workspace --> pkg_api_session_controller
   pkg_client_ui_workspace --> pkg_api_workspace_controller
   pkg_client_ui_workspace --> pkg_client_connection
@@ -1403,6 +1406,7 @@ flowchart TD
   pkg_client_ui_workspace --> pkg_invariants
   pkg_client_ui_workspace --> pkg_schedule
   pkg_client_ui_workspace --> pkg_session
+  pkg_client_ui_workspace --> pkg_typert_protocol
   pkg_client_ui_workspace --> pkg_util_workspace_path
   pkg_client_ui_agent_preset --> pkg_agent_presets
   pkg_client_ui_agent_preset --> pkg_api_remotes
@@ -1430,7 +1434,7 @@ flowchart TD
   pkg_client_ui_brand_official --> pkg_client_ui_renderer
   pkg_client_ui_brand_official --> pkg_client_ui_sidebar
   pkg_client_ui_brand_official --> pkg_invariants
-  pkg_client_ui_directory_picker_browse --> pkg_client_connection
+  pkg_client_ui_directory_picker_browse --> pkg_api_remotes
   pkg_client_ui_directory_picker_browse --> pkg_client_locale
   pkg_client_ui_directory_picker_browse --> pkg_client_ui_renderer
   pkg_client_ui_directory_picker_browse --> pkg_client_ui_workspace
@@ -1866,7 +1870,7 @@ flowchart TD
 | [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
-| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) |
+| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-directory-picker`](../packages/host/directory-picker), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
 | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
@@ -1889,7 +1893,7 @@ flowchart TD
 | [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
-| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
+| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
 | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
 | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
@@ -1906,11 +1910,11 @@ flowchart TD
 | [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-workspace`](../packages/client/ui-workspace), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`util-crypto`](../packages/util/crypto), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
 | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`api-workspace-controller`](../packages/api/workspace-controller), [`client-locale`](../packages/client/locale), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`schedule`](../packages/schedule/schedule), [`session`](../packages/core/session), [`util-workspace-path`](../packages/util/workspace-path) |
+| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`schedule`](../packages/schedule/schedule), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) |
 | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`agent-presets`](../packages/preset/agent-presets), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`client-ui-approval`](../packages/client/ui-approval) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`client-ui-brand-official`](../packages/client/ui-brand-official) | `client` | [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`client-ui-jobs`](../packages/client/ui-jobs) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants) |

+ 2 - 2
docs/subsystems/workspace.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/workspace.md
-workspace.md: af2a8d4c5abbde04764e26751ba178fa99b2b3ef
-workspace.zh.md: f54444f052c8faffc7670468d5423123cd3aeeac
+workspace.md: 82bbe310d490653bae5d88f41d1fcbe76833b431
+workspace.zh.md: 9bf63b7a90f9136173fbec16b55a22821fa380e5

+ 34 - 0
docs/subsystems/workspace.md

@@ -149,6 +149,40 @@ abstract capability(): DirectoryPickerCapability
 
 Source: [`packages/host/directory-picker/src/index.ts`](../../packages/host/directory-picker/src/index.ts)
 
+<a id="ctxdirectorypickercontroller--directorypickercontroller"></a>
+
+### `ctx.directoryPickerController` — `DirectoryPickerController`
+
+Host service backing the generated `ctx.remote.directoryPicker` namespace. The seam it exports is abstract and therefore never a Loader entry of its own, so this controller carries the wire verbs: one composed backend serves either the native chooser or the browse primitives, and a verb the composition cannot serve is refused rather than approximated.
+
+```ts cordis-catalog
+/**
+ * Open the host's OS chooser for a Remote caller.
+ * @param signal - caller lifetime; abort terminates the chooser.
+ * @returns the chosen absolute path, or null when the operator cancels.
+ */
+@Remote('pick') async pick(signal: AbortSignal): Promise<string | null>
+
+/**
+ * List one directory level for a Remote caller's in-app browser.
+ * @param path - absolute directory to list; absent lists the home directory.
+ * @param signal - caller lifetime; abort stops the backend's scan instead of
+ *   letting it outlive a disconnected caller.
+ * @returns the level's listing with its ancestry.
+ */
+@Remote('list') async list(path: string | undefined, signal: AbortSignal): Promise<DirectoryListing>
+
+/**
+ * Create one child directory for a Remote caller's in-app browser.
+ * @param path - absolute existing parent directory.
+ * @param name - single non-blank path segment.
+ * @returns the created directory's absolute path.
+ */
+@Remote('createDirectory') async createDirectory(path: string, name: string): Promise<string>
+```
+
+Source: [`packages/api/workspace-controller/src/directory-picker.ts`](../../packages/api/workspace-controller/src/directory-picker.ts)
+
 <a id="ctxworkspacecontroller--workspacecontroller"></a>
 
 ### `ctx.workspaceController` — `WorkspaceController`

+ 34 - 0
docs/subsystems/workspace.zh.md

@@ -149,6 +149,40 @@ abstract capability(): DirectoryPickerCapability
 
 Source: [`packages/host/directory-picker/src/index.ts`](../../packages/host/directory-picker/src/index.ts)
 
+<a id="ctxdirectorypickercontroller--directorypickercontroller"></a>
+
+### `ctx.directoryPickerController` — `DirectoryPickerController`
+
+Host service backing the generated `ctx.remote.directoryPicker` namespace. The seam it exports is abstract and therefore never a Loader entry of its own, so this controller carries the wire verbs: one composed backend serves either the native chooser or the browse primitives, and a verb the composition cannot serve is refused rather than approximated.
+
+```ts cordis-catalog
+/**
+ * Open the host's OS chooser for a Remote caller.
+ * @param signal - caller lifetime; abort terminates the chooser.
+ * @returns the chosen absolute path, or null when the operator cancels.
+ */
+@Remote('pick') async pick(signal: AbortSignal): Promise<string | null>
+
+/**
+ * List one directory level for a Remote caller's in-app browser.
+ * @param path - absolute directory to list; absent lists the home directory.
+ * @param signal - caller lifetime; abort stops the backend's scan instead of
+ *   letting it outlive a disconnected caller.
+ * @returns the level's listing with its ancestry.
+ */
+@Remote('list') async list(path: string | undefined, signal: AbortSignal): Promise<DirectoryListing>
+
+/**
+ * Create one child directory for a Remote caller's in-app browser.
+ * @param path - absolute existing parent directory.
+ * @param name - single non-blank path segment.
+ * @returns the created directory's absolute path.
+ */
+@Remote('createDirectory') async createDirectory(path: string, name: string): Promise<string>
+```
+
+Source: [`packages/api/workspace-controller/src/directory-picker.ts`](../../packages/api/workspace-controller/src/directory-picker.ts)
+
 <a id="ctxworkspacecontroller--workspacecontroller"></a>
 
 ### `ctx.workspaceController` — `WorkspaceController`

+ 0 - 5
knip.json

@@ -76,11 +76,6 @@
         "tests/**/*.ts"
       ]
     },
-    "packages/api/workspace-controller": {
-      "ignoreDependencies": [
-        "zod"
-      ]
-    },
     "packages/client/ui-approval": {
       "entry": [
         "tests/**/*.spec.tsx"

+ 1 - 1
packages/api/remotes/src/client/index.ts

@@ -53,7 +53,7 @@ export type {} from '@deepseek-ai/dsh-api-session-controller/types'
  */
 export type {
   ConfigurableProviderView, ConnectionHandle, ConnectionSinks, ContentBlock,
-  CredentialView, DirectoryListing, DiscoveredModelView, IApiClient,
+  CredentialView, DiscoveredModelView, IApiClient,
   MessageId, ModelCatalog, ModelCatalogFailure, ModelProviderGroup, ModelReasoningEffort, ModelSelection,
   RpcError, RpcId, RpcRequest, RpcResponse, RpcResult, SessionId,
   SettingsNamespaceView, SettingsPathOpView, SkillEntry, StreamChunk,

+ 0 - 17
packages/api/session-controller/tests/fake-api.client.ts

@@ -167,23 +167,9 @@ export class FakeApiClient implements IApiClient {
     () => Promise.resolve(ok({
       version: '0-fake', cwd: '/f', attachedSessions: 0, home: '/h', canOpenPath: true,
     }))
-  onPickDirectory: (payload: unknown) => Promise<RpcResponse<{ path: string | null }>> =
-    () => Promise.resolve(ok({ path: null }))
   onOpenPath: (payload: unknown) => Promise<RpcResponse<{ opened: true }>> =
     () => Promise.resolve(ok({ opened: true as const }))
 
-  onListDirectory: (payload: unknown) => Promise<RpcResponse<{
-    path: string
-    home: string
-    crumbs: { name: string; path: string; hidden: boolean }[]
-    entries: { name: string; path: string; hidden: boolean }[]
-    truncated: boolean
-  }>> =
-    () => Promise.resolve(ok({ path: '/home/fake', home: '/home/fake', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false }))
-
-  onCreateDirectory: (payload: unknown) => Promise<RpcResponse<{ path: string }>> =
-    () => Promise.resolve(ok({ path: '/home/fake/new' }))
-
   private readonly followConns = new Map<SessionId, ValueStreamConn<SessionFollowFrame>[]>()
   private readonly controlConns: ValueStreamConn<SessionControlFrame>[] = []
   private readonly workspaceConns: ValueStreamConn<WorkspaceFollowFrame>[] = []
@@ -210,9 +196,6 @@ export class FakeApiClient implements IApiClient {
 
   readonly host: IApiClient['host'] = {
     describe: (payload: unknown) => this.record('host.describe', payload, this.onDescribe(payload)),
-    pickDirectory: (payload: unknown) => this.record('host.pickDirectory', payload, this.onPickDirectory(payload)),
-    listDirectory: (payload: unknown) => this.record('host.listDirectory', payload, this.onListDirectory(payload)),
-    createDirectory: (payload: unknown) => this.record('host.createDirectory', payload, this.onCreateDirectory(payload)),
     openPath: (payload: unknown) => this.record('host.openPath', payload, this.onOpenPath(payload)),
   }
 

+ 2 - 2
packages/api/workspace-controller/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/api/workspace-controller/README.md
-README.md: d2f89e6c9f0118be9150650c1c6dec859375f990
-README.zh.md: 3411ca0642df7fa7f8652d1863905868f54cf4bd
+README.md: 731a6331e2a19991921c022759f6bbd971cef525
+README.zh.md: f46c78b7b300f26983eee94d3dfdb488d236116e

+ 1 - 1
packages/api/workspace-controller/README.md

@@ -8,7 +8,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`@deepseek-ai/dsh-api-workspace-controller` owns the Host `ctx.workspaceController` service and the generated Client `ctx.remote.workspace` namespace. Its Remote methods create, rename, remove, and reorder Workspaces, reorder Sessions within a Workspace, archive Sessions from Workspace navigation, and follow the complete Workspace projection. Use it through API Gateway when a Client must change or follow Workspace navigation.
+`@deepseek-ai/dsh-api-workspace-controller` owns the Host `ctx.workspaceController` service and the generated Client `ctx.remote.workspace` namespace. Its Remote methods create, rename, remove, and reorder Workspaces, reorder Sessions within a Workspace, archive Sessions from Workspace navigation, and follow the complete Workspace projection. Use it through API Gateway when a Client must change or follow Workspace navigation. The package also owns `ctx.directoryPickerController` and the generated `ctx.remote.directoryPicker` namespace, because the directory-picking seam it carries is abstract and never a Loader entry of its own.
 
 ## Table of Contents
 

+ 1 - 1
packages/api/workspace-controller/README.zh.md

@@ -8,7 +8,7 @@ kind: "package-reference"
 
 ## 概述
 
-`@deepseek-ai/dsh-api-workspace-controller` 拥有 Host 的 `ctx.workspaceController` 服务和生成的 Client `ctx.remote.workspace` namespace。它的 Remote 方法负责创建、重命名、移除和重排 Workspace,在 Workspace 内重排 Session,从 Workspace 导航中归档 Session,以及跟随完整的 Workspace 投影。当 Client 必须修改或跟随 Workspace 导航时,请通过 API Gateway 使用它。
+`@deepseek-ai/dsh-api-workspace-controller` 拥有 Host 的 `ctx.workspaceController` 服务和生成的 Client `ctx.remote.workspace` namespace。它的 Remote 方法负责创建、重命名、移除和重排 Workspace,在 Workspace 内重排 Session,从 Workspace 导航中归档 Session,以及跟随完整的 Workspace 投影。当 Client 必须修改或跟随 Workspace 导航时,请通过 API Gateway 使用它。本包同时拥有 `ctx.directoryPickerController` 与生成的 `ctx.remote.directoryPicker` namespace,因为它承载的选目录 seam 是抽象的,自身从不作为 Loader entry。
 
 ## 目录
 

+ 2 - 0
packages/api/workspace-controller/package.json

@@ -76,6 +76,7 @@
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-api-gateway": "workspace:^",
     "@deepseek-ai/dsh-client-connection": "workspace:^",
+    "@deepseek-ai/dsh-host-directory-picker": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-storage-domain": "workspace:^",
@@ -87,6 +88,7 @@
     "@deepseek-ai/dsh-api-gateway": "workspace:^",
     "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-client-store": "workspace:^",
+    "@deepseek-ai/dsh-host-directory-picker": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-storage-domain": "workspace:^",

+ 171 - 0
packages/api/workspace-controller/src/directory-picker.ts

@@ -0,0 +1,171 @@
+/**
+ * Host directory-picking Remote owner: capability gating, cancellation, and the
+ * stable wire failure vocabulary over the `ctx.directoryPicker` seam.
+ */
+
+import { Context } from '@deepseek-ai/cordis'
+import { z } from 'zod'
+import { DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker'
+import type { DirectoryPickerCapabilities } from '@deepseek-ai/dsh-host-directory-picker'
+// The seam owns the listing declaration; the generator requires the reference
+// site to name that package rather than this package's re-export of it.
+import type { DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types'
+import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
+import type { DirectoryPickerErrorDetailsMap } from './types.ts'
+
+const createDirectoryRequestSchema = z.object({
+  path: z.string(),
+  name: z.string(),
+}).refine(
+  request => request.name.trim() !== '' && request.name !== '.' && request.name !== '..'
+    && !/[/\\]/.test(request.name),
+  { message: 'host.createDirectory requires a single non-blank path segment name' },
+)
+
+declare module '@deepseek-ai/cordis' {
+  interface Context {
+    /** Host directory-picking Remote namespace owner. */
+    directoryPickerController: DirectoryPickerController
+  }
+}
+
+/**
+ * Host service backing the generated `ctx.remote.directoryPicker` namespace. The
+ * seam it exports is abstract and therefore never a Loader entry of its own, so
+ * this controller carries the wire verbs: one composed backend serves either the
+ * native chooser or the browse primitives, and a verb the composition cannot
+ * serve is refused rather than approximated.
+ */
+export class DirectoryPickerController extends TypertRemoteService {
+  static inject = ['directoryPicker']
+
+  /** @param ctx - Host context carrying the composed directory-picking backend. */
+  constructor(ctx: Context) {
+    super(ctx, 'directoryPickerController', { namespace: 'directoryPicker' })
+  }
+
+  /**
+   * Open the host's OS chooser for a Remote caller.
+   * @param signal - caller lifetime; abort terminates the chooser.
+   * @returns the chosen absolute path, or null when the operator cancels.
+   */
+  @Remote('pick')
+  async pick(signal: AbortSignal): Promise<string | null> {
+    const capability = this.requireCapability('native', 'pick')
+    try {
+      return await capability.pick(signal)
+    } catch (error: unknown) {
+      throw cancellableFailure(error, signal, 'directory picker was aborted', 'directory picker failed')
+    }
+  }
+
+  /**
+   * List one directory level for a Remote caller's in-app browser.
+   * @param path - absolute directory to list; absent lists the home directory.
+   * @param signal - caller lifetime; abort stops the backend's scan instead of
+   *   letting it outlive a disconnected caller.
+   * @returns the level's listing with its ancestry.
+   */
+  @Remote('list')
+  async list(path: string | undefined, signal: AbortSignal): Promise<DirectoryListing> {
+    const capability = this.requireCapability('browse', 'list')
+    try {
+      return await capability.list(path, signal)
+    } catch (error: unknown) {
+      throw cancellableFailure(error, signal, 'directory listing was aborted')
+    }
+  }
+
+  /**
+   * Create one child directory for a Remote caller's in-app browser.
+   * @param path - absolute existing parent directory.
+   * @param name - single non-blank path segment.
+   * @returns the created directory's absolute path.
+   */
+  @Remote('createDirectory')
+  async createDirectory(path: string, name: string): Promise<string> {
+    const request = createDirectoryRequestSchema.safeParse({ path, name })
+    if (!request.success) {
+      throw pickerFailureOf(
+        'bad-request',
+        'invalid payload for host.createDirectory',
+        { issues: request.error.issues },
+      )
+    }
+    const capability = this.requireCapability('browse', 'createDirectory')
+    try {
+      return await capability.createDirectory(request.data.path, request.data.name)
+    } catch (error: unknown) {
+      throw browseFailure(error)
+    }
+  }
+
+  /** Resolve the capability one wire verb needs, or refuse with the kind this backend serves. */
+  private requireCapability<Kind extends keyof DirectoryPickerCapabilities>(
+    kind: Kind,
+    method: string,
+  ): DirectoryPickerCapabilities[Kind] {
+    const capability = this.ctx.directoryPicker.capability()
+    if (capability.kind !== kind) {
+      throw pickerFailureOf(
+        'directory-picker-unavailable',
+        `directoryPicker.${method} needs the ${kind} capability; the composed picker serves "${capability.kind}"`,
+        { capability: capability.kind },
+      )
+    }
+    return capability as DirectoryPickerCapabilities[Kind]
+  }
+}
+
+/**
+ * Raise one entry of the picking wire failure vocabulary.
+ * @param code - the failure code a caller discriminates on.
+ * @param message - operator-facing description.
+ * @param details - the payload this code carries.
+ * @returns the failure to throw across the Remote boundary.
+ */
+function pickerFailureOf<Code extends keyof DirectoryPickerErrorDetailsMap>(
+  code: Code,
+  message: string,
+  details: DirectoryPickerErrorDetailsMap[Code],
+): TypertRemoteFailure {
+  return new TypertRemoteFailure({ code, message, details })
+}
+
+/**
+ * Classify a browse-primitive rejection: the seam's own closed codes carry the
+ * path they are about, and anything else stays an infrastructure failure.
+ * @param error - the primitive's rejection.
+ * @returns the failure to throw across the Remote boundary.
+ */
+function browseFailure(error: unknown): TypertRemoteFailure {
+  if (error instanceof DirectoryPickerError) {
+    return pickerFailureOf(error.code, error.message, { path: error.path })
+  }
+  return pickerFailureOf('internal', errorMessage(error), {})
+}
+
+/**
+ * Classify a cancellable primitive's rejection. An abort is the caller's own
+ * timeout or disconnect, not a backend failure, so it answers `cancelled`
+ * before the business classification runs.
+ * @param error - the primitive's rejection.
+ * @param signal - the caller lifetime the primitive ran under.
+ * @param cancelled - operator-facing text for the abort outcome.
+ * @param failed - prefix for a non-seam failure, when the verb has no closed codes.
+ * @returns the failure to throw across the Remote boundary.
+ */
+function cancellableFailure(
+  error: unknown,
+  signal: AbortSignal,
+  cancelled: string,
+  failed?: string,
+): TypertRemoteFailure {
+  if (signal.aborted) return pickerFailureOf('cancelled', cancelled, {})
+  if (failed === undefined) return browseFailure(error)
+  return pickerFailureOf('internal', `${failed}: ${errorMessage(error)}`, {})
+}
+
+function errorMessage(error: unknown): string {
+  return error instanceof Error ? error.message : String(error)
+}

+ 7 - 0
packages/api/workspace-controller/src/index.ts

@@ -3,6 +3,7 @@
 import { Context } from '@deepseek-ai/cordis'
 import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
 import { WorkspaceCommands } from './commands.ts'
+import { DirectoryPickerController } from './directory-picker.ts'
 import { WorkspaceFeed } from './feed.ts'
 import type {
   WorkspaceArchiveSessionRequest,
@@ -20,6 +21,7 @@ import type {
 } from './types.ts'
 
 export type * from './types.ts'
+export { DirectoryPickerController } from './directory-picker.ts'
 
 declare module '@deepseek-ai/cordis' {
   interface Context {
@@ -40,6 +42,11 @@ export class WorkspaceController extends TypertRemoteService {
     super(ctx, 'workspaceController', { namespace: 'workspace' })
     this.commands = new WorkspaceCommands(ctx)
     this.feed = new WorkspaceFeed(ctx)
+    // This package is the Loader entry for both Remote owners it hosts: the
+    // directory-picking seam is abstract and never an entry itself. The child
+    // stays pending until a picking backend is composed, so a host without one
+    // registers no picking namespace instead of answering an unservable verb.
+    ctx.plugin(DirectoryPickerController)
   }
 
   /**

+ 28 - 1
packages/api/workspace-controller/src/types.ts

@@ -1,9 +1,18 @@
-/** Browser-safe request, result, and state-stream vocabulary for Workspace Remote. */
+/**
+ * Browser-safe request, result, and state-stream vocabulary for the Workspace
+ * and directory-picking Remote namespaces this package owns. The picking seam
+ * declares its own listing types, so they are re-exported here rather than
+ * restated: a browser consumer reads the very declaration the backend answers.
+ */
 
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
+import type { z as zCore } from 'zod'
+
+type ZodIssue = zCore.core.$ZodIssue
 
 export type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
+export type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types'
 
 /** One durable Workspace projected for browser consumers. */
 export interface WorkspaceView {
@@ -43,6 +52,24 @@ export type WorkspaceError = {
   }
 }[keyof WorkspaceErrorDetailsMap]
 
+/** Stable directory-picking failure details returned by the picking wire verbs. */
+export interface DirectoryPickerErrorDetailsMap {
+  /** The directory creation request violates its semantic input constraints. */
+  'bad-request': { readonly issues: ZodIssue[] }
+  /** The verb needs an interaction the composed backend does not serve. */
+  'directory-picker-unavailable': { readonly capability: string }
+  /** The target is not fully qualified, or the backend cannot list it. */
+  'directory-unreadable': { readonly path: string }
+  /** A child of that name is already there. */
+  'directory-exists': { readonly path: string }
+  /** The parent is not fully qualified, the name is not one segment, or creation failed. */
+  'directory-create-failed': { readonly path: string }
+  /** The caller's own timeout or disconnect ended the chooser or the scan. */
+  cancelled: Record<never, never>
+  /** A backend failure with no seam code of its own. */
+  internal: Record<never, never>
+}
+
 /** Existing directory requested for Workspace adoption. */
 export interface WorkspaceCreateRequest {
   readonly path: string

+ 166 - 0
packages/api/workspace-controller/tests/directory-picker.host.spec.ts

@@ -0,0 +1,166 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { Context } from '@deepseek-ai/cordis'
+import { DirectoryPicker, DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker'
+import type { DirectoryPickerCapability } from '@deepseek-ai/dsh-host-directory-picker'
+import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
+import { DirectoryPickerController } from '../src/directory-picker.ts'
+
+const roots: Context[] = []
+
+afterEach(async () => {
+  await Promise.all(roots.splice(0).map(ctx => ctx.fiber.dispose()))
+})
+
+/** A backend serving exactly the capability one case is about. */
+class StubPicker extends DirectoryPicker {
+  static capabilityStub: DirectoryPickerCapability = { kind: 'native', pick: async () => null }
+
+  capability(): DirectoryPickerCapability {
+    return StubPicker.capabilityStub
+  }
+}
+
+const NATIVE_STUB: DirectoryPickerCapability = { kind: 'native', pick: async () => null }
+
+const BROWSE_STUB: DirectoryPickerCapability = {
+  kind: 'browse',
+  list: async (path) => {
+    if (path === '/denied') {
+      throw new DirectoryPickerError('directory-unreadable', '/denied', 'cannot list /denied')
+    }
+    const target = path ?? '/home/user'
+    return {
+      path: target,
+      home: '/home/user',
+      crumbs: [{ name: '/', path: '/', hidden: false }],
+      entries: [{ name: 'projects', path: `${target}/projects`, hidden: false }],
+      truncated: false,
+    }
+  },
+  createDirectory: async (path, name) => {
+    if (name === 'taken') {
+      throw new DirectoryPickerError('directory-exists', `${path}/${name}`, 'already exists')
+    }
+    if (name === 'unwritable') throw new Error('disk detached')
+    if (name === 'gone') throw 'the volume vanished'
+    return `${path}/${name}`
+  },
+}
+
+async function harness(capability: DirectoryPickerCapability = NATIVE_STUB) {
+  StubPicker.capabilityStub = capability
+  const ctx = new Context()
+  roots.push(ctx)
+  await ctx.plugin(StubPicker).await()
+  return new DirectoryPickerController(ctx)
+}
+
+/** The failure payload a refused wire verb carries. */
+async function refused(call: Promise<unknown>): Promise<{ code: string; message: string; details: object }> {
+  try {
+    await call
+  } catch (error: unknown) {
+    if (!(error instanceof TypertRemoteFailure)) throw error
+    return { ...error.failure }
+  }
+  throw new Error('the call was expected to be refused')
+}
+
+describe('directoryPicker pick Remote', () => {
+  it('answers the selected path or the operator\'s cancellation', async () => {
+    const selected = await harness({ kind: 'native', pick: async () => '/tmp/project' })
+    expect(await selected.pick(new AbortController().signal)).toBe('/tmp/project')
+
+    const cancelled = await harness(NATIVE_STUB)
+    expect(await cancelled.pick(new AbortController().signal)).toBeNull()
+  })
+
+  it('reports an aborted chooser as cancelled and any other failure as internal', async () => {
+    const picker = await harness({
+      kind: 'native',
+      pick: signal => new Promise((_resolve, reject) => {
+        signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
+      }),
+    })
+    const abort = new AbortController()
+    const pending = refused(picker.pick(abort.signal))
+    abort.abort()
+    expect((await pending).code).toBe('cancelled')
+
+    const broken = await harness({ kind: 'native', pick: async () => { throw new Error('no chooser installed') } })
+    const failure = await refused(broken.pick(new AbortController().signal))
+    expect(failure.code).toBe('internal')
+    expect(failure.message).toContain('no chooser installed')
+  })
+
+  it('refuses the native verb under a browse composition', async () => {
+    const picker = await harness(BROWSE_STUB)
+    const failure = await refused(picker.pick(new AbortController().signal))
+    expect(failure.code).toBe('directory-picker-unavailable')
+    expect(failure.message).toContain('needs the native capability')
+    expect(failure.details).toEqual({ capability: 'browse' })
+  })
+})
+
+describe('directoryPicker browse Remotes', () => {
+  it('serves listings and creation, defaulting to the home directory', async () => {
+    const picker = await harness(BROWSE_STUB)
+    const signal = new AbortController().signal
+    expect(await picker.list(undefined, signal)).toMatchObject({ path: '/home/user', home: '/home/user' })
+    expect(await picker.list('/home/user/projects', signal))
+      .toMatchObject({ path: '/home/user/projects' })
+    expect(await picker.createDirectory('/home/user', 'fresh')).toBe('/home/user/fresh')
+  })
+
+  it('maps the seam\'s typed failures and folds unknown throws to internal', async () => {
+    const picker = await harness(BROWSE_STUB)
+    expect(await refused(picker.list('/denied', new AbortController().signal)))
+      .toMatchObject({ code: 'directory-unreadable', details: { path: '/denied' } })
+    expect((await refused(picker.createDirectory('/home/user', 'taken'))).code).toBe('directory-exists')
+    expect((await refused(picker.createDirectory('/home/user', 'unwritable'))).code).toBe('internal')
+
+    const thrown = await refused(picker.createDirectory('/home/user', 'gone'))
+    expect(thrown).toMatchObject({ code: 'internal', message: 'the volume vanished' })
+  })
+
+  it('rejects invalid child names before capability dispatch', async () => {
+    const createDirectory = vi.fn(async (path: string, name: string) => `${path}/${name}`)
+    const picker = await harness({
+      kind: 'browse',
+      list: (path, signal) => BROWSE_STUB.list(path, signal),
+      createDirectory,
+    })
+
+    for (const name of ['', ' ', '.', '..', 'a/b', 'a\\b']) {
+      const failure = await refused(picker.createDirectory('/home/user', name))
+      expect(failure).toMatchObject({
+        code: 'bad-request',
+        message: 'invalid payload for host.createDirectory',
+      })
+      expect(Array.isArray(Reflect.get(failure.details, 'issues'))).toBe(true)
+    }
+    expect(createDirectory).not.toHaveBeenCalled()
+  })
+
+  it('reports an aborted listing as cancelled', async () => {
+    const picker = await harness({
+      kind: 'browse',
+      list: (_path, signal) => new Promise((_resolve, reject) => {
+        signal?.addEventListener('abort', () => { reject(new Error('scan aborted')) }, { once: true })
+      }),
+      createDirectory: async () => '/never',
+    })
+    const abort = new AbortController()
+    const pending = refused(picker.list(undefined, abort.signal))
+    abort.abort()
+    expect((await pending).code).toBe('cancelled')
+  })
+
+  it('refuses the browse verbs under a native composition', async () => {
+    const picker = await harness()
+    expect(await refused(picker.list(undefined, new AbortController().signal)))
+      .toMatchObject({ code: 'directory-picker-unavailable', details: { capability: 'native' } })
+    expect(await refused(picker.createDirectory('/x', 'y')))
+      .toMatchObject({ code: 'directory-picker-unavailable', details: { capability: 'native' } })
+  })
+})

+ 1 - 0
packages/api/workspace-controller/tsconfig.client.json

@@ -17,6 +17,7 @@
     { "path": "../../client/connection/tsconfig.client.json" },
     { "path": "../../client/store" },
     { "path": "../../core/session" },
+    { "path": "../../host/directory-picker" },
     { "path": "../../typert/protocol" },
     { "path": "../../workspace/workspace" }
   ]

+ 2 - 0
packages/api/workspace-controller/tsconfig.host.json

@@ -10,11 +10,13 @@
     "src/invariant.ts",
     "src/types.ts",
     "src/commands.ts",
+    "src/directory-picker.ts",
     "src/feed.ts"
   ],
   "references": [
     { "path": "../../../vendor/cordis" },
     { "path": "../../core/session" },
+    { "path": "../../host/directory-picker" },
     { "path": "../../runtime-diagnostics/invariants" },
     { "path": "../../storage/storage-domain" },
     { "path": "../../typert/protocol" },

+ 2 - 0
packages/client/connection/package.json

@@ -53,6 +53,7 @@
     "@deepseek-ai/dsh-attachment": "workspace:^",
     "@deepseek-ai/dsh-credentials": "workspace:^",
     "@deepseek-ai/dsh-host-apiproxy": "workspace:^",
+    "@deepseek-ai/dsh-host-directory-picker": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
@@ -65,6 +66,7 @@
     "@deepseek-ai/dsh-attachment": "workspace:^",
     "@deepseek-ai/dsh-credentials": "workspace:^",
     "@deepseek-ai/dsh-host-apiproxy": "workspace:^",
+    "@deepseek-ai/dsh-host-directory-picker": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",

+ 0 - 1
packages/client/connection/src/client/api.ts

@@ -7,7 +7,6 @@
 
 export type {
   ApiProxy, HostApi,
-  DirectoryEntry, DirectoryListing,
   ResponseValue,
   SkillsApi, SkillEntry,
   ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,

+ 56 - 39
packages/client/connection/src/client/fixture.ts

@@ -29,6 +29,7 @@ import type { TodoItem } from '@deepseek-ai/dsh-tool-todo/client'
 // wire-fabrication boundary (the schema layer's one-cast-point posture).
 import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
 import type { CommandDescriptor, CommandExecution, CommandResult } from '@deepseek-ai/dsh-commands/types'
+import type { DirectoryListing as FixtureDirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types'
 import { deriveEventMessage, foldSurface } from '@deepseek-ai/dsh-session/surface'
 import type {
   ApiProxy, ClientRequest,
@@ -2178,6 +2179,55 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
     },
   }
 
+  /**
+   * Canonical fixture implementation of the generated Directory Picker Remote
+   * contract. The pick is deterministic — the keyless lanes drive the full
+   * pick-then-adopt path without an OS chooser — over the same design-mock
+   * tree the browse primitives serve.
+   */
+  const directoryPickerRemotes = {
+    pick(): ConnectionRpcResult<string | null> {
+      return { ok: true, value: `${FIXTURE_HOME}/Documents/project` }
+    },
+    list(path?: string): ConnectionRpcResult<FixtureDirectoryListing> {
+      const target = path ?? FIXTURE_HOME
+      const children = childrenOf(target)
+      if (children === undefined) {
+        return {
+          ok: false,
+          error: { code: 'directory-unreadable', message: `cannot list ${target}: not in the fixture tree`, details: { path: target } },
+        }
+      }
+      return {
+        ok: true,
+        value: {
+          path: target,
+          home: FIXTURE_HOME,
+          crumbs: crumbsOf(target),
+          entries: [...children].sort((a, b) => a.localeCompare(b))
+            .map(name => ({ name, path: target === '/' ? `/${name}` : `${target}/${name}`, hidden: name.startsWith('.') })),
+          // The fixture tree is tiny; no level ever reaches a backend bound.
+          truncated: false,
+        },
+      }
+    },
+    createDirectory(parent: string, name: string): ConnectionRpcResult<string> {
+      const children = childrenOf(parent)
+      if (children === undefined) {
+        return { ok: false, error: { code: 'directory-create-failed', message: `missing parent ${parent}`, details: { path: parent } } }
+      }
+      // Same root special case as list's entry paths: a plain join under '/'
+      // would mint '//name' and fork the tree's identity.
+      const target = parent === '/' ? `/${name}` : `${parent}/${name}`
+      if (children.includes(name)) {
+        return { ok: false, error: { code: 'directory-exists', message: `${target} already exists`, details: { path: target } } }
+      }
+      directoryTree.set(parent, [...children, name])
+      directoryTree.set(target, [])
+      return { ok: true, value: target }
+    },
+  }
+
   const goalRemotes = {
     create(id: SessionId, request: { objective: string; maxGoalRounds?: number }): RpcResult<{ ref: FxGoalRef }> {
       const missing = requireGoalSession(id)
@@ -3264,42 +3314,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
       describe: request => ok(request, {
         version: '0.0.0-fixture', cwd: '/tmp/fixture', attachedSessions, home: FIXTURE_HOME, canOpenPath: true,
       }),
-      // Deterministic native pick: the keyless lanes drive the full
-      // pick-then-adopt path without an OS chooser (design-mock content,
-      // same tree the browse primitives serve).
-      pickDirectory: request => ok(request, { path: `${FIXTURE_HOME}/Documents/project` }),
-      listDirectory: (request) => {
-        const target = request.payload.path ?? FIXTURE_HOME
-        const children = childrenOf(target)
-        if (children === undefined) {
-          return err(request, { code: 'directory-unreadable', message: `cannot list ${target}: not in the fixture tree`, details: { path: target } })
-        }
-        return ok(request, {
-          path: target,
-          home: FIXTURE_HOME,
-          crumbs: crumbsOf(target),
-          entries: [...children].sort((a, b) => a.localeCompare(b))
-            .map(name => ({ name, path: target === '/' ? `/${name}` : `${target}/${name}`, hidden: name.startsWith('.') })),
-          // The fixture tree is tiny; no level ever reaches a backend bound.
-          truncated: false,
-        })
-      },
-      createDirectory: (request) => {
-        const parent = request.payload.path
-        const children = childrenOf(parent)
-        if (children === undefined) {
-          return err(request, { code: 'directory-create-failed', message: `missing parent ${parent}`, details: { path: parent } })
-        }
-        // Same root special case as listDirectory's entry paths: a plain join
-        // under '/' would mint '//name' and fork the tree's identity.
-        const target = parent === '/' ? `/${request.payload.name}` : `${parent}/${request.payload.name}`
-        if (children.includes(request.payload.name)) {
-          return err(request, { code: 'directory-exists', message: `${target} already exists`, details: { path: target } })
-        }
-        directoryTree.set(parent, [...children, request.payload.name])
-        directoryTree.set(target, [])
-        return ok(request, { path: target })
-      },
       openPath: request => ok(request, { opened: true as const }),
     },
     agentPresets: {
@@ -3425,6 +3439,8 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
           agentId: SessionId
           line?: string
           query?: string
+          path?: string
+          name?: string
           images?: readonly unknown[]
           ref?: { id: string; revision: number }
           agentPreset?: string
@@ -3442,6 +3458,10 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
         case 'commands/execute': return Promise.resolve(commandRemotes.execute(sessionId, args.line as string, args.images ?? []))
         case 'fileReferences/list': return Promise.resolve(referenceRemotes.files(sessionId, args.query ?? ''))
         case 'sessionReferenceResolver/candidates': return Promise.resolve(referenceRemotes.sessions(sessionId, args.query ?? ''))
+        case 'directoryPicker/pick': return Promise.resolve(directoryPickerRemotes.pick())
+        case 'directoryPicker/list': return Promise.resolve(directoryPickerRemotes.list(args.path))
+        case 'directoryPicker/createDirectory':
+          return Promise.resolve(directoryPickerRemotes.createDirectory(args.path ?? '', args.name ?? ''))
         case 'goals/create': return Promise.resolve(goalRemotes.create(sessionId, {
           objective: (request as { objective?: string } | undefined)?.objective as string,
           ...(request as { maxGoalRounds?: number } | undefined)?.maxGoalRounds === undefined
@@ -3596,9 +3616,6 @@ export class FixtureApiClient extends AbstractApiClient {
   ): Promise<RpcResponse<unknown>> {
     switch (method) {
       case 'host.describe': return this.api.host.describe(request)
-      case 'host.pickDirectory': return this.api.host.pickDirectory(request, new AbortController().signal)
-      case 'host.listDirectory': return this.api.host.listDirectory(request, new AbortController().signal)
-      case 'host.createDirectory': return this.api.host.createDirectory(request)
       case 'host.openPath': return this.api.host.openPath(request, new AbortController().signal)
       case 'skill.list': return this.api.skills.list(request)
       case 'agentPreset.openDocument': return this.api.agentPresets.openDocument(request, new AbortController().signal)

+ 0 - 1
packages/client/connection/src/client/index.ts

@@ -31,7 +31,6 @@ declare module '@deepseek-ai/cordis' {
 // ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
 export type {
   ApiProxy, HostApi,
-  DirectoryEntry, DirectoryListing,
   SkillsApi, SkillEntry,
   ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
   MessageId, ModelReasoningEffort, ModelSelection,

+ 0 - 17
packages/client/connection/tests/fake-api.client.ts

@@ -50,30 +50,13 @@ export class FakeApiClient implements IApiClient {
     () => Promise.resolve(ok({
       version: '0-fake', cwd: '/f', attachedSessions: 0, home: '/h', canOpenPath: true,
     }))
-  onPickDirectory: (payload: unknown) => Promise<RpcResponse<{ path: string | null }>> =
-    () => Promise.resolve(ok({ path: null }))
   onOpenPath: (payload: unknown) => Promise<RpcResponse<{ opened: true }>> =
     () => Promise.resolve(ok({ opened: true as const }))
 
-  onListDirectory: (payload: unknown) => Promise<RpcResponse<{
-    path: string
-    home: string
-    crumbs: { name: string; path: string; hidden: boolean }[]
-    entries: { name: string; path: string; hidden: boolean }[]
-    truncated: boolean
-  }>> =
-    () => Promise.resolve(ok({ path: '/home/fake', home: '/home/fake', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false }))
-
-  onCreateDirectory: (payload: unknown) => Promise<RpcResponse<{ path: string }>> =
-    () => Promise.resolve(ok({ path: '/home/fake/new' }))
-
   private readonly generationConns: StreamConn[] = []
 
   readonly host: IApiClient['host'] = {
     describe: payload => this.record('host.describe', payload, this.onDescribe(payload)),
-    pickDirectory: payload => this.record('host.pickDirectory', payload, this.onPickDirectory(payload)),
-    listDirectory: payload => this.record('host.listDirectory', payload, this.onListDirectory(payload)),
-    createDirectory: payload => this.record('host.createDirectory', payload, this.onCreateDirectory(payload)),
     openPath: payload => this.record('host.openPath', payload, this.onOpenPath(payload)),
   }
 

+ 26 - 10
packages/client/connection/tests/fixture.client.spec.ts

@@ -17,8 +17,9 @@ import {
   type FixtureOptions,
 } from '../src/client/fixture.ts'
 import type {
-  ClientConnectionRpc,
+  ClientConnectionRpc, ConnectionRpcResult,
 } from '../src/rpc.ts'
+import type { DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types'
 
 const sid = (id: string): SessionId => id as SessionId
 type WorkspaceId = string & { readonly __fixtureWorkspaceId: 'WorkspaceId' }
@@ -286,6 +287,12 @@ interface FixtureRemoteEventStream extends AsyncIterable<FixtureRemoteEventFrame
 }
 
 type FixtureTestApi = ReturnType<typeof createFixtureFaces>['api'] & {
+  /** The directory-picking Remote namespace as the fixture serves it. */
+  readonly directoryPickerRemote: {
+    pick: () => Promise<ConnectionRpcResult<string | null>>
+    list: (path?: string) => Promise<ConnectionRpcResult<DirectoryListing>>
+    createDirectory: (path: string, name: string) => Promise<ConnectionRpcResult<string>>
+  }
   readonly sessions: FixtureSessionApi
   readonly sessionRemote: FixtureSessionRemote
   readonly workspace: FixtureWorkspaceApi
@@ -298,6 +305,15 @@ type FixtureTestApi = ReturnType<typeof createFixtureFaces>['api'] & {
 function createFixtureApi(options: FixtureOptions = {}): FixtureTestApi {
   const { api, rpc } = createFixtureFaces(options)
   return Object.assign(api, {
+    directoryPickerRemote: {
+      pick: () => rpc.call('/api', 'directoryPicker/pick', { args: {} }) as
+        Promise<ConnectionRpcResult<string | null>>,
+      list: (path?: string) => rpc.call('/api', 'directoryPicker/list', { args: { path } }) as
+        Promise<ConnectionRpcResult<DirectoryListing>>,
+      createDirectory: (path: string, name: string) =>
+        rpc.call('/api', 'directoryPicker/createDirectory', { args: { path, name } }) as
+          Promise<ConnectionRpcResult<string>>,
+    },
     sessions: createSessionApi(rpc),
     sessionRemote: createSessionRemote(rpc),
     workspace: createWorkspaceApi(rpc),
@@ -1049,18 +1065,18 @@ describe('createFixtureApi', () => {
 
   it('createDirectory under the root mints /name whose listing and crumbs share the identity', async () => {
     const api = createFixtureApi()
-    const created = await api.host.createDirectory(req({ path: '/', name: 'srv' }))
-    if (!created.result.ok) throw new Error('create failed')
-    expect(created.result.value.path).toBe('/srv')
-    const listed = await api.host.listDirectory(req({ path: '/srv' }), new AbortController().signal)
-    if (!listed.result.ok) throw new Error('list failed')
-    expect(listed.result.value.crumbs).toEqual([
+    const created = await api.directoryPickerRemote.createDirectory('/', 'srv')
+    if (!created.ok) throw new Error('create failed')
+    expect(created.value).toBe('/srv')
+    const listed = await api.directoryPickerRemote.list('/srv')
+    if (!listed.ok) throw new Error('list failed')
+    expect(listed.value.crumbs).toEqual([
       { name: '/', path: '/', hidden: false },
       { name: 'srv', path: '/srv', hidden: false },
     ])
-    const root = await api.host.listDirectory(req({ path: '/' }), new AbortController().signal)
-    if (!root.result.ok) throw new Error('root list failed')
-    expect(root.result.value.entries).toContainEqual({ name: 'srv', path: '/srv', hidden: false })
+    const root = await api.directoryPickerRemote.list('/')
+    if (!root.ok) throw new Error('root list failed')
+    expect(root.value.entries).toContainEqual({ name: 'srv', path: '/srv', hidden: false })
   })
 
   it('workspace/follow serves the resident baseline and create reuses on path collision', async () => {

+ 2 - 2
packages/client/connection/tests/http-bridge.host.spec.ts

@@ -35,11 +35,11 @@ describe('HTTP bridge abort', () => {
 
   it('aborts a pending native picker request when the browser disconnects', async () => {
     const body = JSON.stringify({
-      type: 'client-request', rpcId: 'picker-1', method: 'host.pickDirectory', payload: {},
+      type: 'client-request', rpcId: 'picker-1', method: 'directoryPicker/pick', payload: { args: {} },
     })
     const request = Readable.from([Buffer.from(body)]) as unknown as IncomingMessage
     Object.assign(request, {
-      url: '/api/host.pickDirectory',
+      url: '/api/directoryPicker/pick',
       method: 'POST',
       headers: { 'content-type': 'application/json' },
     })

+ 2 - 2
packages/client/connection/tests/node-half.host.spec.ts

@@ -174,7 +174,7 @@ describe('connection node half', () => {
   it('requires the same browser session for every method on every trusted authority', async () => {
     const { routes, connection, dispose } = await mounted({ trustedHosts: ['harness.example'] })
     const methods = [
-      'host.pickDirectory', 'host.openPath',
+      'host.openPath',
       'settings.describe', 'settings.update', 'credentials.describe', 'credentials.set',
       'llm.discoverModels', 'llm.models', 'agentPreset.openDocument',
     ]
@@ -503,7 +503,7 @@ describe('connection node half over a real HTTP server', () => {
       const methods = [
         'settings.describe', 'settings.openDocument', 'settings.update', 'settings.replace', 'settings.mutate',
         'credentials.describe', 'credentials.set', 'credentials.unset',
-        'host.pickDirectory', 'host.openPath',
+        'host.openPath',
         'llm.discoverModels',
         'agentPreset.openDocument',
         'llm.providers', 'llm.models',

+ 3 - 0
packages/client/connection/tsconfig.client.json

@@ -36,6 +36,9 @@
     {
       "path": "../../host/apiproxy"
     },
+    {
+      "path": "../../host/directory-picker"
+    },
     {
       "path": "../../interaction/commands"
     },

+ 6 - 6
packages/client/ui-chat/src/client/chat/ChatView.module.css

@@ -101,8 +101,8 @@
 .turnStatusClock {
   margin-left: 8px;
   font: var(--dsw-font-xs-13);
-  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
   font-weight: 400;
   font-variant-numeric: tabular-nums;
   color: var(--dsw-alias-label-caption);
@@ -125,14 +125,14 @@
 
 .hint {
   color: var(--dsw-alias-label-tertiary);
-  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(18px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px));
 }
 
 .openError {
   color: var(--dsw-alias-state-error-primary);
-  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(18px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px));
 }
 
 .older {

+ 2 - 2
packages/client/ui-chat/src/client/chat/ContextInjectionRow.module.css

@@ -29,7 +29,7 @@
   min-width: 0;
   overflow: hidden;
   color: var(--dsw-alias-label-tertiary);
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -41,7 +41,7 @@
   min-width: 0;
   overflow: hidden;
   color: var(--dsw-alias-label-tertiary);
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;

+ 1 - 1
packages/client/ui-chat/src/client/chat/GenericCommandCard.module.css

@@ -55,7 +55,7 @@
   overflow: hidden;
   flex: 1 1 auto;
   color: var(--dsw-alias-label-tertiary);
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;

+ 11 - 11
packages/client/ui-chat/src/client/chat/MessageItem.module.css

@@ -42,8 +42,8 @@
 
 .referenceSummary {
   color: var(--dsw-alias-label-tertiary);
-  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(18px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px));
 }
 .contextRow {
   padding: 2px 0;
@@ -120,7 +120,7 @@
 
 .compactionTitle {
   flex: none;
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-primary-dimmed);
 }
@@ -139,7 +139,7 @@
   min-width: 0;
   overflow: hidden;
   color: var(--dsw-alias-label-tertiary);
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -149,14 +149,14 @@
   /* Indent under the title, which starts at leading (16 + delta) + gap 6. */
   padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
 }
 
 .retryRow {
   color: var(--dsw-alias-label-tertiary);
-  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
 }
 
 .retrySummary {
@@ -227,8 +227,8 @@
   margin-top: 3px;
   padding-left: 14px;
   overflow-wrap: anywhere;
-  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(18px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px));
 }
 
 .retryDetailLabel {
@@ -241,8 +241,8 @@
   gap: 8px;
   align-items: start;
   padding: 2px 0;
-  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
 }
 
 .turnErrorDot {

+ 7 - 6
packages/client/ui-chat/src/client/chat/ReasoningRow.module.css

@@ -50,15 +50,16 @@
   background: var(--dsw-alias-label-caption);
 }
 
-/* Think text sits one 2px step under the body size and follows the Settings
-   font-size preference through the same px delta, keeping the step. */
+/* Think text reads the secondary tier — one step under the body size
+   (setting −1 at ≤14, setting −2 above), shared with every flow-row title
+   and summary. */
 .summary {
   min-width: 0;
   overflow: hidden;
   flex: 1 1 auto;
   color: var(--dsw-alias-label-tertiary);
-  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -71,8 +72,8 @@
   /* Indent under the title, which starts at leading (16 + delta) + gap 6. */
   padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
-  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
   white-space: pre-wrap;
   word-break: break-word;
 }

+ 5 - 4
packages/client/ui-chat/src/client/chat/StatsLine.module.css

@@ -1,5 +1,6 @@
-/* Session stats row: 12/20 tertiary text under the flow, aligned to the
-   shared message column axis (--dsh-chat-content-width). */
+/* Session stats row: 13/20 tertiary text (the secondary content tier) under
+   the flow, aligned to the shared message column axis
+   (--dsh-chat-content-width). */
 
 .root {
   /* Block, not flex: text-overflow only elides a block's inline content, so
@@ -11,8 +12,8 @@
   margin: 0 auto;
   box-sizing: border-box;
   padding: 4px calc(var(--dsh-composer-side-clearance) + 16px) 0px;
-  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
   color: var(--dsw-alias-label-tertiary);
   white-space: nowrap;
   overflow: hidden;

+ 2 - 2
packages/client/ui-chat/src/client/chat/TurnUsageDisclosure.module.css

@@ -29,9 +29,9 @@
   min-width: 0;
   overflow: hidden;
   color: var(--dsw-alias-label-tertiary);
-  font-size: 14px;
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   font-variant-numeric: tabular-nums;
-  line-height: 24px;
+  line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }

+ 7 - 7
packages/client/ui-chat/tests/chat-font-axis-styles.client.spec.ts

@@ -19,25 +19,25 @@ function declarationsFrom(source: string, selector: string): string[] {
 }
 
 describe('chat flow font-size axis', () => {
-  it('think text keeps its 2px step under the body size and moves with the delta', () => {
+  it('think text reads the secondary tier (one step under the body size)', () => {
     const css = read('ReasoningRow.module.css')
     for (const selector of ['.summary', '.thinkBody']) {
       expect(declarationsFrom(css, selector)).toEqual(expect.arrayContaining([
-        'font-size: calc(12px + var(--dsh-content-font-delta, 0px))',
-        'line-height: calc(20px + var(--dsh-content-font-delta, 0px))',
+        'font-size: var(--dsh-content-font-size-secondary, 13px)',
+        'line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px))',
       ]))
     }
   })
 
-  it('command and context summaries ride the axis at the body size', () => {
+  it('command and context summaries read the secondary tier on the shared row line', () => {
     expect(declarationsFrom(read('GenericCommandCard.module.css'), '.summary')).toEqual(expect.arrayContaining([
-      'font-size: var(--dsh-content-font-size, 14px)',
+      'font-size: var(--dsh-content-font-size-secondary, 13px)',
       'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
     ]))
     const context = read('ContextInjectionRow.module.css')
     for (const selector of ['.source', '.summary']) {
       expect(declarationsFrom(context, selector)).toEqual(expect.arrayContaining([
-        'font-size: var(--dsh-content-font-size, 14px)',
+        'font-size: var(--dsh-content-font-size-secondary, 13px)',
         'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
       ]))
     }
@@ -60,7 +60,7 @@ describe('chat flow font-size axis', () => {
     const css = read('MessageItem.module.css')
     for (const selector of ['.compactionTitle', '.compactionSummary', '.compactionBody']) {
       expect(declarationsFrom(css, selector)).toEqual(expect.arrayContaining([
-        'font-size: var(--dsh-content-font-size, 14px)',
+        'font-size: var(--dsh-content-font-size-secondary, 13px)',
         'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
       ]))
     }

+ 3 - 3
packages/client/ui-directory-picker-browse/package.json

@@ -32,7 +32,7 @@
   "dsh": {
     "client": {
       "inject": [
-        "@deepseek-ai/dsh-client-connection",
+        "@deepseek-ai/dsh-api-remotes",
         "@deepseek-ai/dsh-client-ui-renderer",
         "@deepseek-ai/dsh-client-ui-workspace",
         "@deepseek-ai/dsh-client-locale"
@@ -49,7 +49,7 @@
     "clsx": "^2.0.0"
   },
   "peerDependencies": {
-    "@deepseek-ai/dsh-client-connection": "workspace:^",
+    "@deepseek-ai/dsh-api-remotes": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
     "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
@@ -57,7 +57,7 @@
     "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
-    "@deepseek-ai/dsh-client-connection": "workspace:^",
+    "@deepseek-ai/dsh-api-remotes": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",

+ 1 - 1
packages/client/ui-directory-picker-browse/src/client/DirectoryBrowser.tsx

@@ -40,7 +40,7 @@ import {
   Button, IconCheckOutline16, IconChevronRightOutline14, IconEditOutline16, IconFolderClose16, IconFolderOpen16,
   IconPlusOutline16, Modal,
 } from '@deepseek-ai/dsh-client-ui-primitives'
-import type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-client-connection/client'
+import type { DirectoryEntry, DirectoryListing } from '@deepseek-ai/dsh-api-remotes/client'
 import type { Translate } from '@deepseek-ai/dsh-client-locale/client'
 import css from './DirectoryBrowser.module.css'
 

+ 1 - 1
packages/client/ui-directory-picker-browse/src/client/flow.ts

@@ -5,7 +5,7 @@
  */
 import { createElement } from 'react'
 import type { ReactElement } from 'react'
-import type { DirectoryListing } from '@deepseek-ai/dsh-client-connection/client'
+import type { DirectoryListing } from '@deepseek-ai/dsh-api-remotes/client'
 import type { Translate } from '@deepseek-ai/dsh-client-locale/client'
 // Type-only: the owner contract of the directory-flow holes.
 import type { DirectoryFlowOwnerProps } from '@deepseek-ai/dsh-client-ui-workspace/client'

+ 5 - 4
packages/client/ui-directory-picker-browse/src/client/index.ts

@@ -2,10 +2,11 @@
  * Browser half of the browse directory-picker backend: fills ui-workspace's
  * two directory-flow holes with the in-app Select Workspace Directory dialog
  * (figma `Harness` 813-23126 family), driving the node half's
- * `host.listDirectory`/`host.createDirectory` primitives. Mounting this
- * package therefore composes both sides of the browse interaction with one
- * cordis.yml row; no client code branches on a capability kind. The dialog's
- * copy is locale-registered here — the flow package owns its own strings.
+ * `directoryPicker/list`/`directoryPicker/createDirectory` primitives.
+ * Mounting this package therefore composes both sides of the browse
+ * interaction with one cordis.yml row; no client code branches on a
+ * capability kind. The dialog's copy is locale-registered here — the flow
+ * package owns its own strings.
  */
 import type { Context as ClientContext } from '@deepseek-ai/cordis'
 // Type-only: pulls the SlotMap merge declaring the directory-flow holes.

+ 1 - 1
packages/client/ui-directory-picker-browse/tests/client-flow.client.spec.tsx

@@ -2,7 +2,7 @@
 import { Context } from '@deepseek-ai/cordis'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
-import type { DirectoryListing } from '@deepseek-ai/dsh-client-connection/client'
+import type { DirectoryListing } from '@deepseek-ai/dsh-api-remotes/client'
 import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
 import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
 import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'

+ 1 - 1
packages/client/ui-directory-picker-browse/tests/directory-browser.client.spec.tsx

@@ -1,7 +1,7 @@
 // @vitest-environment jsdom
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
-import type { DirectoryListing } from '@deepseek-ai/dsh-client-connection/client'
+import type { DirectoryListing } from '@deepseek-ai/dsh-api-remotes/client'
 import { DirectoryBrowser } from '../src/client/DirectoryBrowser.tsx'
 
 afterEach(cleanup)

+ 1 - 1
packages/client/ui-directory-picker-browse/tsconfig.json

@@ -18,7 +18,7 @@
       "path": "../ui-primitives"
     },
     {
-      "path": "../connection/tsconfig.client.json"
+      "path": "../../api/remotes/tsconfig.client.json"
     },
     {
       "path": "../locale"

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

@@ -1,7 +1,7 @@
 /**
  * Browser half of the native directory-picker backend: fills ui-workspace's
  * two directory-flow holes with a renderless occupant that answers each
- * `open` by driving `host.pickDirectory` (the node half's OS chooser) and
+ * `open` by driving `directoryPicker/pick` (the node half's OS chooser) and
  * reporting the one outcome — picked path, cancellation, or failure — back
  * through the owner conversation. Mounting this package therefore composes
  * both sides of the native interaction with one cordis.yml row; no client

+ 1 - 1
packages/client/ui-message-feedback/src/client/MessageFeedbackActions.module.css

@@ -46,7 +46,7 @@
   border-radius: 14px;
   background: transparent;
   color: var(--dsw-alias-label-tertiary);
-  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(28px + var(--dsh-content-font-delta, 0px));
   white-space: nowrap;
   text-overflow: ellipsis;

+ 7 - 5
packages/client/ui-primitives/src/DisclosureRow.module.css

@@ -1,8 +1,10 @@
-/* Shared disclosure header: [16px leading] gap 6 [title 14/24] at the default
+/* Shared disclosure header: [16px leading] gap 6 [title 13/24] at the default
    size. The Settings font-size preference moves the row through the
-   body-published axis: title size follows --dsh-content-font-size, and the row
-   height, leading box, and glyph edge shift by the same px delta so the icon
-   keeps its optical share of the line. */
+   body-published axis: title size follows the secondary tier
+   (--dsh-content-font-size-secondary: one step under the body — setting −1 at
+   ≤14, setting −2 above), and the row height, leading box, and glyph edge
+   shift by the body px delta so the icon keeps its optical share of the
+   line. */
 
 .root {
   display: flex;
@@ -76,7 +78,7 @@ button.leading {
 
 .title {
   flex: none;
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-secondary);
 }

+ 2 - 2
packages/client/ui-primitives/tests/disclosure-row-styles.client.spec.ts

@@ -19,9 +19,9 @@ function declarations(selector: string): string[] {
 }
 
 describe('DisclosureRow.module.css font-size axis', () => {
-  it('sizes the title from the content font axis with the shared px delta', () => {
+  it('sizes the title from the secondary content tier on the shared row line', () => {
     expect(declarations('.title')).toEqual(expect.arrayContaining([
-      'font-size: var(--dsh-content-font-size, 14px)',
+      'font-size: var(--dsh-content-font-size-secondary, 13px)',
       'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
     ]))
   })

+ 2 - 2
packages/client/ui-theme/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-theme/README.md
-README.md: 12ea48aa2f321deec82ecea14c42aad7362523d6
-README.zh.md: a720916f5c9eab005a056415455e5017c0d08f0d
+README.md: 32ec60edb39b30afdb9b54804e62302cdc58e531
+README.zh.md: ab17910ab9cec4e1ff519af0922ee27034a6fa7c

+ 2 - 2
packages/client/ui-theme/README.md

@@ -29,7 +29,7 @@ Users switch the color scheme and content font size from two rows in Settings (G
 
 ### Appearance and font size
 
-The plugin registers Appearance preference cubes and a font-size stepper in the General section. The stepper accepts integer values from 12 through 17 px and defaults to 14 px. It changes conversation headings and base text by the same increment, including the user bubble and composer draft; tables, small text, and code keep fixed sizes. Each accepted change writes through the Host settings API. Rapid changes serialize in gesture order with namespace revisions, and a rejected latest write reloads the durable values. Non-loopback pages keep both choices process-local.
+The plugin registers Appearance preference cubes and a font-size stepper in the General section. The stepper accepts integer values from 12 through 17 px and defaults to 14 px. It changes conversation headings and base text by the same increment, including the user bubble and composer draft; flow-row titles, summaries, and tables follow one step under the body size, while small text and code keep fixed sizes. Each accepted change writes through the Host settings API. Rapid changes serialize in gesture order with namespace revisions, and a rejected latest write reloads the durable values. Non-loopback pages keep both choices process-local.
 
 ### Registering a theme
 
@@ -53,7 +53,7 @@ The service owns theme and font-size state and publishes snapshots. The ui-layou
 
 `src/styles/` holds five sheets imported in order by ui-theme's dynamic client entry: `base.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` is the sole consumer of the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them.
 
-`gradient-shadow-text.css` derives `--dsh-content-font-delta` from `--dsh-content-font-size` and shifts the Markdown heading and base-text ladder by that increment. Dense table, small, and code variants stay fixed. The user bubble and composer draft consume the same variables outside that ladder.
+`gradient-shadow-text.css` derives `--dsh-content-font-delta` from `--dsh-content-font-size` and shifts the Markdown heading and base-text ladder by that increment. It also derives the secondary tier `--dsh-content-font-size-secondary` (setting −1 at ≤14, setting −2 above; 13px at the default) with its own `--dsh-content-font-delta-secondary` for the table variants and the flow rows one step under the body. Dense small and code variants stay fixed. Outside the ladder, the user bubble and composer draft read the body pair directly, and flow-row titles and summaries read the secondary pair.
 
 ### Scrollbar rebinding
 

+ 2 - 2
packages/client/ui-theme/README.zh.md

@@ -29,7 +29,7 @@ kind: "package-reference"
 
 ### 外观与字号
 
-插件在「通用」分区注册外观偏好方块与字号步进器。步进器接受 12 至 17 px 的整数,默认值为 14 px。它以相同增量调整会话标题与基础文本,包括用户气泡与 composer 草稿;表格、小号文本和代码保持固定字号。每次通过的变更都经 Host settings API 写入。连续快速变更按操作顺序携带命名空间 revision 串行写入,最新写入被拒时重新加载持久值。非 loopback 页面把两个选择都保留在进程内。
+插件在「通用」分区注册外观偏好方块与字号步进器。步进器接受 12 至 17 px 的整数,默认值为 14 px。它以相同增量调整会话标题与基础文本,包括用户气泡与 composer 草稿;流内行的标题、摘要与表格跟随比正文低一档的字号,小号文本和代码保持固定字号。每次通过的变更都经 Host settings API 写入。连续快速变更按操作顺序携带命名空间 revision 串行写入,最新写入被拒时重新加载持久值。非 loopback 页面把两个选择都保留在进程内。
 
 ### 注册主题
 
@@ -53,7 +53,7 @@ kind: "package-reference"
 
 `src/styles/` 下有五张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR 会随 ui-theme 一同移除。`scrollbar.css` 是 `--dsw-alias-scrollbar-*` token 的唯一消费方,必须排在声明这些 token 的 `design-platform.css` 之后。
 
-`gradient-shadow-text.css` 从 `--dsh-content-font-size` 派生 `--dsh-content-font-delta`,并以该增量移动 Markdown 标题与基础文本阶梯。紧凑的表格、小号文本与代码变体保持固定字号。用户气泡与 composer 草稿在该阶梯之外消费相同变量。
+`gradient-shadow-text.css` 从 `--dsh-content-font-size` 派生 `--dsh-content-font-delta`,并以该增量移动 Markdown 标题与基础文本阶梯。它同时派生低一档变量 `--dsh-content-font-size-secondary`(设置 ≤14 时为设置值 −1,>14 时为设置值 −2;默认设置下为 13px)及配套的 `--dsh-content-font-delta-secondary`,供表格变体与比正文低一档的流内行使用。紧凑的小号文本与代码变体保持固定字号。阶梯之外,用户气泡与 composer 草稿直接读取正文档变量对,流内行的标题及摘要读取低一档变量对。
 
 ### 滚动条重新绑定
 

+ 14 - 8
packages/client/ui-theme/src/styles/gradient-shadow-text.css

@@ -22,10 +22,16 @@ body[data-ds-dark-theme] {
    重新从 Figma 导出时需要保留这一缩放。 */
 /* 正文字号调节:--dsh-content-font-size 由主题 bootstrap 与 ThemePresenter
    写在 body 上(整数 px,12–17,默认 14)。标题与正文变体按同一像素增量
-   (--dsh-content-font-delta)随动,保持层级;表格、small、code 属于密集
-   次级文本,保持固定。 */
+   (--dsh-content-font-delta)随动,保持层级。低一档文本(think、工具/bash
+   行的标题与摘要、markdown 表格、流内次级说明等)统一读
+   --dsh-content-font-size-secondary:设置 ≤14 时为设置值 −1,>14 时为
+   设置值 −2(12→11、13→12、14→13、15→13、16→14、17→15)。式中
+   max(13px,·) 的作用是让低设置取 −1 而非 −2,不是结果下限——12 设置下
+   层级触底 11px。small、code 属于密集次级文本,保持固定。 */
 body {
   --dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px);
+  --dsh-content-font-size-secondary: min(calc(var(--dsh-content-font-size, 14px) - 1px), max(13px, calc(var(--dsh-content-font-size, 14px) - 2px)));
+  --dsh-content-font-delta-secondary: calc(var(--dsh-content-font-size-secondary) - 13px);
 
   --dsw-font-markdown-h1: 700 calc(21px + var(--dsh-content-font-delta)) / calc(30px + var(--dsh-content-font-delta)) var(--dsw-font-family);
   --dsw-font-markdown-h1-font-family: var(--dsw-font-family);
@@ -83,18 +89,18 @@ body {
   --dsw-font-markdown-base-strong-italic-font-size: var(--dsh-content-font-size, 14px);
   --dsw-font-markdown-base-strong-italic-font-style: italic;
 
-  --dsw-font-markdown-table: 13px/22px var(--dsw-font-family);
+  --dsw-font-markdown-table: var(--dsh-content-font-size-secondary, 13px)/calc(22px + var(--dsh-content-font-delta-secondary, 0px)) var(--dsw-font-family);
   --dsw-font-markdown-table-font-family: var(--dsw-font-family);
   --dsw-font-markdown-table-font-weight: 400;
-  --dsw-font-markdown-table-line-height: 22px;
-  --dsw-font-markdown-table-font-size: 13px;
+  --dsw-font-markdown-table-line-height: calc(22px + var(--dsh-content-font-delta-secondary, 0px));
+  --dsw-font-markdown-table-font-size: var(--dsh-content-font-size-secondary, 13px);
   --dsw-font-markdown-table-font-style: normal;
 
-  --dsw-font-markdown-table-head: 500 13px/22px var(--dsw-font-family);
+  --dsw-font-markdown-table-head: 500 var(--dsh-content-font-size-secondary, 13px)/calc(22px + var(--dsh-content-font-delta-secondary, 0px)) var(--dsw-font-family);
   --dsw-font-markdown-table-head-font-family: var(--dsw-font-family);
   --dsw-font-markdown-table-head-font-weight: 500;
-  --dsw-font-markdown-table-head-line-height: 22px;
-  --dsw-font-markdown-table-head-font-size: 13px;
+  --dsw-font-markdown-table-head-line-height: calc(22px + var(--dsh-content-font-delta-secondary, 0px));
+  --dsw-font-markdown-table-head-font-size: var(--dsh-content-font-size-secondary, 13px);
   --dsw-font-markdown-table-head-font-style: normal;
 
   --dsw-font-markdown-small: 12px/20px var(--dsw-font-family);

+ 8 - 6
packages/client/ui-tool/src/client/tool/components/ToolRow.module.css

@@ -1,8 +1,10 @@
 /* Tool summary row (figma 122:9479): 24px single line at the default size —
-   [16 leading] gap6 [title 14/24] gap8 [2x2 dot] gap8 [summary FILL truncate].
+   [16 leading] gap6 [title 13/24] gap8 [2x2 dot] gap8 [summary FILL truncate].
    The Settings font-size preference rides the body-published axis: the shared
-   DisclosureRow chrome scales the row height, title, and leading glyph; the
-   summary texts here follow the same size and px delta. */
+   DisclosureRow chrome scales the row height and leading glyph by the body px
+   delta; the title and the summary texts here read the secondary tier
+   (--dsh-content-font-size-secondary: one step under the body — setting −1 at
+   ≤14, setting −2 above), matching think text. */
 
 .root {
   display: flex;
@@ -82,7 +84,7 @@
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
 }
@@ -96,7 +98,7 @@
   flex: none;
   margin-left: 4px;
   white-space: nowrap;
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
 }
@@ -114,7 +116,7 @@
   background: none;
   font: inherit;
   text-align: left;
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-secondary);
   text-decoration: underline;

+ 2 - 2
packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css

@@ -170,7 +170,7 @@
 
 .title {
   flex: none;
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-secondary);
 }
@@ -190,7 +190,7 @@
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   color: var(--dsw-alias-label-tertiary);
 }

+ 4 - 3
packages/client/ui-tool/tests/tool-row-styles.client.spec.ts

@@ -43,12 +43,13 @@ describe('ToolRow.module.css summary line', () => {
     expect(declarations('.summarySuffix')).not.toEqual(expect.arrayContaining(['text-overflow: ellipsis']))
   })
 
-  it('sizes the summary texts from the content font axis', () => {
+  it('sizes the summary texts from the secondary content tier', () => {
     // The Settings font-size preference must reach tool-call rows, not only
-    // the narration body: summary, suffix, and file link ride the axis.
+    // the narration body: summary, suffix, and file link read the secondary
+    // tier (one step under the body), matching think text.
     for (const selector of ['.summary', '.summarySuffix', '.fileLink']) {
       expect(declarations(selector)).toEqual(expect.arrayContaining([
-        'font-size: var(--dsh-content-font-size, 14px)',
+        'font-size: var(--dsh-content-font-size-secondary, 13px)',
         'line-height: calc(24px + var(--dsh-content-font-delta, 0px))',
       ]))
     }

+ 16 - 13
packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css

@@ -37,7 +37,7 @@
   flex: none;
   max-width: 42%;
   color: var(--dsw-alias-label-secondary);
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   font-weight: 510;
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
@@ -49,8 +49,8 @@
   flex: 1;
   min-width: 0;
   color: var(--dsw-alias-label-tertiary);
-  font-size: calc(12px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(18px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -102,7 +102,7 @@
   min-width: 0;
   max-width: 42%;
   color: var(--dsw-alias-label-secondary);
-  font-size: var(--dsh-content-font-size, 14px);
+  font-size: var(--dsh-content-font-size-secondary, 13px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -113,8 +113,8 @@
   flex: 1;
   min-width: 0;
   color: var(--dsw-alias-label-tertiary);
-  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -124,10 +124,10 @@
   flex: none;
   /* Column width scales with the text (~132/13 ≈ 10px per font-size px) so
      larger sizes keep the same status text visible instead of truncating. */
-  width: calc(132px + var(--dsh-content-font-delta, 0px) * 10);
+  width: calc(132px + var(--dsh-content-font-delta-secondary, 0px) * 10);
   color: var(--dsw-alias-label-secondary);
-  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
   text-align: right;
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -223,6 +223,9 @@
   flex: 1;
   min-width: 0;
   color: var(--dsw-alias-label-secondary);
+  /* Body size, not the secondary tier: the member label is the panel's
+     content (an agent's task), while the run/phase headers, counts, and
+     statuses around it are chrome one step down. */
   font-size: var(--dsh-content-font-size, 14px);
   line-height: calc(24px + var(--dsh-content-font-delta, 0px));
   text-overflow: ellipsis;
@@ -234,8 +237,8 @@
   overflow: hidden;
   width: 64px;
   color: var(--dsw-alias-label-secondary);
-  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
   text-align: right;
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -243,8 +246,8 @@
 
 .empty {
   color: var(--dsw-alias-label-tertiary);
-  font-size: calc(13px + var(--dsh-content-font-delta, 0px));
-  line-height: calc(20px + var(--dsh-content-font-delta, 0px));
+  font-size: var(--dsh-content-font-size-secondary, 13px);
+  line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
   padding: 0;
 }
 

+ 8 - 6
packages/client/ui-workflow-run/tests/panel-styles.client.spec.ts

@@ -1,8 +1,10 @@
 /**
  * WorkflowRunPanel's font-size-axis adoption as CSS text. jsdom has no
  * layout, so these read the declarations that make the run/phase headers and
- * the expanded member rows follow the Settings font-size preference through
- * --dsh-content-font-size / --dsh-content-font-delta.
+ * the expanded member rows follow the Settings font-size preference: member
+ * labels at the body size (--dsh-content-font-size / --dsh-content-font-delta),
+ * the chrome around them on the secondary tier
+ * (--dsh-content-font-size-secondary / --dsh-content-font-delta-secondary).
  */
 import { readFileSync } from 'node:fs'
 import { fileURLToPath } from 'node:url'
@@ -31,18 +33,18 @@ describe('WorkflowRunPanel.module.css font-size axis', () => {
     ]))
   })
 
-  it('member status and the empty placeholder keep their 1px step under the body', () => {
+  it('member status and the empty placeholder read the secondary tier', () => {
     for (const selector of ['.memberStatus', '.empty']) {
       expect(declarations(selector)).toEqual(expect.arrayContaining([
-        'font-size: calc(13px + var(--dsh-content-font-delta, 0px))',
-        'line-height: calc(20px + var(--dsh-content-font-delta, 0px))',
+        'font-size: var(--dsh-content-font-size-secondary, 13px)',
+        'line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px))',
       ]))
     }
   })
 
   it('the phase status column widens with the text so larger sizes do not truncate', () => {
     expect(declarations('.phaseStatus')).toEqual(expect.arrayContaining([
-      'width: calc(132px + var(--dsh-content-font-delta, 0px) * 10)',
+      'width: calc(132px + var(--dsh-content-font-delta-secondary, 0px) * 10)',
     ]))
   })
 })

+ 5 - 0
packages/client/ui-workspace/package.json

@@ -32,6 +32,7 @@
   "dsh": {
     "client": {
       "inject": [
+        "@deepseek-ai/dsh-api-remotes",
         "@deepseek-ai/dsh-api-session-controller",
         "@deepseek-ai/dsh-api-workspace-controller",
         "@deepseek-ai/dsh-client-connection",
@@ -53,6 +54,7 @@
     "clsx": "^2.0.0"
   },
   "peerDependencies": {
+    "@deepseek-ai/dsh-api-remotes": "workspace:^",
     "@deepseek-ai/dsh-api-session-controller": "workspace:^",
     "@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
     "@deepseek-ai/dsh-client-connection": "workspace:^",
@@ -63,11 +65,13 @@
     "@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-schedule": "workspace:^",
+    "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-util-workspace-path": "workspace:^"
   },
   "devDependencies": {
+    "@deepseek-ai/dsh-api-remotes": "workspace:^",
     "@deepseek-ai/dsh-api-session-controller": "workspace:^",
     "@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
     "@deepseek-ai/dsh-client-connection": "workspace:^",
@@ -82,6 +86,7 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-schedule": "workspace:^",
+    "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-util-workspace-path": "workspace:^",
     "@types/react": "~18.3.1",

+ 5 - 2
packages/client/ui-workspace/src/client/index.ts

@@ -59,7 +59,9 @@ const NS = 'workspace'
  * provides a waitable service. apply therefore depends on each slot
  * declaration through `slots.inject()` instead of assuming order.
  */
-export const inject = ['slots', 'sessions', 'workspaces', 'locale', 'connection']
+export const inject = [
+  'slots', 'sessions', 'workspaces', 'locale', 'connection', 'remote', 'remote.directoryPicker',
+]
 
 /**
  * Register the browser and picker once their slot declarations are on the
@@ -72,7 +74,8 @@ export function apply(ctx: Context): void {
   const sessions = ctx.get('sessions') as ISessions
   const workspaces = ctx.get('workspaces') as IWorkspaces
   const hostDescription = connection.hostDescription
-  const uiWorkspace = new UiWorkspaceService(ctx, connection.api, workspaces, sessions)
+  const uiWorkspace = new UiWorkspaceService(
+    ctx, connection.api, ctx.remote.directoryPicker, workspaces, sessions)
   ctx.slots.provideRoot({ hooks: { workspaces: workspaces.list } })
   ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-workspace: dictionaries')
 

+ 15 - 15
packages/client/ui-workspace/src/client/navigation.ts

@@ -1,9 +1,9 @@
 /** Workspace archive and directory UI capability. */
 
 import { Service, type Context } from '@deepseek-ai/cordis'
-import type {
-  DirectoryListing, IApiClient, RpcError,
-} from '@deepseek-ai/dsh-client-connection/client'
+import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
+import type { ClientRemote, DirectoryListing } from '@deepseek-ai/dsh-api-remotes/client'
+import type { RemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
 import type {
   ISessions,
   SessionListState,
@@ -69,7 +69,7 @@ export class DirectoryBrowseError extends Error {
   override readonly name = 'DirectoryBrowseError'
 
   /** @param rpcError - Host directory business failure. */
-  constructor(readonly rpcError: RpcError) {
+  constructor(readonly rpcError: RemoteFailure) {
     super(`directory browse failed: ${rpcError.code}: ${rpcError.message}`)
   }
 }
@@ -81,12 +81,14 @@ class UiWorkspaceService extends Service implements UiWorkspace {
   /**
    * @param ctx - Client root Context.
    * @param api - shared Host API carrier.
+   * @param directoryPicker - the directory-picking Remote namespace.
    * @param workspaces - pure Workspace Controller.
    * @param sessions - pure Session Controller.
    */
   constructor(
     ctx: Context,
     private readonly api: IApiClient,
+    private readonly directoryPicker: ClientRemote['directoryPicker'],
     private readonly workspaces: IWorkspaces,
     private readonly sessions: ISessions,
   ) {
@@ -144,23 +146,21 @@ class UiWorkspaceService extends Service implements UiWorkspace {
   }
 
   async pickDirectory(): Promise<string | null> {
-    const response = await this.api.host.pickDirectory({})
-    if (!response.result.ok) {
-      throw new Error(`directory picker failed: ${response.result.error.message}`)
-    }
-    return response.result.value.path
+    const result = await this.directoryPicker.pick()
+    if (!result.ok) throw new Error(`directory picker failed: ${result.error.message}`)
+    return result.value
   }
 
   async listDirectory(path?: string, signal?: AbortSignal): Promise<DirectoryListing> {
-    const response = await this.api.host.listDirectory(path === undefined ? {} : { path }, signal)
-    if (!response.result.ok) throw new DirectoryBrowseError(response.result.error)
-    return response.result.value
+    const result = await this.directoryPicker.list(path, signal)
+    if (!result.ok) throw new DirectoryBrowseError(result.error)
+    return result.value
   }
 
   async createDirectory(path: string, name: string): Promise<string> {
-    const response = await this.api.host.createDirectory({ path, name })
-    if (!response.result.ok) throw new DirectoryBrowseError(response.result.error)
-    return response.result.value.path
+    const result = await this.directoryPicker.createDirectory(path, name)
+    if (!result.ok) throw new DirectoryBrowseError(result.error)
+    return result.value
   }
 
   async openPath(path: string): Promise<void> {

+ 7 - 2
packages/client/ui-workspace/tests/apply.client.spec.ts

@@ -1,6 +1,7 @@
 import { Context } from '@deepseek-ai/cordis'
 import { describe, expect, it, vi } from 'vitest'
 import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
+import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
 import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
 import { apply, inject } from '@deepseek-ai/dsh-client-ui-workspace/client'
 import type { WorkspaceBrowserInjected, WorkspacePickerInjected } from '@deepseek-ai/dsh-client-ui-workspace/client'
@@ -60,6 +61,10 @@ async function bench() {
   ctx.provide('connection', {
     hostDescription: { getSnapshot: () => undefined, subscribe: () => () => {} },
   } as never)
+  const pickDirectory = vi.fn(() => Promise.resolve({ ok: true as const, value: '/projects/picked' }))
+  const directoryPicker = { pick: pickDirectory }
+  Object.assign(new TestRemote(ctx), { directoryPicker })
+  ctx.provide('remote.directoryPicker', directoryPicker as never)
   const locale = new LocaleRuntime(ctx)
   // These specs assert the shipped Chinese copy. There is no jsdom `window`
   // in this lane, so browser-language detection never runs and the locale
@@ -68,7 +73,7 @@ async function bench() {
   ctx.provide('locale', locale)
   return {
     ctx, slots: ctx.get('slots') as SlotRegistry, locale, create, rename,
-    insertSessionBefore, open, clear, search, renameSession, binding, fork,
+    insertSessionBefore, open, clear, search, renameSession, binding, fork, pickDirectory,
   }
 }
 
@@ -83,7 +88,7 @@ function declare(slots: SlotRegistry, ...names: HoleName[]): () => void {
 describe('ui-workspace apply', () => {
   it('declares the services it drives', () => {
     expect(inject).toEqual([
-      'slots', 'sessions', 'workspaces', 'locale', 'connection',
+      'slots', 'sessions', 'workspaces', 'locale', 'connection', 'remote', 'remote.directoryPicker',
     ])
   })
 

+ 6 - 1
packages/client/ui-workspace/tests/rename-assembly.client.spec.tsx

@@ -17,7 +17,7 @@ import type { ISession } from '@deepseek-ai/dsh-api-session-controller/client'
 import type { WorkspaceId } from '@deepseek-ai/dsh-api-workspace-controller/client'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
-import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
+import { SlotTestRuntime, TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
 import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
 import { apply, inject } from '@deepseek-ai/dsh-client-ui-workspace/client'
 
@@ -37,6 +37,11 @@ async function createRuntime(): Promise<SlotTestRuntime> {
   runtime.ctx.provide('connection', {
     hostDescription: { getSnapshot: () => undefined, subscribe: () => () => {} },
   })
+  // The rename flow never picks a directory; the namespace only has to be there
+  // for ui-workspace's inject to settle.
+  const directoryPicker = {}
+  Object.assign(new TestRemote(runtime.ctx), { directoryPicker })
+  runtime.ctx.provide('remote.directoryPicker', directoryPicker as never)
   const locale = new LocaleRuntime(runtime.ctx)
   runtime.ctx.provide('locale', locale)
   runtime.slots.installLocale(locale)

+ 45 - 20
packages/client/ui-workspace/tests/workspaces-service.client.spec.ts

@@ -8,11 +8,12 @@ import type {
 } from '@deepseek-ai/dsh-api-workspace-controller/client'
 import {
   RpcId,
-  type DirectoryListing,
   type IApiClient,
   type RpcError,
   type RpcResponse,
 } from '@deepseek-ai/dsh-client-connection/client'
+import type { ClientRemote, DirectoryListing } from '@deepseek-ai/dsh-api-remotes/client'
+import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
 import { SessionId } from '@deepseek-ai/dsh-session/types'
 import { DirectoryBrowseError, UiWorkspaceService } from '../src/client/navigation.ts'
 
@@ -180,9 +181,6 @@ class FakeApiClient implements IApiClient {
     home: '/home/u',
     canOpenPath: true,
   }))
-  onPickDirectory: IApiClient['host']['pickDirectory'] = () => Promise.resolve(ok({ path: null }))
-  onListDirectory: IApiClient['host']['listDirectory'] = () => Promise.resolve(ok(listing))
-  onCreateDirectory: IApiClient['host']['createDirectory'] = () => Promise.resolve(ok({ path: '/home/u/new' }))
   onOpenPath: IApiClient['host']['openPath'] = () => Promise.resolve(ok({ opened: true }))
 
   declare readonly skills: IApiClient['skills']
@@ -193,9 +191,6 @@ class FakeApiClient implements IApiClient {
 
   readonly host: IApiClient['host'] = {
     describe: (payload, signal) => this.record('host.describe', payload, this.onDescribe(payload, signal)),
-    pickDirectory: (payload, signal) => this.record('host.pickDirectory', payload, this.onPickDirectory(payload, signal)),
-    listDirectory: (payload, signal) => this.record('host.listDirectory', payload, this.onListDirectory(payload, signal)),
-    createDirectory: (payload, signal) => this.record('host.createDirectory', payload, this.onCreateDirectory(payload, signal)),
     openPath: (payload, signal) => this.record('host.openPath', payload, this.onOpenPath(payload, signal)),
   }
 
@@ -209,6 +204,32 @@ class FakeApiClient implements IApiClient {
   }
 }
 
+/** The directory-picking Remote namespace, recorded and scripted per case. */
+class FakeDirectoryPicker {
+  readonly calls: { method: string; payload: unknown }[] = []
+
+  onPick: () => Promise<RemoteResult<string | null>> = () => Promise.resolve({ ok: true, value: null })
+  onList: () => Promise<RemoteResult<DirectoryListing>> = () => Promise.resolve({ ok: true, value: listing })
+  onCreateDirectory: () => Promise<RemoteResult<string>> =
+    () => Promise.resolve({ ok: true, value: '/home/u/new' })
+
+  readonly remote: ClientRemote['directoryPicker'] = {
+    pick: () => this.record('pick', {}, this.onPick()),
+    list: (path?: string) => this.record('list', { path }, this.onList()),
+    createDirectory: (path: string, name: string) =>
+      this.record('createDirectory', { path, name }, this.onCreateDirectory()),
+  }
+
+  callsOf(method: string): unknown[] {
+    return this.calls.filter(call => call.method === method).map(call => call.payload)
+  }
+
+  private record<T>(method: string, payload: unknown, result: Promise<T>): Promise<T> {
+    this.calls.push({ method, payload })
+    return result
+  }
+}
+
 interface BenchOptions {
   readonly workspaces?: WorkspaceSnapshot
   readonly sessions?: SessionListState
@@ -217,15 +238,17 @@ interface BenchOptions {
 function bench(options: BenchOptions = {}) {
   const ctx = new Context()
   const api = new FakeApiClient()
+  const directoryPicker = new FakeDirectoryPicker()
   const workspaces = new FakeWorkspaces(options.workspaces ?? workspaceState([], [], 'pending'))
   const sessions = new FakeSessions(options.sessions ?? sessionState([], undefined, 'pending'))
   const uiWorkspace = new UiWorkspaceService(
     ctx,
     api,
+    directoryPicker.remote,
     workspaces,
     sessions as unknown as ISessions,
   )
-  return { api, ctx, sessions, uiWorkspace, workspaces }
+  return { api, ctx, directoryPicker, sessions, uiWorkspace, workspaces }
 }
 
 async function flush(): Promise<void> {
@@ -451,31 +474,33 @@ describe('UiWorkspaceService', () => {
 
   it('passes directory operations to the Host and preserves structured browse failures', async () => {
     const b = bench()
-    b.api.onPickDirectory = () => Promise.resolve(ok({ path: '/w/alpha' }))
+    b.directoryPicker.onPick = () => Promise.resolve({ ok: true, value: '/w/alpha' })
     await expect(b.uiWorkspace.pickDirectory()).resolves.toBe('/w/alpha')
-    b.api.onPickDirectory = () => Promise.resolve(ok({ path: null }))
+    b.directoryPicker.onPick = () => Promise.resolve({ ok: true, value: null })
     await expect(b.uiWorkspace.pickDirectory()).resolves.toBeNull()
-    expect(b.api.callsOf('host.pickDirectory')).toEqual([{}, {}])
+    expect(b.directoryPicker.callsOf('pick')).toEqual([{}, {}])
 
     await expect(b.uiWorkspace.listDirectory()).resolves.toEqual(listing)
     await expect(b.uiWorkspace.listDirectory('/home/u')).resolves.toEqual(listing)
-    expect(b.api.callsOf('host.listDirectory')).toEqual([{}, { path: '/home/u' }])
+    expect(b.directoryPicker.callsOf('list')).toEqual([{ path: undefined }, { path: '/home/u' }])
     await expect(b.uiWorkspace.createDirectory('/home/u', 'new')).resolves.toBe('/home/u/new')
-    expect(b.api.callsOf('host.createDirectory')).toEqual([{ path: '/home/u', name: 'new' }])
+    expect(b.directoryPicker.callsOf('createDirectory')).toEqual([{ path: '/home/u', name: 'new' }])
     await expect(b.uiWorkspace.openPath('/w/alpha/file.ts')).resolves.toBeUndefined()
     expect(b.api.callsOf('host.openPath')).toEqual([{ path: '/w/alpha/file.ts' }])
 
-    b.api.onPickDirectory = () => Promise.resolve(failed({ code: 'internal', message: 'no chooser', details: {} }))
+    b.directoryPicker.onPick = () => Promise.resolve({
+      ok: false, error: { code: 'internal', message: 'no chooser', details: {} },
+    })
     await expect(b.uiWorkspace.pickDirectory()).rejects.toThrow('directory picker failed: no chooser')
-    b.api.onListDirectory = () => Promise.resolve(failed({
-      code: 'directory-unreadable', message: 'denied', details: { path: '/private' },
-    }))
+    b.directoryPicker.onList = () => Promise.resolve({
+      ok: false, error: { code: 'directory-unreadable', message: 'denied', details: { path: '/private' } },
+    })
     const listFailure = b.uiWorkspace.listDirectory('/private')
     await expect(listFailure).rejects.toBeInstanceOf(DirectoryBrowseError)
     await expect(listFailure).rejects.toMatchObject({ rpcError: { code: 'directory-unreadable' } })
-    b.api.onCreateDirectory = () => Promise.resolve(failed({
-      code: 'directory-exists', message: 'taken', details: { path: '/home/u/new' },
-    }))
+    b.directoryPicker.onCreateDirectory = () => Promise.resolve({
+      ok: false, error: { code: 'directory-exists', message: 'taken', details: { path: '/home/u/new' } },
+    })
     await expect(b.uiWorkspace.createDirectory('/home/u', 'new')).rejects.toMatchObject({
       rpcError: { code: 'directory-exists' },
     })

+ 3 - 0
packages/client/ui-workspace/tsconfig.json

@@ -29,6 +29,9 @@
     {
       "path": "../connection/tsconfig.client.json"
     },
+    {
+      "path": "../../api/remotes/tsconfig.client.json"
+    },
     {
       "path": "../store"
     },

+ 34 - 1
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -768,6 +768,31 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
       },
     ],
   },
+  {
+    key: 'directoryPickerController',
+    summary: 'Host service backing the generated `ctx.remote.directoryPicker` namespace.',
+    description: 'Host service backing the generated `ctx.remote.directoryPicker` namespace. The seam it exports is abstract and therefore never a Loader entry of its own, so this controller carries the wire verbs: one composed backend serves either the native chooser or the browse primitives, and a verb the composition cannot serve is refused rather than approximated.',
+    methods: [
+      {
+        signature: '@Remote(\'pick\') async pick(signal: AbortSignal): Promise<string | null>',
+        description: 'Open the host\'s OS chooser for a Remote caller.',
+        parameters: [{ name: 'signal', description: 'caller lifetime; abort terminates the chooser.' }],
+        returns: 'the chosen absolute path, or null when the operator cancels.',
+      },
+      {
+        signature: '@Remote(\'list\') async list(path: string | undefined, signal: AbortSignal): Promise<DirectoryListing>',
+        description: 'List one directory level for a Remote caller\'s in-app browser.',
+        parameters: [{ name: 'path', description: 'absolute directory to list; absent lists the home directory.' }, { name: 'signal', description: 'caller lifetime; abort stops the backend\'s scan instead of letting it outlive a disconnected caller.' }],
+        returns: 'the level\'s listing with its ancestry.',
+      },
+      {
+        signature: '@Remote(\'createDirectory\') async createDirectory(path: string, name: string): Promise<string>',
+        description: 'Create one child directory for a Remote caller\'s in-app browser.',
+        parameters: [{ name: 'path', description: 'absolute existing parent directory.' }, { name: 'name', description: 'single non-blank path segment.' }],
+        returns: 'the created directory\'s absolute path.',
+      },
+    ],
+  },
   {
     key: 'e2b',
     summary: 'Creates one lazily consumable E2B SDK handle and deletes the sandbox at timeout or disposal.',
@@ -3660,6 +3685,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'DiffResultView',
     declaration: 'export interface DiffResultView {\n    card: \'diff\';\n    title?: string;\n    diffs: FileDiff[];\n}',
   },
+  {
+    name: 'DirectoryEntry',
+    declaration: 'export interface DirectoryEntry {\n    name: string;\n    path: string;\n    hidden: boolean;\n}',
+  },
+  {
+    name: 'DirectoryListing',
+    declaration: 'export interface DirectoryListing {\n    path: string;\n    home: string;\n    crumbs: DirectoryEntry[];\n    entries: DirectoryEntry[];\n    truncated: boolean;\n}',
+  },
   {
     name: 'DirectoryPickerBrowseCapability',
     declaration: 'export interface DirectoryPickerBrowseCapability {\n    kind: \'browse\';\n    list(path?: string, signal?: AbortSignal): Promise<DirectoryListing>;\n    createDirectory(path: string, name: string): Promise<string>;\n}',
@@ -4422,7 +4455,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'RpcErrorDetailsMap',
-    declaration: 'export interface RpcErrorDetailsMap {\n    \'bad-request\': {\n        issues: ZodIssue[];\n    };\n    \'cancelled\': {};\n    \'session-not-found\': {\n        sessionId: SessionId;\n    };\n    \'invalid-time-zone\': {\n        value: string;\n    };\n    \'directory-unreadable\': {\n        path: string;\n    };\n    \'directory-exists\': {\n        path: string;\n    };\n    \'directory-create-failed\': {\n        path: string;\n    };\n    \'directory-picker-unavailable\': {\n        capability: string;\n    };\n    \'agent-preset-read-only\': {\n        agentPreset: string;\n        reason: string;\n    };\n    \'agent-preset-locked\': {\n        sessionId: SessionId;\n        agentPreset: string;\n    };\n    \'agent-preset-not-found\': {\n        agentPreset: string;\n        available: readonly string[];\n    };\n    \'agent-preset-invalid\': {\n        agentPreset: string;\n        reason: string;\n    };\n    \'agent-busy\': {\n        reason: string;\n    };\n    \'settings-rejected\': {\n        ns: string;\n    };\n    \'settings-conflict\': {\n        ns: string;\n        expected: number;\n        actual: number;\n    };\n    \'credential-rejected\': {\n        ref: string;\n    };\n    \'model-discovery-failed\': {\n        settingsNs: string;\n        baseURL?: string;\n    };\n    \'internal\': {};\n}',
+    declaration: 'export interface RpcErrorDetailsMap {\n    \'bad-request\': {\n        issues: ZodIssue[];\n    };\n    \'cancelled\': {};\n    \'session-not-found\': {\n        sessionId: SessionId;\n    };\n    \'invalid-time-zone\': {\n        value: string;\n    };\n    \'agent-preset-read-only\': {\n        agentPreset: string;\n        reason: string;\n    };\n    \'agent-preset-locked\': {\n        sessionId: SessionId;\n        agentPreset: string;\n    };\n    \'agent-preset-not-found\': {\n        agentPreset: string;\n        available: readonly string[];\n    };\n    \'agent-preset-invalid\': {\n        agentPreset: string;\n        reason: string;\n    };\n    \'agent-busy\': {\n        reason: string;\n    };\n    \'settings-rejected\': {\n        ns: string;\n    };\n    \'settings-conflict\': {\n        ns: string;\n        expected: number;\n        actual: number;\n    };\n    \'credential-rejected\': {\n        ref: string;\n    };\n    \'model-discovery-failed\': {\n        settingsNs: string;\n        baseURL?: string;\n    };\n    \'internal\': {};\n}',
   },
   {
     name: 'RpcId',

+ 2 - 2
packages/host/apiproxy/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md
-README.md: 3460d0ec021f658fe91e1ed8b617196d437cb937
-README.zh.md: a6cfac365fe728f26b3afd22ef32e661bd0146ff
+README.md: 9f96999faf26090ccb58763a9301381a4d21d2de
+README.zh.md: 3633880a27e23c2c35706d97b8c905f24a17882d

+ 1 - 2
packages/host/apiproxy/README.md

@@ -40,7 +40,7 @@ The HTTP carrier refuses non-JSON POST bodies with 415 before dispatch, so cross
 
 ### What the gateway exposes
 
-The API is grouped into domains: `sessions` (list, create, history, prompt, cancel, queue, models, selectModel, rename, fork, search, attachment), `workspace`, `host` (describe, pickDirectory, listDirectory, createDirectory, openPath), `skills`, `agentPresets`, `goals`, `settings`, `credentials`, `llm`, `events`, and `downloads`. The sessions, workspace, and events contracts are owned by the Session Controller, Workspace Controller, and API Remotes packages respectively; the remaining domain contracts and the `RpcMethodMap` live in `src/api/`.
+The API is grouped into domains: `sessions` (list, create, history, prompt, cancel, queue, models, selectModel, rename, fork, search, attachment), `workspace`, `host` (describe, openPath), `skills`, `agentPresets`, `goals`, `settings`, `credentials`, `llm`, `events`, and `downloads`. The sessions, workspace, and events contracts are owned by the Session Controller, Workspace Controller, and API Remotes packages respectively; the remaining domain contracts and the `RpcMethodMap` live in `src/api/`.
 
 ### Sessions and history
 
@@ -140,7 +140,6 @@ These limits define where the gateway is a poor fit; they are current package co
 - **Reserved seams stay out of `RpcMethodMap`** — `prompt.mode: 'inject'`, `job.list`, and a describe `hostInstanceId` are documented reservations; model discovery uses `llm.models`. An unknown method fails loud at envelope parse rather than getting a not-implemented code.
 - **No protocol version field** — client and host ship together; `host.describe` gains a version negotiation field only when an independently released client exists.
 - **Search failures include provider diagnostics** — the gateway is a single-user local service; a carrier that exposes it to multiple users must replace internal search details with a public-safe diagnostic.
-- **Linux native picker requires desktop tooling** — under the `native` capability, `host.pickDirectory` reports an actionable error when neither Zenity nor KDialog is installed; the browse backend is the composition-level fallback (see the [native backend README](../directory-picker-native/README.md)).
 - **Cold-list hints degrade only toward visibility and older ordering** — a projection-cache miss or stale `lastPromptAt` falls back to `createdAt` unless an eligible small artifact supplies an exact fold. The [bounded blank-verification decision](../../../.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) owns this safety direction; an authoritative exact recency index remains scoped in the [last-activity-index proposal](../../../.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md).
 
 <a id="dev-note"></a>

+ 1 - 2
packages/host/apiproxy/README.zh.md

@@ -40,7 +40,7 @@ HTTP 载体在分发前以 415 拒绝非 JSON 的 POST 请求体,因此跨站
 
 ### 网关暴露什么
 
-API 按领域分组:`sessions`(list、create、history、prompt、cancel、queue、models、selectModel、rename、fork、search、attachment)、`workspace`、`host`(describe、pickDirectory、listDirectory、createDirectory、openPath)、`skills`、`agentPresets`、`goals`、`settings`、`credentials`、`llm`、`events` 与 `downloads`。sessions、workspace 与 events 契约分别归 Session Controller、Workspace Controller 与 API Remotes 包所有;其余领域契约与 `RpcMethodMap` 位于 `src/api/`。
+API 按领域分组:`sessions`(list、create、history、prompt、cancel、queue、models、selectModel、rename、fork、search、attachment)、`workspace`、`host`(describe、openPath)、`skills`、`agentPresets`、`goals`、`settings`、`credentials`、`llm`、`events` 与 `downloads`。sessions、workspace 与 events 契约分别归 Session Controller、Workspace Controller 与 API Remotes 包所有;其余领域契约与 `RpcMethodMap` 位于 `src/api/`。
 
 ### 会话与历史
 
@@ -140,7 +140,6 @@ API 按领域分组:`sessions`(list、create、history、prompt、cancel、q
 - **预留 seam 不进入 `RpcMethodMap`**——`prompt.mode: 'inject'`、`job.list` 和描述字段 `hostInstanceId` 都是已记录的预留项;模型发现使用 `llm.models`。未知方法会在信封解析时直接失败,而不会返回「尚未实现」错误码。
 - **没有协议版本字段**——客户端与宿主一同发布;只有出现独立发布的客户端后,`host.describe` 才会增加版本协商字段。
 - **搜索失败会包含提供方诊断信息**——网关是单用户本地服务;将其暴露给多名用户的载体必须用可安全公开的诊断信息替代内部搜索细节。
-- **Linux 原生选择器依赖桌面工具**——在 `native` 能力下,Zenity 和 KDialog 均未安装时,`host.pickDirectory` 会给出包含解决建议的错误提示;组合层面的回退是浏览后端(见 [native 后端 README](../directory-picker-native/README.zh.md))。
 - **冷列表提示只向“保持可见、排序偏旧”降级**——projection cache miss 或陈旧的 `lastPromptAt` 会回退到 `createdAt`,除非符合资格的小工件提供精确折叠。[有界空白验证决策](../../../.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md)规定了这个安全方向;权威且精确的最近时间索引仍属于[最后活动索引提案](../../../.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md)的范围。
 
 <a id="dev-note"></a>

+ 0 - 75
packages/host/apiproxy/src/api-proxy.ts

@@ -41,7 +41,6 @@ import type { SettingsDescriptor, SettingsNamespace, SettingsPathOp } from '@dee
 import { credentialRef } from '@deepseek-ai/dsh-credentials'
 import type { ScopeKey } from '@deepseek-ai/dsh-scope'
 import type { RpcError, RpcRequest, RpcResponse } from './api/rpc.ts'
-import { DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker'
 import { canOpenNativePath, openNativePath, openNativeTextFile } from './native-path-opener.ts'
 
 /** Read live abort state across awaits without treating it as synchronously immutable. */
@@ -59,14 +58,6 @@ function err<T>(request: RpcRequest<unknown>, error: RpcError): RpcResponse<T> {
   return { rpcId: request.rpcId, result: { ok: false, error } }
 }
 
-/** Map a browse-primitive failure onto the wire error vocabulary (unknown throws stay internal). */
-function directoryError(error: unknown): RpcError {
-  if (error instanceof DirectoryPickerError) {
-    return { code: error.code, message: error.message, details: { path: error.path } }
-  }
-  return { code: 'internal', message: error instanceof Error ? error.message : String(error), details: {} }
-}
-
 /** Deployment metadata and Host integrations consumed by the API implementation. */
 export interface ApiProxyDefaults {
   /** Current deployment model selection reported by `host.describe`. */
@@ -290,72 +281,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
         }))
       },
 
-      async pickDirectory(request, signal) {
-        const capability = ctx.directoryPicker.capability()
-        if (capability.kind !== 'native') {
-          return err(request, {
-            code: 'directory-picker-unavailable',
-            message: `host.pickDirectory needs the native capability; the composed picker serves "${capability.kind}"`,
-            details: { capability: capability.kind },
-          })
-        }
-        try {
-          const path = await capability.pick(signal)
-          return ok(request, { path })
-        } catch (error: unknown) {
-          if (signal.aborted) {
-            return err(request, {
-              code: 'cancelled',
-              message: 'directory picker was aborted',
-              details: {},
-            })
-          }
-          return err(request, {
-            code: 'internal',
-            message: `directory picker failed: ${error instanceof Error ? error.message : String(error)}`,
-            details: {},
-          })
-        }
-      },
-
-      async listDirectory(request, signal) {
-        const capability = ctx.directoryPicker.capability()
-        if (capability.kind !== 'browse') {
-          return err(request, {
-            code: 'directory-picker-unavailable',
-            message: `host.listDirectory needs the browse capability; the composed picker serves "${capability.kind}"`,
-            details: { capability: capability.kind },
-          })
-        }
-        try {
-          // The carrier's signal follows the caller: a disconnect or timeout
-          // stops the backend's directory scan instead of outliving it.
-          return ok(request, await capability.list(request.payload.path, signal))
-        } catch (error: unknown) {
-          // An abort is the caller's own timeout/disconnect, not a server failure.
-          if (signal.aborted) {
-            return err(request, { code: 'cancelled', message: 'directory listing was aborted', details: {} })
-          }
-          return err(request, directoryError(error))
-        }
-      },
-
-      async createDirectory(request) {
-        const capability = ctx.directoryPicker.capability()
-        if (capability.kind !== 'browse') {
-          return err(request, {
-            code: 'directory-picker-unavailable',
-            message: `host.createDirectory needs the browse capability; the composed picker serves "${capability.kind}"`,
-            details: { capability: capability.kind },
-          })
-        }
-        try {
-          return ok(request, { path: await capability.createDirectory(request.payload.path, request.payload.name) })
-        } catch (error: unknown) {
-          return err(request, directoryError(error))
-        }
-      },
-
       async openPath(request, signal) {
         return openPath(request, request.payload.path, signal)
       },

+ 0 - 44
packages/host/apiproxy/src/api/host.schema.ts

@@ -3,7 +3,6 @@
  */
 
 import { z } from 'zod'
-import type { DirectoryEntry } from './host.ts'
 import type { RequestPayload, ResponseValue } from './rpc-map.ts'
 import type { Wire } from './rpc.schema.ts'
 
@@ -21,49 +20,6 @@ export const hostDescribeValueSchema = z.object({
   canOpenPath: z.boolean(),
 }) satisfies z.ZodType<Wire<ResponseValue<'host.describe'>>>
 
-/** host.pickDirectory request payload (empty object literal). */
-export const hostPickDirectoryRequestSchema = z.object({}) satisfies z.ZodType<Wire<RequestPayload<'host.pickDirectory'>>>
-
-/** host.pickDirectory response value; null means the user cancelled. */
-export const hostPickDirectoryValueSchema = z.object({
-  path: z.string().nullable(),
-}) satisfies z.ZodType<Wire<ResponseValue<'host.pickDirectory'>>>
-
-/** Directory row shared by listing entries and breadcrumb crumbs. */
-export const directoryEntrySchema = z.object({
-  name: z.string(),
-  path: z.string(),
-  hidden: z.boolean(),
-}) satisfies z.ZodType<Wire<DirectoryEntry>>
-
-/** host.listDirectory request payload; an absent path lists the home directory. */
-export const hostListDirectoryRequestSchema = z.object({
-  path: z.string().optional(),
-}) satisfies z.ZodType<Wire<RequestPayload<'host.listDirectory'>>>
-
-/** host.listDirectory response value. */
-export const hostListDirectoryValueSchema = z.object({
-  path: z.string(),
-  home: z.string(),
-  crumbs: z.array(directoryEntrySchema),
-  entries: z.array(directoryEntrySchema),
-  truncated: z.boolean(),
-}) satisfies z.ZodType<Wire<ResponseValue<'host.listDirectory'>>>
-
-/** host.createDirectory request payload: name must be one plain path segment. */
-export const hostCreateDirectoryRequestSchema = z.object({
-  path: z.string(),
-  name: z.string(),
-}).refine(
-  payload => payload.name.trim() !== '' && payload.name !== '.' && payload.name !== '..'
-    && !/[/\\]/.test(payload.name),
-  { message: 'host.createDirectory requires a single non-blank path segment name' },
-) satisfies z.ZodType<Wire<RequestPayload<'host.createDirectory'>>>
-
-/** host.createDirectory response value: the created directory's absolute path. */
-export const hostCreateDirectoryValueSchema = z.object({
-  path: z.string(),
-}) satisfies z.ZodType<Wire<ResponseValue<'host.createDirectory'>>>
 /** host.openPath request payload. */
 export const hostOpenPathRequestSchema = z.object({
   path: z.string().min(1),

+ 0 - 58
packages/host/apiproxy/src/api/host.ts

@@ -5,33 +5,6 @@
 
 import type { RpcRequest, RpcResponse } from './rpc.ts'
 
-/** One directory row of a listing: a child entry or a breadcrumb ancestor. */
-export interface DirectoryEntry {
-  /** Base name shown in a browser row (a root crumb carries its full path). */
-  name: string
-  /** Absolute host path — the client never joins path segments itself. */
-  path: string
-  /** Hidden by the host platform's convention (dot-prefixed on POSIX); the client owns whether to show it. */
-  hidden: boolean
-}
-
-/** host.listDirectory response value: one directory level plus its ancestry. */
-export interface DirectoryListing {
-  /** Absolute path of the listed directory. */
-  path: string
-  /** The host account's home directory (breadcrumb "Home" rooting). */
-  home: string
-  /**
-   * Ancestor chain from the filesystem root to the listed directory
-   * inclusive; every crumb is a jump target (crumb `hidden` is always false).
-   */
-  crumbs: DirectoryEntry[]
-  /** Direct child directories, name-sorted; symlinks to directories included. */
-  entries: DirectoryEntry[]
-  /** True when the backend cut `entries` at its complete-result bound (the name-sorted tail is absent). */
-  truncated: boolean
-}
-
 /** Host-level unary methods. */
 export interface HostApi {
   /**
@@ -54,37 +27,6 @@ export interface HostApi {
     canOpenPath: boolean
   }>>
 
-  /**
-   * Open the operating system's single-directory picker; cancellation returns
-   * null. Only served under the `native` capability.
-   */
-  pickDirectory(
-    request: RpcRequest<{}>,
-    signal: AbortSignal,
-  ): Promise<RpcResponse<{ path: string | null }>>
-
-  /**
-   * List one directory level for the in-app browser; an absent path lists the
-   * host account's home directory. Only served under the `browse` capability;
-   * unreadable or missing targets fail with `directory-unreadable`. The
-   * carrier's request signal follows the caller, stopping the backend's scan
-   * on disconnect or timeout.
-   */
-  listDirectory(
-    request: RpcRequest<{ path?: string }>,
-    signal: AbortSignal,
-  ): Promise<RpcResponse<DirectoryListing>>
-
-  /**
-   * Create one child directory under an existing parent (the browser's
-   * "New folder"). Only served under the `browse` capability; an existing
-   * child fails with `directory-exists`, every other filesystem failure with
-   * `directory-create-failed`.
-   */
-  createDirectory(
-    request: RpcRequest<{ path: string; name: string }>,
-  ): Promise<RpcResponse<{ path: string }>>
-
   /**
    * Open a filesystem path with the operating system's default application
    * (Finder / Explorer / xdg-open hand-off). The browser carrier's

+ 1 - 1
packages/host/apiproxy/src/api/index.ts

@@ -29,7 +29,7 @@ export type {
   ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
   ModelReasoningEffort, ModelSelection,
 } from '@deepseek-ai/dsh-api-session-controller/types'
-export type { DirectoryEntry, DirectoryListing, HostApi } from './host.ts'
+export type { HostApi } from './host.ts'
 export type { SkillsApi, SkillEntry } from './skills.ts'
 export type { AgentPresetsApi } from './agent-presets.ts'
 export type { SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView } from './settings.ts'

+ 0 - 3
packages/host/apiproxy/src/api/rpc-map.ts

@@ -18,9 +18,6 @@ import type { RpcResponse } from './rpc.ts'
  */
 export interface RpcMethodMap {
   'host.describe': HostApi['describe']
-  'host.pickDirectory': HostApi['pickDirectory']
-  'host.listDirectory': HostApi['listDirectory']
-  'host.createDirectory': HostApi['createDirectory']
   'host.openPath': HostApi['openPath']
   'skill.list': SkillsApi['list']
   'agentPreset.openDocument': AgentPresetsApi['openDocument']

+ 0 - 4
packages/host/apiproxy/src/api/rpc.schema.ts

@@ -36,10 +36,6 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
   z.object({ code: z.literal('cancelled'), message: z.string(), details: z.object({}) }),
   z.object({ code: z.literal('session-not-found'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
   z.object({ code: z.literal('invalid-time-zone'), message: z.string(), details: z.object({ value: z.string() }) }),
-  z.object({ code: z.literal('directory-unreadable'), message: z.string(), details: z.object({ path: z.string() }) }),
-  z.object({ code: z.literal('directory-exists'), message: z.string(), details: z.object({ path: z.string() }) }),
-  z.object({ code: z.literal('directory-create-failed'), message: z.string(), details: z.object({ path: z.string() }) }),
-  z.object({ code: z.literal('directory-picker-unavailable'), message: z.string(), details: z.object({ capability: z.string() }) }),
   z.object({ code: z.literal('agent-preset-read-only'), message: z.string(), details: z.object({ agentPreset: z.string(), reason: z.string() }) }),
   z.object({ code: z.literal('agent-preset-locked'), message: z.string(), details: z.object({ sessionId: z.string(), agentPreset: z.string() }) }),
   z.object({ code: z.literal('agent-preset-not-found'), message: z.string(), details: z.object({ agentPreset: z.string(), available: z.array(z.string()) }) }),

+ 0 - 4
packages/host/apiproxy/src/api/rpc.ts

@@ -31,10 +31,6 @@ export interface RpcErrorDetailsMap {
   'cancelled': {}
   'session-not-found': { sessionId: SessionId }
   'invalid-time-zone': { value: string }
-  'directory-unreadable': { path: string }
-  'directory-exists': { path: string }
-  'directory-create-failed': { path: string }
-  'directory-picker-unavailable': { capability: string }
   'agent-preset-read-only': { agentPreset: string; reason: string }
   'agent-preset-locked': { sessionId: SessionId; agentPreset: string }
   'agent-preset-not-found': { agentPreset: string; available: readonly string[] }

+ 8 - 29
packages/host/apiproxy/src/fetch/client.ts

@@ -13,8 +13,7 @@ import { RpcId } from '../api/rpc.ts'
 import type { Wire } from '../api/rpc.schema.ts'
 import { serverResponseSchema } from '../api/rpc.schema.ts'
 import {
-  hostCreateDirectoryValueSchema, hostDescribeValueSchema,
-  hostListDirectoryValueSchema, hostOpenPathValueSchema, hostPickDirectoryValueSchema,
+  hostDescribeValueSchema, hostOpenPathValueSchema,
 } from '../api/host.schema.ts'
 import { skillListValueSchema } from '../api/skills.schema.ts'
 import {
@@ -44,9 +43,6 @@ import { llmDiscoverModelsValueSchema, llmModelsValueSchema, llmProvidersValueSc
 export interface IApiClient {
   host: {
     describe(payload: RequestPayload<'host.describe'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'host.describe'>>>
-    pickDirectory(payload: RequestPayload<'host.pickDirectory'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'host.pickDirectory'>>>
-    listDirectory(payload: RequestPayload<'host.listDirectory'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'host.listDirectory'>>>
-    createDirectory(payload: RequestPayload<'host.createDirectory'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'host.createDirectory'>>>
     openPath(payload: RequestPayload<'host.openPath'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'host.openPath'>>>
   }
   skills: {
@@ -80,9 +76,6 @@ export interface IApiClient {
  */
 const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseValue<K>>> } = {
   'host.describe': hostDescribeValueSchema,
-  'host.pickDirectory': hostPickDirectoryValueSchema,
-  'host.listDirectory': hostListDirectoryValueSchema,
-  'host.createDirectory': hostCreateDirectoryValueSchema,
   'host.openPath': hostOpenPathValueSchema,
   'skill.list': skillListValueSchema,
   'agentPreset.openDocument': agentPresetOpenDocumentValueSchema,
@@ -102,9 +95,6 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
 /** Default timeout for bounded unary calls (rpc-compare 2026-07-19: a hung host must not leave callers pending forever). */
 const DEFAULT_TIMEOUT_MS = 30_000
 
-/** Whether a unary call uses the transport health deadline or only caller/connection cancellation. */
-type UnaryTimeoutPolicy = 'default' | 'caller-signal-only'
-
 /** URL base for in-process handler injection (fake authority, opencode precedent). */
 const INTERNAL_BASE = 'http://dsh.internal'
 
@@ -122,7 +112,7 @@ export abstract class AbstractApiClient implements IApiClient {
   private flushScheduled = false
   private readonly envelopeListeners = new Set<(batch: readonly RpcMessage[]) => void>()
 
-  /** @param timeoutMs - timeout for bounded unary calls; user-paced calls do not use it. */
+  /** @param timeoutMs - timeout for unary calls. */
   constructor(protected readonly timeoutMs: number = DEFAULT_TIMEOUT_MS) {}
 
   /** Transport aspect: browser fetch, injected handler.fetch, IPC bridge, ... */
@@ -178,24 +168,21 @@ export abstract class AbstractApiClient implements IApiClient {
 
   /**
    * Shared POST leg of unary calls: JSON body,
-   * optional default timeout merged with the caller's external signal, non-2xx → transport throw.
+   * default timeout merged with the caller's external signal, non-2xx → transport throw.
    */
   private async postJson(
     path: string,
     body: ClientRequest,
     signal: AbortSignal | undefined,
-    timeoutPolicy: UnaryTimeoutPolicy = 'default',
   ): Promise<Response> {
-    const requestSignal = timeoutPolicy === 'default'
-      ? signal === undefined
-        ? AbortSignal.timeout(this.timeoutMs)
-        : AbortSignal.any([AbortSignal.timeout(this.timeoutMs), signal])
-      : signal
+    const requestSignal = signal === undefined
+      ? AbortSignal.timeout(this.timeoutMs)
+      : AbortSignal.any([AbortSignal.timeout(this.timeoutMs), signal])
     const response = await this.doFetch(new URL(path, this.resolveBase()), {
       method: 'POST',
       headers: { 'content-type': 'application/json' },
       body: JSON.stringify(body),
-      ...requestSignal === undefined ? {} : { signal: requestSignal },
+      signal: requestSignal,
     })
     if (!response.ok) throw new Error(`transport failure for ${path}: HTTP ${response.status}`)
     return response
@@ -210,11 +197,10 @@ export abstract class AbstractApiClient implements IApiClient {
     method: K,
     payload: RequestPayload<K>,
     signal?: AbortSignal,
-    timeoutPolicy: UnaryTimeoutPolicy = 'default',
   ): Promise<RpcResponse<ResponseValue<K>>> {
     const message: ClientRequest = { type: 'client-request', rpcId: this.mintRpcId(), method, payload }
     this.onEnvelope(message)
-    const response = await this.postJson(`/api/${method}`, message, signal, timeoutPolicy)
+    const response = await this.postJson(`/api/${method}`, message, signal)
     const full = serverResponseSchema.parse(await response.json())
     this.onEnvelope(full)
     if (full.rpcId !== message.rpcId) throw new Error(`rpcId mismatch for ${method}: sent ${message.rpcId}, got ${full.rpcId}`)
@@ -229,13 +215,6 @@ export abstract class AbstractApiClient implements IApiClient {
 
   readonly host: IApiClient['host'] = {
     describe: (payload, signal) => this.callUnary('host.describe', payload, signal),
-    // A native system dialog is user-paced and may legitimately stay open
-    // longer than the normal unary deadline. Caller/connection aborts remain.
-    pickDirectory: (payload, signal) => this.callUnary(
-      'host.pickDirectory', payload, signal, 'caller-signal-only',
-    ),
-    listDirectory: (payload, signal) => this.callUnary('host.listDirectory', payload, signal),
-    createDirectory: (payload, signal) => this.callUnary('host.createDirectory', payload, signal),
     openPath: (payload, signal) => this.callUnary('host.openPath', payload, signal),
   }
 

+ 1 - 6
packages/host/apiproxy/src/fetch/handler.ts

@@ -15,9 +15,7 @@ import { RpcId } from '../api/rpc.ts'
 import type { Wire } from '../api/rpc.schema.ts'
 import { clientRequestSchema } from '../api/rpc.schema.ts'
 import {
-  hostCreateDirectoryRequestSchema, hostDescribeRequestSchema,
-  hostListDirectoryRequestSchema, hostOpenPathRequestSchema,
-  hostPickDirectoryRequestSchema,
+  hostDescribeRequestSchema, hostOpenPathRequestSchema,
 } from '../api/host.schema.ts'
 import { skillListRequestSchema } from '../api/skills.schema.ts'
 import {
@@ -50,9 +48,6 @@ type UnaryRoutes = {
 
 const UNARY_ROUTES: UnaryRoutes = {
   'host.describe': { schema: hostDescribeRequestSchema, invoke: (api, r) => api.host.describe(r) },
-  'host.pickDirectory': { schema: hostPickDirectoryRequestSchema, invoke: (api, r, signal) => api.host.pickDirectory(r, signal) },
-  'host.listDirectory': { schema: hostListDirectoryRequestSchema, invoke: (api, r, signal) => api.host.listDirectory(r, signal) },
-  'host.createDirectory': { schema: hostCreateDirectoryRequestSchema, invoke: (api, r) => api.host.createDirectory(r) },
   'host.openPath': { schema: hostOpenPathRequestSchema, invoke: (api, r, signal) => api.host.openPath(r, signal) },
   'skill.list': { schema: skillListRequestSchema, invoke: (api, r) => api.skills.list(r) },
   'agentPreset.openDocument': { schema: agentPresetOpenDocumentRequestSchema, invoke: (api, r, signal) => api.agentPresets.openDocument(r, signal) },

+ 1 - 0
packages/host/apiproxy/src/index.ts

@@ -15,6 +15,7 @@ import { Context, Service } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
 import type {} from '@deepseek-ai/dsh-agent-default-model'
 import type {} from '@deepseek-ai/dsh-api-session-controller'
+import type {} from '@deepseek-ai/dsh-host-directory-picker'
 import type { ApiProxy } from './api/index.ts'
 import { createApiProxy } from './api-proxy.ts'
 import {

+ 4 - 133
packages/host/apiproxy/tests/api-proxy-host.spec.ts

@@ -2,8 +2,6 @@ import { homedir } from 'node:os'
 import { afterEach, describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
-import { DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker'
-import type { DirectoryPickerCapability } from '@deepseek-ai/dsh-host-directory-picker'
 import type { RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
 import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
 import { createApiProxy } from '../src/api-proxy.ts'
@@ -26,7 +24,6 @@ function expectOk<T>(response: { readonly result: { readonly ok: true; readonly
 }
 
 async function harness(
-  picker: DirectoryPickerCapability = { kind: 'native', pick: async () => null },
   extras: {
     openPath?: (path: string, signal: AbortSignal) => Promise<void>
     canOpenPath?: () => boolean
@@ -35,7 +32,6 @@ async function harness(
   const ctx = new Context()
   contexts.push(ctx)
   await ctx.plugin(AgentRegistry)
-  ctx.provide('directoryPicker', { capability: () => picker } as never)
   const api = createApiProxy(ctx, {
     defaultModelSelection: () => ({ provider: 'test', model: 'test-model' }),
     cwd: '/tmp/dsh-apiproxy-host',
@@ -45,135 +41,10 @@ async function harness(
   return { api }
 }
 
-describe('host.pickDirectory', () => {
-  it('returns a selected path or explicit cancellation from the native capability', async () => {
-    const selected = await harness({ kind: 'native', pick: async () => '/tmp/project' })
-    expect((await selected.api.host.pickDirectory(request({}), new AbortController().signal)).result)
-      .toEqual({ ok: true, value: { path: '/tmp/project' } })
-
-    const cancelled = await harness({ kind: 'native', pick: async () => null })
-    expect((await cancelled.api.host.pickDirectory(request({}), new AbortController().signal)).result)
-      .toEqual({ ok: true, value: { path: null } })
-  })
-
-  it('propagates abort into the native capability as a cancelled RPC error', async () => {
-    const { api } = await harness({
-      kind: 'native',
-      pick: signal => new Promise((_resolve, reject) => {
-        signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
-      }),
-    })
-    const abort = new AbortController()
-    const pending = api.host.pickDirectory(request({}), abort.signal)
-    abort.abort()
-    expect((await pending).result).toMatchObject({ ok: false, error: { code: 'cancelled' } })
-  })
-
-  it('folds a non-abort native-chooser failure into an internal error', async () => {
-    const { api } = await harness({
-      kind: 'native',
-      pick: async () => { throw new Error('no chooser installed') },
-    })
-    const response = await api.host.pickDirectory(request({}), new AbortController().signal)
-    expect(response.result).toMatchObject({ ok: false, error: { code: 'internal' } })
-  })
-
-  it('refuses the native RPC under a browse composition', async () => {
-    const { api } = await harness(BROWSE_STUB)
-    const response = await api.host.pickDirectory(request({}), new AbortController().signal)
-    expect(response.result).toMatchObject({
-      ok: false,
-      error: { code: 'directory-picker-unavailable', details: { capability: 'browse' } },
-    })
-  })
-})
-
-const BROWSE_STUB: DirectoryPickerCapability = {
-  kind: 'browse',
-  list: async (path) => {
-    if (path === '/denied') {
-      throw new DirectoryPickerError('directory-unreadable', '/denied', 'cannot list /denied')
-    }
-    const target = path ?? '/home/user'
-    return {
-      path: target,
-      home: '/home/user',
-      crumbs: [{ name: '/', path: '/', hidden: false }],
-      entries: [{ name: 'projects', path: `${target}/projects`, hidden: false }],
-      truncated: false,
-    }
-  },
-  createDirectory: async (path, name) => {
-    if (name === 'taken') {
-      throw new DirectoryPickerError('directory-exists', `${path}/${name}`, 'already exists')
-    }
-    if (name === 'unwritable') throw new Error('disk detached')
-    return `${path}/${name}`
-  },
-}
-
-describe('host.listDirectory / host.createDirectory', () => {
-  it('serves listings and creation through the browse capability, defaulting to home', async () => {
-    const { api } = await harness(BROWSE_STUB)
-    const home = await api.host.listDirectory(request({}), new AbortController().signal)
-    expect(home.result).toMatchObject({ ok: true, value: { path: '/home/user', home: '/home/user' } })
-    const listed = await api.host.listDirectory(
-      request({ path: '/home/user/projects' }),
-      new AbortController().signal,
-    )
-    expect(listed.result).toMatchObject({ ok: true, value: { path: '/home/user/projects' } })
-    const created = await api.host.createDirectory(request({ path: '/home/user', name: 'fresh' }))
-    expect(created.result).toEqual({ ok: true, value: { path: '/home/user/fresh' } })
-  })
-
-  it('maps typed picker failures onto wire errors and folds unknown throws to internal', async () => {
-    const { api } = await harness(BROWSE_STUB)
-    expect((await api.host.listDirectory(
-      request({ path: '/denied' }),
-      new AbortController().signal,
-    )).result).toMatchObject({
-      ok: false,
-      error: { code: 'directory-unreadable', details: { path: '/denied' } },
-    })
-    expect((await api.host.createDirectory(request({ path: '/home/user', name: 'taken' }))).result)
-      .toMatchObject({ ok: false, error: { code: 'directory-exists' } })
-    expect((await api.host.createDirectory(request({ path: '/home/user', name: 'unwritable' }))).result)
-      .toMatchObject({ ok: false, error: { code: 'internal' } })
-  })
-
-  it('reports an aborted listing as cancelled', async () => {
-    const { api } = await harness({
-      kind: 'browse',
-      list: (_path, signal) => new Promise((_resolve, reject) => {
-        signal?.addEventListener('abort', () => { reject(new Error('scan aborted')) }, { once: true })
-      }),
-      createDirectory: async () => '/never',
-    })
-    const abort = new AbortController()
-    const pending = api.host.listDirectory(request({}), abort.signal)
-    abort.abort()
-    expect((await pending).result).toMatchObject({ ok: false, error: { code: 'cancelled' } })
-  })
-
-  it('refuses the browse RPCs under a native composition', async () => {
-    const { api } = await harness()
-    expect((await api.host.listDirectory(request({}), new AbortController().signal)).result)
-      .toMatchObject({
-        ok: false,
-        error: { code: 'directory-picker-unavailable', details: { capability: 'native' } },
-      })
-    expect((await api.host.createDirectory(request({ path: '/x', name: 'y' }))).result)
-      .toMatchObject({
-        ok: false,
-        error: { code: 'directory-picker-unavailable', details: { capability: 'native' } },
-      })
-  })
-})
-
 describe('host.openPath', () => {
   it('describes whether the deployment can reach a native desktop', async () => {
-    const visible = await harness(undefined, { canOpenPath: () => true })
-    const headless = await harness(undefined, { canOpenPath: () => false })
+    const visible = await harness({ canOpenPath: () => true })
+    const headless = await harness({ canOpenPath: () => false })
     expect(expectOk(await visible.api.host.describe(request({}))).canOpenPath).toBe(true)
     expect(expectOk(await headless.api.host.describe(request({}))).canOpenPath).toBe(false)
     expect(expectOk(await visible.api.host.describe(request({}))).home).toBe(homedir())
@@ -181,7 +52,7 @@ describe('host.openPath', () => {
 
   it('opens through the injected native boundary', async () => {
     const opened: string[] = []
-    const { api } = await harness(undefined, {
+    const { api } = await harness({
       openPath: async (path) => { opened.push(path) },
     })
     expect((await api.host.openPath(
@@ -192,7 +63,7 @@ describe('host.openPath', () => {
   })
 
   it('propagates abort into the native boundary as a cancelled RPC error', async () => {
-    const { api } = await harness(undefined, {
+    const { api } = await harness({
       openPath: (_path, signal) => new Promise((_resolve, reject) => {
         signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true })
       }),

+ 0 - 3
packages/host/apiproxy/tests/client-handler.spec.ts

@@ -29,9 +29,6 @@ function scriptedApi(overrides: {
       describe: r => ok(r, {
         version: '0-test', cwd: '/t', attachedSessions: 0, home: '/h', canOpenPath: true,
       }),
-      pickDirectory: r => ok(r, { path: null }),
-      listDirectory: r => ok(r, { path: '/t', home: '/t', crumbs: [], entries: [], truncated: false }),
-      createDirectory: r => ok(r, { path: '/t/new' }),
       openPath: r => ok(r, { opened: true as const }),
       ...overrides.host,
     },

+ 7 - 70
packages/host/apiproxy/tests/fetch-carrier.spec.ts

@@ -18,15 +18,6 @@ function fakeApi(overrides: Partial<{ crashOn: string }> = {}): ApiProxy {
           },
         }
       },
-      async pickDirectory(request) {
-        return { rpcId: request.rpcId, result: { ok: true, value: { path: null } } }
-      },
-      async listDirectory(request) {
-        return { rpcId: request.rpcId, result: { ok: true, value: { path: '/w', home: '/w', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false } } }
-      },
-      async createDirectory(request) {
-        return { rpcId: request.rpcId, result: { ok: true, value: { path: '/w/new' } } }
-      },
       async openPath(request) {
         return { rpcId: request.rpcId, result: { ok: true, value: { opened: true as const } } }
       },
@@ -124,29 +115,6 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
       .toEqual({ ok: true, value: { opened: true } })
   })
 
-  it('round-trips the native picker without the default unary timeout', async () => {
-    const api = fakeApi()
-    api.host.pickDirectory = async (request) => {
-      await new Promise(resolve => setTimeout(resolve, 15))
-      return { rpcId: request.rpcId, result: { ok: true, value: { path: '/tmp/project' } } }
-    }
-    const response = await client(api, 1).host.pickDirectory({})
-    expect(response.result).toEqual({ ok: true, value: { path: '/tmp/project' } })
-  })
-
-  it('round-trips the browse listing and creation calls through the wire form', async () => {
-    const c = client()
-    const listed = await c.host.listDirectory({ path: '/w' })
-    expect(listed.result).toEqual({
-      ok: true,
-      value: { path: '/w', home: '/w', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false },
-    })
-    const home = await c.host.listDirectory({})
-    expect(home.result).toMatchObject({ ok: true, value: { home: '/w' } })
-    const created = await c.host.createDirectory({ path: '/w', name: 'fresh' })
-    expect(created.result).toEqual({ ok: true, value: { path: '/w/new' } })
-  })
-
   it('round-trips host.openPath through the wire form', async () => {
     const api = fakeApi()
     let opened: string | undefined
@@ -165,41 +133,10 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
     expect(skills.result).toEqual({ ok: true, value: { skills: [{ name: 'commit-helper', description: 'Git commits', modelInvocable: true }] } })
   })
 
-  it('lets host.pickDirectory finish after the 30-second default unary deadline', async () => {
-    vi.useFakeTimers()
-    const timeoutSpy = vi.spyOn(AbortSignal, 'timeout').mockImplementation((milliseconds) => {
-      const controller = new AbortController()
-      setTimeout(() => {
-        controller.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
-      }, milliseconds)
-      return controller.signal
-    })
-    try {
-      const api = fakeApi()
-      api.host.pickDirectory = async (request) => {
-        await new Promise(resolve => setTimeout(resolve, 30_001))
-        return { rpcId: request.rpcId, result: { ok: true, value: { path: '/tmp/slow' } } }
-      }
-      const execution = client(api).host.pickDirectory({})
-      const assertion = expect(execution).resolves.toMatchObject({
-        result: { ok: true, value: { path: '/tmp/slow' } },
-      })
-
-      await Promise.all([
-        vi.advanceTimersByTimeAsync(30_001),
-        assertion,
-      ])
-      expect(timeoutSpy).not.toHaveBeenCalled()
-    } finally {
-      timeoutSpy.mockRestore()
-      vi.useRealTimers()
-    }
-  })
-
-  it('keeps caller and connection aborts on a deadline-exempt unary', async () => {
+  it('keeps caller and connection aborts on a signal-taking unary', async () => {
     const api = fakeApi()
     const started = Promise.withResolvers<AbortSignal>()
-    api.host.pickDirectory = async (request, signal) => {
+    api.host.openPath = async (request, signal) => {
       started.resolve(signal)
       if (!signal.aborted) {
         await new Promise<void>((resolve) => {
@@ -212,7 +149,7 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
       }
     }
     const controller = new AbortController()
-    const execution = client(api).host.pickDirectory({}, controller.signal)
+    const execution = client(api).host.openPath({ path: '/tmp/a.txt' }, controller.signal)
     const handlerSignal = await started.promise
 
     controller.abort(new Error('connection closed'))
@@ -221,9 +158,9 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
     expect(handlerSignal.aborted).toBe(true)
   })
 
-  it('propagates the carrier Request signal into host.pickDirectory', async () => {
+  it('propagates the carrier Request signal into host.openPath', async () => {
     const api = fakeApi()
-    api.host.pickDirectory = async (request, signal) => {
+    api.host.openPath = async (request, signal) => {
       if (!signal.aborted) {
         await new Promise<void>((resolve) => {
           signal.addEventListener('abort', () => { resolve() }, { once: true })
@@ -236,8 +173,8 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
     }
     const handler = toFetchHandler(api)
     const controller = new AbortController()
-    const body = JSON.stringify({ type: 'client-request', rpcId: 'r-picker', method: 'host.pickDirectory', payload: {} })
-    const pending = handler.fetch(new Request('http://x/api/host.pickDirectory', {
+    const body = JSON.stringify({ type: 'client-request', rpcId: 'r-opener', method: 'host.openPath', payload: { path: '/tmp/a.txt' } })
+    const pending = handler.fetch(new Request('http://x/api/host.openPath', {
       method: 'POST', headers: { 'content-type': 'application/json' }, body, signal: controller.signal,
     }))
     controller.abort()

+ 1 - 30
packages/host/apiproxy/tests/rpc-schemas.spec.ts

@@ -5,11 +5,7 @@ import {
   rpcResultSchema, serverResponseSchema,
 } from '../src/api/rpc.schema.ts'
 import { z } from 'zod'
-import {
-  hostCreateDirectoryRequestSchema, hostCreateDirectoryValueSchema,
-  hostDescribeRequestSchema, hostDescribeValueSchema,
-  hostListDirectoryRequestSchema, hostListDirectoryValueSchema,
-} from '../src/api/host.schema.ts'
+import { hostDescribeRequestSchema, hostDescribeValueSchema } from '../src/api/host.schema.ts'
 import { skillEntrySchema, skillListRequestSchema, skillListValueSchema } from '../src/api/skills.schema.ts'
 import { agentPresetOpenDocumentValueSchema } from '../src/api/agent-presets.schema.ts'
 
@@ -36,10 +32,6 @@ describe('rpcErrorSchema', () => {
     expect(rpcErrorSchema.parse({ code: 'cancelled', message: 'm', details: {} }).code).toBe('cancelled')
     expect(rpcErrorSchema.parse({ code: 'session-not-found', message: 'm', details: { sessionId: 's' } }).code).toBe('session-not-found')
     expect(rpcErrorSchema.parse({ code: 'invalid-time-zone', message: 'm', details: { value: 'CST' } }).code).toBe('invalid-time-zone')
-    expect(rpcErrorSchema.parse({ code: 'directory-unreadable', message: 'm', details: { path: '/x' } }).code).toBe('directory-unreadable')
-    expect(rpcErrorSchema.parse({ code: 'directory-exists', message: 'm', details: { path: '/x' } }).code).toBe('directory-exists')
-    expect(rpcErrorSchema.parse({ code: 'directory-create-failed', message: 'm', details: { path: '/x' } }).code).toBe('directory-create-failed')
-    expect(rpcErrorSchema.parse({ code: 'directory-picker-unavailable', message: 'm', details: { capability: 'none' } }).code).toBe('directory-picker-unavailable')
     expect(rpcErrorSchema.parse({ code: 'agent-preset-read-only', message: 'm', details: { agentPreset: 'p', reason: 'system' } }).code).toBe('agent-preset-read-only')
     expect(rpcErrorSchema.parse({ code: 'agent-preset-locked', message: 'm', details: { sessionId: 's', agentPreset: 'p' } }).code).toBe('agent-preset-locked')
     expect(rpcErrorSchema.parse({ code: 'agent-preset-not-found', message: 'm', details: { agentPreset: 'p', available: [] } }).code).toBe('agent-preset-not-found')
@@ -55,7 +47,6 @@ describe('rpcErrorSchema', () => {
 
   it('rejects a known code with missing details', () => {
     expect(() => rpcErrorSchema.parse({ code: 'agent-busy', message: 'm', details: {} })).toThrow()
-    expect(() => rpcErrorSchema.parse({ code: 'directory-unreadable', message: 'm', details: {} })).toThrow()
     expect(() => rpcErrorSchema.parse({ code: 'internal', message: 'm' })).toThrow()
     expect(() => rpcErrorSchema.parse({ code: 'nope', message: 'm', details: {} })).toThrow()
   })
@@ -109,26 +100,6 @@ describe('host domain schemas', () => {
       version: '1', cwd: '/x', attachedSessions: 0, canOpenPath: true,
     })).toThrow()
   })
-
-  it('validates the browse listing/creation payloads', () => {
-    expect(hostListDirectoryRequestSchema.parse({})).toEqual({})
-    expect(hostListDirectoryRequestSchema.parse({ path: '/x' })).toEqual({ path: '/x' })
-    const listing = hostListDirectoryValueSchema.parse({
-      path: '/home/u/p',
-      home: '/home/u',
-      crumbs: [{ name: '/', path: '/', hidden: false }, { name: 'p', path: '/home/u/p', hidden: false }],
-      entries: [{ name: '.dot', path: '/home/u/p/.dot', hidden: true }],
-      truncated: false,
-    })
-    expect(listing.entries[0]?.hidden).toBe(true)
-    // The flag is part of the wire value, not an optional decoration.
-    expect(() => hostListDirectoryValueSchema.parse({ path: '/x', home: '/x', crumbs: [], entries: [] })).toThrow()
-    expect(hostCreateDirectoryRequestSchema.parse({ path: '/x', name: 'new' })).toEqual({ path: '/x', name: 'new' })
-    for (const name of ['', ' ', '.', '..', 'a/b', 'a\\b']) {
-      expect(() => hostCreateDirectoryRequestSchema.parse({ path: '/x', name })).toThrow()
-    }
-    expect(hostCreateDirectoryValueSchema.parse({ path: '/x/new' })).toEqual({ path: '/x/new' })
-  })
 })
 
 describe('skills domain schemas', () => {

+ 2 - 2
packages/host/directory-picker-browse/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/host/directory-picker-browse/README.md
-README.md: 0b161afa4b70c291d20523507b62789c4856d29a
-README.zh.md: 4207a1eccdb913b322733f9c1e56f79d298c16b7
+README.md: 986504a48da5b1ea28c174168eec41e40bb77e4e
+README.zh.md: 54a722bdad2d88e6cadc6f39c74f479851197351

+ 1 - 1
packages/host/directory-picker-browse/README.md

@@ -25,7 +25,7 @@ Users who cannot reach an OS chooser still pick a workspace directory through `d
 <a id="use-this-package"></a>
 ## Use this package
 
-Compose this backend when a workspace directory must be chosen without an OS chooser — remote browsers, SSH-forwarded sessions, or unattended hosts. The workspace flow drives `host.listDirectory` and `host.createDirectory`; both primitives answer from the host filesystem.
+Compose this backend when a workspace directory must be chosen without an OS chooser — remote browsers, SSH-forwarded sessions, or unattended hosts. The workspace flow drives `directoryPicker/list` and `directoryPicker/createDirectory`; both primitives answer from the host filesystem.
 
 ### Listing a directory
 

+ 1 - 1
packages/host/directory-picker-browse/README.zh.md

@@ -25,7 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-当工作区目录必须在没有 OS 选择器的情况下被选中时——远程浏览器、SSH 转发会话或无人值守宿主——组合此后端。工作区流程驱动 `host.listDirectory` 与 `host.createDirectory`;两个原语都从宿主文件系统作答。
+当工作区目录必须在没有 OS 选择器的情况下被选中时——远程浏览器、SSH 转发会话或无人值守宿主——组合此后端。工作区流程驱动 `directoryPicker/list` 与 `directoryPicker/createDirectory`;两个原语都从宿主文件系统作答。
 
 ### 列举目录
 

+ 2 - 2
packages/host/directory-picker-browse/src/index.ts

@@ -303,8 +303,8 @@ export default class BrowseDirectoryPicker extends DirectoryPicker {
       throw new DirectoryPickerError('directory-create-failed', path, `cannot create under "${path}": not a fully qualified parent path`)
     }
     const parent = resolve(path)
-    // The backend owns segment validation (the wire schema also refuses these,
-    // but direct service consumers must hit the same fence).
+    // The backend owns segment validation; the Remote controller also refuses
+    // invalid wire input, but direct service consumers must hit the same fence.
     if (name.trim() === '' || name === '.' || name === '..' || /[/\\]/.test(name)) {
       throw new DirectoryPickerError('directory-create-failed', join(parent, name), `"${name}" is not a single path segment`)
     }

+ 2 - 2
packages/host/directory-picker-native/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/host/directory-picker-native/README.md
-README.md: 9bea790348293398b800271cfee2dce75d02072a
-README.zh.md: 53c582c33a55397c1e0fe526e5b92ebc270fe8f7
+README.md: 420ec71899366733c9be538ca5900644e81b668d
+README.zh.md: a6671d2f93fbc605bfa74d73e80d835cb0c7824e

+ 1 - 1
packages/host/directory-picker-native/README.md

@@ -33,7 +33,7 @@ Choose this backend for a workstation-local operator on macOS, Windows, or deskt
 
 ### What an operator experiences
 
-Each call opens one native chooser on the host display and waits for the operator; aborting the caller's signal terminates the chooser process instead of leaving it open. On Linux the chooser needs either Zenity or KDialog installed; with neither present, `pick` rejects with an actionable error instead of falling back to a typed-path prompt. The browser half of this package registers a renderless flow occupant into the workspace flow — every `open` request drives `host.pickDirectory` and reports the one outcome (picked path, cancel, or failure).
+Each call opens one native chooser on the host display and waits for the operator; aborting the caller's signal terminates the chooser process instead of leaving it open. On Linux the chooser needs either Zenity or KDialog installed; with neither present, `pick` rejects with an actionable error instead of falling back to a typed-path prompt. The browser half of this package registers a renderless flow occupant into the workspace flow — every `open` request drives `directoryPicker/pick` and reports the one outcome (picked path, cancel, or failure).
 
 ### Observable failures
 

+ 1 - 1
packages/host/directory-picker-native/README.zh.md

@@ -33,7 +33,7 @@ kind: "package-reference"
 
 ### 操作者会看到什么
 
-每次调用在宿主屏幕上打开一个原生选择器并等待操作者;中止调用方的信号会终止选择器进程,而不是让它留在屏幕上。Linux 上选择器需要安装 Zenity 或 KDialog 之一;两者都没有时,`pick` 以包含解决建议的错误拒绝,而不会回退为手输路径提示。本包的 browser 半侧向工作区流程注册一个无渲染的流程占用者——每次 `open` 请求驱动 `host.pickDirectory`,并上报唯一结果(所选路径、取消或失败)。
+每次调用在宿主屏幕上打开一个原生选择器并等待操作者;中止调用方的信号会终止选择器进程,而不是让它留在屏幕上。Linux 上选择器需要安装 Zenity 或 KDialog 之一;两者都没有时,`pick` 以包含解决建议的错误拒绝,而不会回退为手输路径提示。本包的 browser 半侧向工作区流程注册一个无渲染的流程占用者——每次 `open` 请求驱动 `directoryPicker/pick`,并上报唯一结果(所选路径、取消或失败)。
 
 ### 可观察的失败
 

+ 2 - 2
packages/host/directory-picker/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/host/directory-picker/README.md
-README.md: 93afde4ec012fe366f0a44e8d9632596876f6b69
-README.zh.md: 1c9942cdc4271b7201d7086868d01da23ccd4d46
+README.md: bcd2c4dbfa2848e669edbc9f0ebba93804df622c
+README.zh.md: 8604b54c99e88cedfedf820e3dd0f8800f9c5972

+ 2 - 2
packages/host/directory-picker/README.md

@@ -29,11 +29,11 @@ Mount exactly one directory-picker backend and let the workspace flow drive it:
 
 ### Choosing a backend
 
-The [native backend](../directory-picker-native/README.md) is the right choice when the operator sits at the host's display: `host.pickDirectory` opens one OS chooser and returns the chosen absolute path, or `null` on cancel. The [browse backend](../directory-picker-browse/README.md) works everywhere — it lists one directory level and creates child directories from the browser, so remote clients that cannot reach an OS dialog still pick a workspace. When the host situation varies between boots, compose the [adaptive chooser](../directory-picker-auto/README.md), which resolves the situation once at boot and mounts the matching backend.
+The [native backend](../directory-picker-native/README.md) is the right choice when the operator sits at the host's display: `directoryPicker/pick` opens one OS chooser and returns the chosen absolute path, or `null` on cancel. The [browse backend](../directory-picker-browse/README.md) works everywhere — it lists one directory level and creates child directories from the browser, so remote clients that cannot reach an OS dialog still pick a workspace. When the host situation varies between boots, compose the [adaptive chooser](../directory-picker-auto/README.md), which resolves the situation once at boot and mounts the matching backend.
 
 ### The capability contract
 
-`capability()` returns a discriminated union describing how an operator selects a directory: `{ kind: 'native', pick(signal) }` for the OS chooser, or `{ kind: 'browse', list(path?), createDirectory(path, name) }` for the in-app browser. Consumers switch on `kind`; a capability kind no composition implements means the UI hides the picking affordance rather than failing. Browse failures throw the typed `DirectoryPickerError` with a closed code set — `directory-unreadable`, `directory-exists`, or `directory-create-failed` — each carrying the subject path, which the consuming gateway maps onto wire error codes.
+`capability()` returns a discriminated union describing how an operator selects a directory: `{ kind: 'native', pick(signal) }` for the OS chooser, or `{ kind: 'browse', list(path?), createDirectory(path, name) }` for the in-app browser. Consumers switch on `kind`; a capability kind no composition implements means the UI hides the picking affordance rather than failing. Browse failures throw the typed `DirectoryPickerError` with a closed code set — `directory-unreadable`, `directory-exists`, or `directory-create-failed` — each carrying the subject path, which the picking Remote controller maps onto wire failure codes.
 
 ### What rows carry
 

Деякі файли не було показано, через те що забагато файлів було змінено