ソースを参照

refactor(client): consume migrated Remote namespaces

imccyu 1 ヶ月 前
親
コミット
5b2f679e4a
25 ファイル変更、219 行追加、212 行削除
  1. 12 8
      packages/api/remotes/src/client/index.ts
  2. 0 3
      packages/client/connection/src/client/api.ts
  3. 74 83
      packages/client/connection/src/client/fixture.ts
  4. 0 3
      packages/client/connection/src/client/index.ts
  5. 1 1
      packages/client/ui-agent-preset/src/client/index.ts
  6. 8 9
      packages/client/ui-agent-preset/src/client/section-store.ts
  7. 1 3
      packages/client/ui-chat/package.json
  8. 6 5
      packages/client/ui-chat/src/client/apply.ts
  9. 0 3
      packages/client/ui-chat/tsconfig.json
  10. 0 3
      packages/client/ui-model-selection/package.json
  11. 8 11
      packages/client/ui-model-selection/src/client/catalog.ts
  12. 1 1
      packages/client/ui-model-selection/src/client/index.ts
  13. 2 5
      packages/client/ui-model-selection/src/client/service.ts
  14. 2 2
      packages/client/ui-settings-general/src/client/index.ts
  15. 4 4
      packages/client/ui-settings-general/src/client/settings-document-store.ts
  16. 0 2
      packages/client/ui-settings-models/package.json
  17. 9 10
      packages/client/ui-settings-models/src/client/ModelListEditor.tsx
  18. 6 8
      packages/client/ui-settings-models/src/client/index.ts
  19. 3 3
      packages/client/ui-settings-models/src/client/slot-contract.ts
  20. 64 12
      packages/client/ui-settings-models/src/client/store.ts
  21. 4 4
      packages/client/ui-settings-plugins/src/client/index.ts
  22. 7 7
      packages/client/ui-settings-plugins/src/client/subagent-model-selection-card-controller.ts
  23. 6 6
      packages/client/ui-skill/src/client/index.ts
  24. 1 1
      packages/client/ui-workspace/src/client/index.ts
  25. 0 15
      packages/client/ui-workspace/src/client/navigation.ts

+ 12 - 8
packages/api/remotes/src/client/index.ts

@@ -5,8 +5,8 @@ import agentPresetsRemote from '@deepseek-ai/dsh-agent-presets/remote'
 import commandsRemote from '@deepseek-ai/dsh-commands/remote'
 import settingsControllerRemote from '@deepseek-ai/dsh-api-settings-controller/remote'
 import goalsRemote from '@deepseek-ai/dsh-goal/remote'
+import llmRemote from '@deepseek-ai/dsh-llm/remote'
 import dynamicRemote from '@deepseek-ai/dsh-cordis-host-runner/remote'
-import fileReferencesRemote from '@deepseek-ai/dsh-file-reference/remote'
 import pluginInventoryRemote from '@deepseek-ai/dsh-host-plugin-inventory/remote'
 import messageFeedbackRemote from '@deepseek-ai/dsh-message-feedback/remote'
 import sessionReferencesRemote from '@deepseek-ai/dsh-session-reference/remote'
@@ -20,8 +20,8 @@ export type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inven
 export type {} from '@deepseek-ai/dsh-agent-presets/remote'
 export type {} from '@deepseek-ai/dsh-commands/remote'
 export type {} from '@deepseek-ai/dsh-api-settings-controller/remote'
-export type {} from '@deepseek-ai/dsh-file-reference/remote'
 export type {} from '@deepseek-ai/dsh-goal/remote'
+export type {} from '@deepseek-ai/dsh-llm/remote'
 export type {} from '@deepseek-ai/dsh-host-plugin-inventory/remote'
 export type {} from '@deepseek-ai/dsh-message-feedback/remote'
 export type {} from '@deepseek-ai/dsh-session-reference/remote'
@@ -54,11 +54,10 @@ export type {} from '@deepseek-ai/dsh-api-session-controller/types'
  * the carrier's runtime values stay behind their own module edge.
  */
 export type {
-  ConfigurableProviderView, ConnectionHandle, ConnectionSinks, ContentBlock,
-  DiscoveredModelView, IApiClient,
-  MessageId, ModelCatalog, ModelCatalogFailure, ModelProviderGroup, ModelReasoningEffort, ModelSelection,
+  ConnectionHandle, ConnectionSinks, ContentBlock, IApiClient,
+  MessageId, ModelCatalogFailure, ModelProviderGroup, ModelReasoningEffort, ModelSelection,
   RpcError, RpcId, RpcRequest, RpcResponse, RpcResult, SessionId,
-  SkillEntry, StreamChunk,
+  StreamChunk,
 } from '@deepseek-ai/dsh-client-connection/client'
 export type {} from '@deepseek-ai/dsh-api-gateway/client'
 export type {} from '@deepseek-ai/dsh-cordis-host-runner/remote'
@@ -111,6 +110,11 @@ export type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
 export type {
   SettingsDescribeValue, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
 } from '@deepseek-ai/dsh-settings/types'
+// Provider registry and discovery vocabulary for the llm namespace.
+export type {
+  LlmConfigurableProvider, LlmDiscoveredModel, LlmModelDiscoveryError,
+  LlmModelDiscoveryRequest, LlmProviderInfo,
+} from '@deepseek-ai/dsh-llm/types'
 // Reference-discovery result vocabulary for the fileReferences and
 // sessionReferenceResolver namespaces.
 export type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
@@ -123,6 +127,7 @@ export type ClientFailure =
   | import('@deepseek-ai/dsh-api-session-controller/types').SessionError
   | import('@deepseek-ai/dsh-api-settings-controller/types').CredentialError
   | import('@deepseek-ai/dsh-api-settings-controller/types').SettingsError
+  | import('@deepseek-ai/dsh-llm/types').LlmModelDiscoveryError
   | import('@deepseek-ai/dsh-subagent/client').SubagentControlError
   | import('@deepseek-ai/dsh-api-workspace-controller/types').WorkspaceError
 
@@ -150,8 +155,7 @@ export async function apply(ctx: Context): Promise<() => Promise<void>> {
   const disposers: Array<() => Promise<void>> = []
   try {
     for (const contribution of [
-      agentPresetsRemote, commandsRemote, settingsControllerRemote, goalsRemote, dynamicRemote,
-      fileReferencesRemote,
+      agentPresetsRemote, commandsRemote, settingsControllerRemote, goalsRemote, llmRemote, dynamicRemote,
       pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote,
       subagentsRemote, sessionRemote, workspaceRemote,
     ]) {

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

@@ -8,11 +8,8 @@
 export type {
   ApiProxy, HostApi,
   ResponseValue,
-  SkillsApi, SkillEntry,
   ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
   ModelReasoningEffort, ModelSelection,
-  SettingsApi,
-  ConfigurableProviderView, DiscoveredModelView, LlmApi,
 } from '@deepseek-ai/dsh-host-apiproxy/api'
 export type {
   RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,

+ 74 - 83
packages/client/connection/src/client/fixture.ts

@@ -551,7 +551,7 @@ const OPENAI_REASONING = {
   defaultEffort: 'medium',
 }
 
-/** Catalog served by `llm.models` (fresh copies per call). */
+/** Catalog served by `session/modelCatalog` (fresh copies per call). */
 function fixtureModelGroups(): ModelProviderGroup[] {
   return [
     {
@@ -1834,6 +1834,25 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
         },
       }
     },
+    openSettingsDocument(): RpcResult<{ opened: true }> {
+      return { ok: true, value: { opened: true } }
+    },
+    openAgentPresetDirectory(agentPreset: string): RpcResult<
+      { opened: true } | { opened: false; path: string }
+    > {
+      const existing = fixturePresets.get(agentPreset)
+      if (existing === undefined || existing.trust === 'system') {
+        return {
+          ok: false,
+          error: {
+            code: 'agent-preset-read-only',
+            message: `agent preset "${agentPreset}" ships with the deployment`,
+            details: { agentPreset, reason: 'it ships with the deployment' },
+          },
+        }
+      }
+      return { ok: true, value: { opened: true } }
+    },
   }
 
   const credentialRemotes = {
@@ -1999,10 +2018,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
   function ok<P, T>(request: RpcRequest<P>, value: T): Promise<RpcResponse<T>> {
     return Promise.resolve({ rpcId: request.rpcId, result: { ok: true, value } })
   }
-  function err<P, T>(request: RpcRequest<P>, error: Extract<RpcResult<T>, { ok: false }>['error']): Promise<RpcResponse<T>> {
-    return Promise.resolve({ rpcId: request.rpcId, result: { ok: false, error } })
-  }
-
   function sessionOk<T>(value: T): Promise<ConnectionRpcResult<T>> {
     return Promise.resolve({ ok: true, value })
   }
@@ -2012,16 +2027,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
   }
 
   const summaryOf = (id: SessionId): FixtureSessionSummary | undefined => sessions.find(s => s.sessionId === id)
-  /** Shared session guard for sessionId-addressed catalog routes: the error
-   *  response when the session is unknown, undefined when it exists. */
-  const requireSession = (request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<never>> | undefined => {
-    if (summaryOf(request.payload.sessionId) !== undefined) return undefined
-    return err<{ sessionId: SessionId }, never>(request, {
-      code: 'session-not-found',
-      message: `no session ${request.payload.sessionId}`,
-      details: { sessionId: request.payload.sessionId },
-    })
-  }
   const requireRemoteSession = (
     request: { readonly sessionId: SessionId },
   ): Promise<ConnectionRpcResult<never>> | undefined => {
@@ -3398,65 +3403,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
       describe: request => ok(request, {
         version: '0.0.0-fixture', cwd: '/tmp/fixture', attachedSessions, home: FIXTURE_HOME, canOpenPath: true,
       }),
-      openPath: request => ok(request, { opened: true as const }),
-    },
-    agentPresets: {
-      // Native opens are deterministic no-op successes in this fixture, so the
-      // open-directory affordance renders and the path-text fallback stays a
-      // component-test concern.
-      openDocument: (request) => {
-        const { agentPreset } = request.payload
-        const existing = fixturePresets.get(agentPreset)
-        if (existing === undefined || existing.trust === 'system') {
-          return err(request, {
-            code: 'agent-preset-read-only',
-            message: `agent preset "${agentPreset}" ships with the deployment`,
-            details: { agentPreset, reason: 'it ships with the deployment' },
-          })
-        }
-        return ok(request, { opened: true as const })
-      },
-    },
-
-    skills: {
-      list: (request) => {
-        const missing = requireSession(request)
-        if (missing !== undefined) return missing
-        return ok(request, {
-          skills: [
-            { name: 'fixture-demo', description: 'fixture 技能样本', whenToUse: '仅供 UI 目录渲染验收', modelInvocable: true },
-            { name: 'fixture-user-only', description: 'fixture 仅用户技能样本', modelInvocable: false },
-          ],
-        })
-      },
-    },
-    settings: {
-      // Native opens are deterministic no-op successes in this fixture, as is host.openPath.
-      openDocument: request => ok(request, { opened: true as const }),
-    },
-    llm: {
-      providers: request => ok(request, {
-        providers: [
-          { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true },
-          { provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: true, declared: false },
-          { provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], active: false, declared: false },
-          // One hand-declared route, so a surface reading this fixture meets
-          // the tagged shape rather than only the shipped one.
-          { provider: 'acme-gateway', displayName: 'Acme Gateway', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'acme-gateway'], active: true, declared: true },
-        ],
-      }),
-      models: request => ok(request, {
-        default: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
-        routableProviders: ['deepseek-official', 'openai', 'acme-gateway'],
-        groups: fixtureModelGroups(),
-        failures: [],
-      }),
-      // The fixture endpoint is imaginary, so the interrogation answers the
-      // catalog it already serves — enough for a surface to exercise adopting
-      // candidates without a reachable provider.
-      discoverModels: request => ok(request, {
-        models: fixtureModelGroups().flatMap(group => group.models.map(model => ({ id: model.id, name: model.name }))),
-      }),
     },
     // Satisfies the ApiProxy contract type only: the browser export button
     // hands GET /api/session.export to the native download manager, so this
@@ -3484,6 +3430,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
           refs?: readonly string[]
           value?: string
           ns?: string
+          settingsNs?: string
           agentPreset?: string
           from?: string
           id?: string
@@ -3538,6 +3485,58 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
         case 'credentials/set': return Promise.resolve(credentialRemotes.set(args.ref as string))
         case 'credentials/unset': return Promise.resolve(credentialRemotes.unset(args.ref as string))
         case 'settings/describe': return Promise.resolve(settingsRemotes.describe())
+        case 'settings/openSettingsDocument': return Promise.resolve(settingsRemotes.openSettingsDocument())
+        case 'settings/openAgentPresetDirectory': return Promise.resolve(
+          settingsRemotes.openAgentPresetDirectory(args.agentPreset as string),
+        )
+        case 'skills/list': {
+          const skillRequest = request as { readonly sessionId: SessionId }
+          const missing = requireRemoteSession(skillRequest)
+          if (missing !== undefined) return missing
+          return sessionOk({
+            skills: [
+              { name: 'fixture-demo', description: 'fixture 技能样本', whenToUse: '仅供 UI 目录渲染验收', modelInvocable: true },
+              { name: 'fixture-user-only', description: 'fixture 仅用户技能样本', modelInvocable: false },
+            ],
+          })
+        }
+        case 'session/openWorkspacePath': {
+          const pathRequest = request as { readonly sessionId: SessionId; readonly path: string }
+          const missing = requireRemoteSession(pathRequest)
+          return missing ?? sessionOk({ opened: true as const })
+        }
+        case 'session/modelCatalog': return Promise.resolve({
+          ok: true,
+          value: {
+            default: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
+            routableProviders: ['deepseek-official', 'openai', 'acme-gateway'],
+            groups: fixtureModelGroups(),
+            failures: [],
+          },
+        })
+        case 'llm/listProviders': return Promise.resolve({
+          ok: true,
+          value: [
+            { id: 'deepseek-official', name: 'DeepSeek' },
+            { id: 'openai', name: 'openai' },
+            { id: 'acme-gateway', name: 'Acme Gateway' },
+          ],
+        })
+        case 'llm/listConfigurableProviders': return Promise.resolve({
+          ok: true,
+          value: [
+            { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] },
+            { provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], declared: false },
+            { provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], declared: false },
+            { provider: 'acme-gateway', displayName: 'Acme Gateway', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'acme-gateway'], declared: true },
+          ],
+        })
+        // The fixture endpoint is imaginary, so interrogation answers the
+        // catalog it already serves without a network request.
+        case 'llm/discoverModels': return Promise.resolve({
+          ok: true,
+          value: fixtureModelGroups().flatMap(group => group.models.map(model => ({ id: model.id, name: model.name }))),
+        })
         case 'settings/update': return Promise.resolve(settingsRemotes.update(args.ns as string))
         case 'settings/replace': return Promise.resolve(settingsRemotes.replace(args.ns as string))
         case 'settings/mutate': return Promise.resolve(settingsRemotes.mutate(args.ns as string))
@@ -3643,13 +3642,13 @@ export class FixtureApiClient extends AbstractApiClient {
     payload: RequestPayload<K>,
     signal?: AbortSignal,
   ): Promise<RpcResponse<ResponseValue<K>>> {
+    void signal
     const request = rpcRequest(payload)
     const full: ClientRequest = { type: 'client-request', rpcId: request.rpcId, method, payload }
     this.onEnvelope(full)
     const response = await this.dispatch(
       method,
       request as RpcRequest<never>,
-      signal ?? new AbortController().signal,
     ) as RpcResponse<ResponseValue<K>>
     const fullResponse: ServerResponse = { type: 'server-response', rpcId: response.rpcId, result: response.result }
     this.onEnvelope(fullResponse)
@@ -3660,17 +3659,9 @@ export class FixtureApiClient extends AbstractApiClient {
   private dispatch(
     method: keyof RpcMethodMap,
     request: RpcRequest<never>,
-    signal: AbortSignal,
   ): Promise<RpcResponse<unknown>> {
     switch (method) {
       case 'host.describe': return this.api.host.describe(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)
-      case 'settings.openDocument': return this.api.settings.openDocument(request, signal)
-      case 'llm.providers': return this.api.llm.providers(request)
-      case 'llm.models': return this.api.llm.models(request)
-      case 'llm.discoverModels': return this.api.llm.discoverModels(request, signal)
     }
   }
 

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

@@ -31,14 +31,11 @@ declare module '@deepseek-ai/cordis' {
 // ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
 export type {
   ApiProxy, HostApi,
-  SkillsApi, SkillEntry,
   ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
   MessageId, ModelReasoningEffort, ModelSelection,
   RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
   ClientRequest, ServerResponse, RpcMessage,
   HostDescription, IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
-  SettingsApi,
-  ConfigurableProviderView, DiscoveredModelView, LlmApi,
 } from './api.ts'
 export {
   RpcId,

+ 1 - 1
packages/client/ui-agent-preset/src/client/index.ts

@@ -65,7 +65,7 @@ export function apply(ctx: ClientContext): void {
   // One roster, four surfaces. The chip is registered in a later scope, so it
   // subscribes here rather than being reached from this one.
   const rosterReaders = new Set<() => void>()
-  const section = new AgentPresetSectionController({ ...api, ...settingsWire }, ctx.remote, () => {
+  const section = new AgentPresetSectionController(api, ctx.remote, () => {
     void controller.load()
     for (const read of rosterReaders) read()
   })

+ 8 - 9
packages/client/ui-agent-preset/src/client/section-store.ts

@@ -15,7 +15,6 @@
  */
 
 import type { ClientRemote, IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
-import type { SettingsWireFace } from '@deepseek-ai/dsh-client-ui-settings/client'
 import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
 import { beginRosterRead, messageOf, writeDefaultPreset } from './settings-store.ts'
 
@@ -134,8 +133,8 @@ export class AgentPresetSectionController {
   readonly store: SnapshotStore<AgentPresetSectionState> = createSnapshotStore(INITIAL)
 
   constructor(
-    private readonly api: SettingsWireFace & Pick<IApiClient, 'agentPresets' | 'host'>,
-    private readonly remote: Pick<ClientRemote, 'agentPresets'>,
+    private readonly api: Pick<IApiClient, 'host'>,
+    private readonly remote: Pick<ClientRemote, 'agentPresets' | 'settings'>,
     /**
      * Called after this page changes the roster DIRECTORY, so the other
      * surfaces reading the same roster re-read it. A settings field moving is
@@ -296,13 +295,13 @@ export class AgentPresetSectionController {
    */
   async openLocation(id: string): Promise<void> {
     try {
-      const response = await this.api.agentPresets.openDocument({ agentPreset: id })
-      if (!response.result.ok) {
-        this.set({ error: response.result.error.message })
+      const result = await this.remote.settings.openAgentPresetDirectory(id)
+      if (!result.ok) {
+        this.set({ error: result.error.message })
         return
       }
-      if (response.result.value.opened) return
-      const { path } = response.result.value
+      if (result.value.opened) return
+      const { path } = result.value
       this.set({ revealedPaths: { ...this.store.getSnapshot().revealedPaths, [id]: path } })
     } catch (error) {
       this.set({ error: messageOf(error) })
@@ -350,7 +349,7 @@ export class AgentPresetSectionController {
    * @returns once the write settled and the roster was re-read.
    */
   async makeDefault(id: string): Promise<void> {
-    const failure = await writeDefaultPreset(this.api, id)
+    const failure = await writeDefaultPreset(this.remote, id)
     if (failure !== undefined) {
       this.set({ error: failure })
       return

+ 1 - 3
packages/client/ui-chat/package.json

@@ -74,8 +74,7 @@
     "@deepseek-ai/dsh-session-stats": "workspace:^",
     "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/dsh-token-meter": "workspace:^",
-    "@deepseek-ai/dsh-tools": "workspace:^",
-    "@deepseek-ai/dsh-util-workspace-path": "workspace:^"
+    "@deepseek-ai/dsh-tools": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -105,7 +104,6 @@
     "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/dsh-token-meter": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
-    "@deepseek-ai/dsh-util-workspace-path": "workspace:^",
     "@types/react": "~18.3.1",
     "react": "^18.2.0"
   },

+ 6 - 5
packages/client/ui-chat/src/client/apply.ts

@@ -1,10 +1,10 @@
 /** Register the Chat Conversation target, renderers, stats, and details surface. */
 import type { Context } from '@deepseek-ai/cordis'
 import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
+import type {} from '@deepseek-ai/dsh-api-remotes/client'
 import type { SessionBinding } from '@deepseek-ai/dsh-api-session-controller/client'
 import type { BoundActions, ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
-import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path'
 // Type-only service and declaration merges used by the apply world.
 import type {} from '@deepseek-ai/dsh-client-locale/client'
 import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
@@ -46,7 +46,8 @@ const CHAT_NODE_INJECT: ChatNodeTurnDataInjected = {
 
 /** Services required by the Chat target and its presentation registrations. */
 export const inject = [
-  'slots', 'sessions', 'uiSession', 'uiConversation', 'uiWorkspace', 'layout', 'locale', 'settingsScope',
+  'slots', 'sessions', 'uiSession', 'uiConversation', 'layout', 'locale',
+  'settingsScope', 'remote', 'remote.session',
 ]
 
 /**
@@ -115,9 +116,9 @@ export function apply(ctx: Context): void {
             ctx.layout.openDetails()
           },
           fileMentions: (owner: TurnTailOwnerProps) => ctx.get('chatFileMentions')?.forClosing(owner),
-          openFile: (path) => {
-            const cwd = ctx.sessions.list.getSnapshot().byId[sessionId]?.cwd
-            return ctx.uiWorkspace.openPath(resolveWorkspacePath(cwd, path))
+          openFile: async (path) => {
+            const result = await ctx.remote.session.openWorkspacePath({ sessionId, path })
+            if (!result.ok) throw new Error(`path open failed: ${result.error.message}`)
           },
           loadOlder: () => { void session.loadOlder() },
           loadImage: Object.assign(

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

@@ -50,9 +50,6 @@
     {
       "path": "../../runtime-diagnostics/invariants"
     },
-    {
-      "path": "../../util/workspace-path"
-    },
     {
       "path": "../../session/session-stats"
     },

+ 0 - 3
packages/client/ui-model-selection/package.json

@@ -33,7 +33,6 @@
     "client": {
       "inject": [
         "@deepseek-ai/dsh-api-session-controller",
-        "@deepseek-ai/dsh-client-connection",
         "@deepseek-ai/dsh-client-locale",
         "@deepseek-ai/dsh-client-ui-commands",
         "@deepseek-ai/dsh-api-remotes"
@@ -49,7 +48,6 @@
   "peerDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
     "@deepseek-ai/dsh-api-session-controller": "workspace:^",
-    "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-ui-commands": "workspace:^",
     "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
@@ -64,7 +62,6 @@
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
     "@deepseek-ai/dsh-api-session-controller": "workspace:^",
-    "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-ui-commands": "workspace:^",

+ 8 - 11
packages/client/ui-model-selection/src/client/catalog.ts

@@ -1,9 +1,6 @@
 /** One Host-generation model catalog shared by every Session selector. */
 
-import {
-  type IApiClient,
-  type ModelCatalog,
-} from '@deepseek-ai/dsh-client-connection/client'
+import type { ClientRemote, ModelCatalog } from '@deepseek-ai/dsh-api-remotes/client'
 import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
 
 /** Observable lifecycle of the shared model catalog. */
@@ -25,8 +22,8 @@ export class ModelCatalogDirectory {
   private generation = 0
   private inflight: Promise<ModelCatalog> | undefined
 
-  /** @param api - shared connection API client. */
-  constructor(private readonly api: IApiClient) {}
+  /** @param session - Session Remote namespace carrying the Host-generation catalog. */
+  constructor(private readonly session: Pick<ClientRemote['session'], 'modelCatalog'>) {}
 
   /**
    * Return the current generation's catalog, sharing its one in-flight load.
@@ -41,14 +38,14 @@ export class ModelCatalogDirectory {
       draft.status = 'loading'
       draft.error = null
     })
-    const operation = this.api.llm.models({}).then((response) => {
-      if (!response.result.ok) {
-        throw new Error(`${response.result.error.code}: ${response.result.error.message}`)
+    const operation = this.session.modelCatalog().then((response) => {
+      if (!response.ok) {
+        throw new Error(`${response.error.code}: ${response.error.message}`)
       }
       if (generation === this.generation) {
-        this.store.set({ value: response.result.value, status: 'ready', error: null })
+        this.store.set({ value: response.value, status: 'ready', error: null })
       }
-      return response.result.value
+      return response.value
     }).catch((error: unknown) => {
       if (generation === this.generation) {
         this.store.update((draft) => {

+ 1 - 1
packages/client/ui-model-selection/src/client/index.ts

@@ -2,7 +2,7 @@
  * Model selection plugin, browser half — TWO entries over ONE per-session
  * directory owned by ModelDirectoryResolver (`ctx.modelDirectories`). The /model popupSelect
  * contribution and the composer's named `conversation.input.model` seat share
- * one Host-generation `llm.models` catalog, combine it with the Session's
+ * one Host-generation `session/modelCatalog` catalog, combine it with the Session's
  * durable model-selection projection, and submit through `session.selectModel`.
  * A switch made in either entry is what the other shows next. Failures
  * ride each entry's own retry surface (popup shell error/retry; seat menu

+ 2 - 5
packages/client/ui-model-selection/src/client/service.ts

@@ -15,7 +15,6 @@
 import { Service } from '@deepseek-ai/cordis'
 import type { Context } from '@deepseek-ai/cordis'
 import type {} from '@deepseek-ai/dsh-api-session-controller/client'
-import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import { ModelCatalogDirectory } from './catalog.ts'
 import { ModelDirectory } from './directory.ts'
@@ -34,7 +33,7 @@ interface LiveState {
 
 /** The `ctx.modelDirectories` session model-selection service. */
 export class ModelDirectoryResolver extends Service {
-  static inject = ['sessions', 'remote', 'remote.session', 'connection']
+  static inject = ['sessions', 'remote', 'remote.session']
 
   private readonly live: LiveState = { directories: new Map() }
   private readonly catalog: ModelCatalogDirectory
@@ -49,9 +48,7 @@ export class ModelDirectoryResolver extends Service {
   constructor(ctx: Context, config: { blockReason: () => string }) {
     super(ctx, 'modelDirectories')
     this.blockReason = config.blockReason
-    const connection = ctx.get('connection') as ConnectionHandle | undefined
-    if (connection === undefined) throw new Error('ui-model-selection: connection service is unavailable')
-    this.catalog = new ModelCatalogDirectory(connection.api)
+    this.catalog = new ModelCatalogDirectory(ctx.remote.session)
     void this.catalog.load().catch(() => { /* selectors expose the shared error */ })
     ctx.on('connection/reset', () => {
       this.catalog.resetGeneration()

+ 2 - 2
packages/client/ui-settings-general/src/client/index.ts

@@ -55,7 +55,7 @@ const NS = 'settings'
  * ui-settings' apply, whose activation order relative to this one is NOT
  * constrained; registrations depend on their slots through `slots.inject()`.
  */
-export const inject = ['slots', 'locale', 'connection', 'settingsScope']
+export const inject = ['slots', 'locale', 'connection', 'remote', 'remote.settings', 'settingsScope']
 
 /**
  * Register the `settings` dictionaries, the chrome content, and the General
@@ -72,7 +72,7 @@ export function apply(ctx: ClientContext): void {
   const connection = ctx.get('connection') as ConnectionHandle
   // The shared SettingsScope mirror updates after document commits and reconnects.
   const documentController = connection.isLoopback
-    ? new SettingsDocumentStore(connection.api, ctx.settingsScope.describe())
+    ? new SettingsDocumentStore(ctx.remote, ctx.settingsScope.describe())
     : undefined
   const documentInjected = documentController === undefined
     ? undefined

+ 4 - 4
packages/client/ui-settings-general/src/client/settings-document-store.ts

@@ -1,6 +1,6 @@
 /** State owner for the optional local settings-document action. */
 
-import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
+import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'
 import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
 import type { SettingsDescribeFace } from '@deepseek-ai/dsh-client-ui-settings/client'
 
@@ -32,7 +32,7 @@ export class SettingsDocumentStore {
    * @param describeFace - the shared mirror's describe face (`hasDocument` source).
    */
   constructor(
-    private readonly api: Pick<IApiClient, 'settings'>,
+    private readonly remote: Pick<ClientRemote, 'settings'>,
     private readonly describeFace: SettingsDescribeFace,
   ) {}
 
@@ -63,8 +63,8 @@ export class SettingsDocumentStore {
       state.error = null
     })
     try {
-      const response = await this.api.settings.openDocument({})
-      if (!response.result.ok) throw new Error(response.result.error.message)
+      const result = await this.remote.settings.openSettingsDocument()
+      if (!result.ok) throw new Error(result.error.message)
     } catch (error) {
       this.store.update((state) => { state.error = messageOf(error) })
     } finally {

+ 0 - 2
packages/client/ui-settings-models/package.json

@@ -47,7 +47,6 @@
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
-    "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
@@ -55,7 +54,6 @@
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
-    "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-store": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",

+ 9 - 10
packages/client/ui-settings-models/src/client/ModelListEditor.tsx

@@ -16,11 +16,11 @@
 
 import { useState } from 'react'
 import type { ReactNode } from 'react'
-import type { DiscoveredModelView, IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
+import type { LlmDiscoveredModel } from '@deepseek-ai/dsh-api-remotes/client'
 import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
 import { formatCapacity, parseCapacity } from './DeepSeekModelsEditor.tsx'
 import type { DeepSeekModelDraft } from './DeepSeekModelsEditor.tsx'
-import { messageOf } from './store.ts'
+import { messageOf, type ModelsWire } from './store.ts'
 import type { en } from './locales.ts'
 import styles from './ModelsSection.module.css'
 
@@ -80,7 +80,7 @@ export interface ModelListEditorProps {
    */
   probeBlocked?: keyof typeof en | undefined
   /** Wire face the fetch action calls. */
-  api: Pick<IApiClient, 'llm'>
+  api: Pick<ModelsWire, 'llm'>
   /** Section copy. */
   t: (key: keyof typeof en) => string
   /** Disable every control (read-only deployment or a pending write). */
@@ -142,7 +142,7 @@ function capacitySpelling(value: number | undefined): string {
 }
 
 /** Adopt a candidate, keeping whatever capacities the provider disclosed. */
-function adopt(candidate: DiscoveredModelView): ModelDraft {
+function adopt(candidate: LlmDiscoveredModel): ModelDraft {
   return {
     id: candidate.id,
     ...candidate.name === undefined ? {} : { name: candidate.name },
@@ -160,7 +160,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
   const { models, onChange, probe, api, t, disabled } = props
   const [busy, setBusy] = useState(false)
   const [failure, setFailure] = useState<string | undefined>(undefined)
-  const [candidates, setCandidates] = useState<readonly DiscoveredModelView[] | undefined>(undefined)
+  const [candidates, setCandidates] = useState<readonly LlmDiscoveredModel[] | undefined>(undefined)
   const [picked, setPicked] = useState<ReadonlySet<string>>(new Set())
   // Rows carry an id and a name; capacities are the exception, so they stay
   // folded until asked for rather than crowding every row with four inputs.
@@ -229,18 +229,17 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
     setBusy(true)
     setFailure(undefined)
     try {
-      const response = await api.llm.discoverModels({
-        settingsNs: probe.settingsNs,
+      const response = await api.llm.discoverModels(probe.settingsNs, {
         ...probe.provider === undefined ? {} : { provider: probe.provider },
         ...probe.baseURL === undefined || probe.baseURL.length === 0 ? {} : { baseURL: probe.baseURL },
         ...probe.api === undefined ? {} : { api: probe.api },
         ...probe.apiKey === undefined ? {} : { apiKey: probe.apiKey },
       })
-      if (!response.result.ok) {
-        setFailure(response.result.error.message)
+      if (!response.ok) {
+        setFailure(response.error.message)
         return
       }
-      const found = response.result.value.models
+      const found = response.value
       if (found.length === 0) {
         setFailure(t('fetchEmpty'))
         return

+ 6 - 8
packages/client/ui-settings-models/src/client/index.ts

@@ -7,7 +7,6 @@
  * packages/client/AGENTS.md.
  */
 import type { Context as ClientContext } from '@deepseek-ai/cordis'
-import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
 // Type-only: pulls the shell's SlotMap merge (the 'settings.section' entry).
 import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
 // Type-only: pulls the locale plugin's Context merge (ctx.locale).
@@ -42,7 +41,9 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
 
 /** Dictionary namespace owned by this plugin. */
 const NS = 'settings.models'
-export type { ModelsCredentials, ModelsSettingsState, ModelsWire, ProviderRow } from './store.ts'
+export type {
+  ModelsCredentials, ModelsLlm, ModelsSettingsState, ModelsWire, ProviderDirectoryEntry, ProviderRow,
+} from './store.ts'
 
 /**
  * Refetch the page snapshot only after its first load: an unopened Models
@@ -60,7 +61,7 @@ export function refreshIfLoaded(controller: ModelsSettingsStore): void {
  * constrained; registration depends on each slot through `slots.inject()`.
  */
 export const inject = [
-  'slots', 'locale', 'connection', 'remote', 'remote.credentials', 'remote.settings',
+  'slots', 'locale', 'remote', 'remote.credentials', 'remote.llm', 'remote.settings',
   'settingsScope', 'settingsSchema',
 ]
 
@@ -73,14 +74,11 @@ export const inject = [
 export function apply(ctx: ClientContext): void {
   ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-settings-models: copy dictionaries')
 
-  const connection = ctx.get('connection') as ConnectionHandle
   const schema = createSettingsSchemaOperations(ctx.settingsSchema)
-  // The page's two carriers under one face: model discovery and the catalog
-  // still ride the unary API, while settings and credentials are Remote
-  // namespaces.
+  // Every configuration operation rides its owning Remote namespace.
   const wire: ModelsWire = {
-    ...connection.api,
     credentials: ctx.remote.credentials,
+    llm: ctx.remote.llm,
     settings: ctx.remote.settings,
   }
   const controller = new ModelsSettingsStore(wire, schema, ctx.settingsScope.describe())

+ 3 - 3
packages/client/ui-settings-models/src/client/slot-contract.ts

@@ -4,7 +4,7 @@
  * without editing it.
  *
  * `settings.models.provider-card` is keyed by the row's owning settings
- * namespace (`ConfigurableProviderView.settingsNs`): an adapter family's
+ * namespace (`ProviderDirectoryEntry.settingsNs`): an adapter family's
  * companion plugin registers one entry under the family's namespace and
  * receives every card of that family — shipped, added, and hand-declared rows
  * alike — while the section never learns what the namespace means. Keying on
@@ -17,8 +17,8 @@
  * the declaration. The types therefore live with their declarer.
  */
 
-import type { ConfigurableProviderView } from '@deepseek-ai/dsh-api-remotes/client'
 import type {} from '@deepseek-ai/dsh-client-ui-slots'
+import type { ProviderDirectoryEntry } from './store.ts'
 
 declare module '@deepseek-ai/dsh-client-ui-slots' {
   interface SlotMap {
@@ -42,7 +42,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
 /** Owner share of one provider-card extension occurrence. */
 export interface ProviderCardExtrasOwnerProps {
   /** The card's directory row (route id, display name, settings address, live state). */
-  provider: ConfigurableProviderView
+  provider: ProviderDirectoryEntry
   /** Whether any layer configures this provider (its profile resolves); `false` while the add-provider draft edits a dormant row. */
   configured: boolean
   /** Whether the row's referenced api-key credential is confirmed configured (the page's credential join). */

+ 64 - 12
packages/client/ui-settings-models/src/client/store.ts

@@ -1,13 +1,14 @@
 /**
  * Models settings page store: one snapshot joining the configurable-provider
- * directory (`llm.providers`), the settings namespaces (shared settings mirror),
+ * directory (`llm/listProviders` joined with `llm/listConfigurableProviders`),
+ * the settings namespaces (shared settings mirror),
  * and the referenced credentials (`credentials/describe`). The host stays the
  * single fact source — every mutation writes through the wire and the page
  * re-renders from the next describe, pushed or refetched.
  */
 
 import type {
-  ClientRemote, ConfigurableProviderView, CredentialInfo, IApiClient, SettingsNamespaceView,
+  ClientRemote, CredentialInfo, LlmConfigurableProvider, LlmProviderInfo, SettingsNamespaceView,
 } from '@deepseek-ai/dsh-api-remotes/client'
 import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
 import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
@@ -23,22 +24,71 @@ const PROBE_ROUTE = '\u0000probe'
 /** The credentials Remote methods the Models page reads and writes through. */
 export type ModelsCredentials = Pick<ClientRemote['credentials'], 'describe' | 'set' | 'unset'>
 
+/** LLM Remote methods used by the Models page. */
+export type ModelsLlm = Pick<
+  ClientRemote['llm'],
+  'discoverModels' | 'listConfigurableProviders' | 'listProviders'
+>
+
+/** One provider row after joining the configurable directory with live routes. */
+export interface ProviderDirectoryEntry {
+  readonly provider: string
+  readonly displayName: string
+  readonly settingsNs: string
+  readonly settingsPath: readonly string[]
+  readonly active: boolean
+  readonly declared?: boolean
+}
+
+/**
+ * Join declared configurable providers with the currently registered routes.
+ * @param registered - live provider routes in registration order.
+ * @param directory - declared configurable providers in declaration order.
+ * @returns declared rows followed by live routes with no declaration.
+ */
+export function joinProviderDirectory(
+  registered: readonly LlmProviderInfo[],
+  directory: readonly LlmConfigurableProvider[],
+): ProviderDirectoryEntry[] {
+  const active = new Set(registered.map(provider => provider.id))
+  const declared = new Set(directory.map(entry => entry.provider))
+  const rows: ProviderDirectoryEntry[] = directory.map(entry => ({
+    provider: entry.provider,
+    displayName: entry.displayName,
+    settingsNs: entry.settingsNs,
+    settingsPath: [...entry.settingsPath],
+    active: active.has(entry.provider),
+    ...entry.declared === undefined ? {} : { declared: entry.declared },
+  }))
+  for (const provider of registered) {
+    if (declared.has(provider.id)) continue
+    rows.push({
+      provider: provider.id,
+      displayName: provider.name,
+      settingsNs: '',
+      settingsPath: [],
+      active: true,
+    })
+  }
+  return rows
+}
+
 /**
- * Every wire face the Models page reaches: the settings and llm unary domains,
- * plus the credentials Remote namespace, which is addressed by reference name
- * and never answers with a value.
+ * Every Remote wire face the Models page reaches.
  */
-export interface ModelsWire extends Pick<IApiClient, 'llm'> {
+export interface ModelsWire {
   /** The settings Remote namespace: the redacted read and the profile writes. */
   settings: SettingsRemote
   /** Credential state and writes for the references provider profiles name. */
   credentials: ModelsCredentials
+  /** Provider directory reads and draft endpoint discovery. */
+  llm: ModelsLlm
 }
 
 /** One provider row the page renders. */
 export interface ProviderRow {
   /** The directory entry (route id, display name, settings address, live state). */
-  entry: ConfigurableProviderView
+  entry: ProviderDirectoryEntry
   /** Whether any layer configures this provider (its profile resolves). */
   configured: boolean
   /** Whether the user layer alone carries the profile (removal restores the base). */
@@ -157,20 +207,22 @@ export class ModelsSettingsStore {
   async load(): Promise<void> {
     const generation = ++this.generation
     this.store.update((s) => { s.status = 'loading'; s.error = null })
-    let providers: ConfigurableProviderView[]
+    let providers: ProviderDirectoryEntry[]
     let writable: boolean
     let views: readonly SettingsNamespaceView[]
     try {
-      const [providersResponse] = await Promise.all([
-        this.api.llm.providers({}),
+      const [registered, declared] = await Promise.all([
+        this.api.llm.listProviders(),
+        this.api.llm.listConfigurableProviders(),
         this.describeFace.ensure(),
       ])
-      if (!providersResponse.result.ok) throw new Error(providersResponse.result.error.message)
+      if (!registered.ok) throw new Error(registered.error.message)
+      if (!declared.ok) throw new Error(declared.error.message)
       const mirrored = this.describeFace.getSnapshot()
       if (mirrored.view === undefined) {
         throw new Error(mirrored.error ?? 'settings are unavailable in this browser')
       }
-      providers = providersResponse.result.value.providers
+      providers = joinProviderDirectory(registered.value, declared.value)
       writable = mirrored.view.writable
       views = mirrored.view.namespaces
     } catch (error) {

+ 4 - 4
packages/client/ui-settings-plugins/src/client/index.ts

@@ -9,7 +9,6 @@
  * settings scope, which keeps them unaware of one another and of other tabs.
  */
 
-import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
 // Type-only: pulls the locale plugin's Context merge (ctx.locale).
 import type {} from '@deepseek-ai/dsh-client-locale/client'
 // Type-only: the settings shell's SlotMap merge (the 'settings.section' entry)
@@ -54,14 +53,15 @@ export type { WebSearchCardFace, WebSearchCardState } from './web-search-card-co
 const NS = 'settings.plugins'
 
 /** Required services (cordis fiber inject). */
-export const inject = ['slots', 'locale', 'connection', 'remote', 'remote.credentials', 'settingsScope']
+export const inject = [
+  'slots', 'locale', 'connection', 'remote', 'remote.credentials', 'remote.session', 'settingsScope',
+]
 
 /**
  * Mount the plugin configuration section and the cards this package ships.
  * @param ctx - the browser plugin context.
  */
 export function apply(ctx: ClientContext): void {
-  const { api } = ctx.get('connection') as ConnectionHandle
   const t = ctx.locale.bind(NS)
   ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-settings-plugins: section dictionaries')
 
@@ -71,7 +71,7 @@ export function apply(ctx: ClientContext): void {
     ctx.settingsScope.bind({ namespace: WEB_SEARCH_NS }), ctx.remote.credentials)
   const subagentModelSelection = new SubagentModelSelectionCardController(
     ctx.settingsScope.bind({ namespace: SUBAGENT_MODEL_SELECTION_NS }),
-    api,
+    ctx.remote.session,
   )
 
   // The credential a card reports is not part of any settings section, so its

+ 7 - 7
packages/client/ui-settings-plugins/src/client/subagent-model-selection-card-controller.ts

@@ -1,7 +1,7 @@
 /** Staged editor for the Host-owned subagent model allowlist. */
 
 import type {
-  IApiClient,
+  ClientRemote,
   ModelProviderGroup,
 } from '@deepseek-ai/dsh-api-remotes/client'
 import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
@@ -145,11 +145,11 @@ export class SubagentModelSelectionCardController {
 
   /**
    * @param scope - bound `subagent-model-selection` settings scope.
-   * @param api - Host LLM directory face.
+   * @param session - Host Session model-catalog face.
    */
   constructor(
     private readonly scope: SettingsScope<SubagentModelSelectionSettings>,
-    private readonly api: Pick<IApiClient, 'llm'>,
+    private readonly session: Pick<ClientRemote['session'], 'modelCatalog'>,
   ) {
     this.store = createSnapshotStore(this.projection())
     this.unsubscribe = scope.subscribe(() => {
@@ -321,11 +321,11 @@ export class SubagentModelSelectionCardController {
     this.catalogPartial = false
     this.publish()
     try {
-      const response = await this.api.llm.models({})
+      const response = await this.session.modelCatalog()
       if (generation !== this.catalogGeneration) return
-      if (!response.result.ok) throw new Error(response.result.error.message)
-      this.catalogGroups = response.result.value.groups
-      this.catalogPartial = response.result.value.failures.length > 0
+      if (!response.ok) throw new Error(response.error.message)
+      this.catalogGroups = response.value.groups
+      this.catalogPartial = response.value.failures.length > 0
       this.catalogStatus = 'ready'
     } catch {
       if (generation !== this.catalogGeneration) return

+ 6 - 6
packages/client/ui-skill/src/client/index.ts

@@ -1,6 +1,6 @@
 /**
  * Skill reference plugin, browser half: registers the '/' skill source —
- * candidates from the skill.list RPC addressed by the per-call session
+ * candidates from the `skills/list` Remote addressed by the per-call session
  * projection's sessionId (sessions are always agent-backed; the host
  * resolves cwd from the session header). A pick lands the literal `/name `
  * text and the prompt ships the same literal (plain-text-reference decision;
@@ -10,7 +10,7 @@
  * leading `/name` naming a user-invocable skill and injects the rendered
  * body for every entry point, including `disable-model-invocation` skills the
  * model-side catalog never lists (issue #1470). The RPC rides the plugin's
- * root-context connection captured at registration — the source never reads
+ * root-context Remote captured at registration — the source never reads
  * services off a per-call argument. Draft chip visuals derive from
  * the lexicon scan; this source implements no reference codec.
  *
@@ -31,7 +31,7 @@
  */
 // Type-only: the carrier types, the forwarded Host-event face and the ctx.remote merge.
 import type { Context as ClientContext } from '@deepseek-ai/cordis'
-import type { ConnectionHandle, SkillEntry } from '@deepseek-ai/dsh-api-remotes/client'
+import type { SkillEntry } from '@deepseek-ai/dsh-api-remotes/client'
 import type {} from '@deepseek-ai/dsh-api-session-controller/client'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import type { InputTriggerServiceContract, InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
@@ -71,7 +71,7 @@ export function apply(ctx: ClientContext): void {
     SkillRow,
   ))
 
-  const skills = (ctx.get('connection') as ConnectionHandle).api.skills
+  const skills = ctx.remote.skills
   const sessions = ctx.sessions
   // Session-keyed catalog cache; single-flight per key. Plugin-closure state:
   // the fiber effect below is its teardown boundary.
@@ -98,8 +98,8 @@ export function apply(ctx: ClientContext): void {
     if (existing !== undefined) return existing.promise
     const abort = new AbortController()
     const promise = (async () => {
-      const { result } = await skills.list({ sessionId }, abort.signal)
-      if (!result.ok) throw new Error(`skill.list failed: ${result.error.code}: ${result.error.message}`)
+      const result = await skills.list({ sessionId }, abort.signal)
+      if (!result.ok) throw new Error(`skills/list failed: ${result.error.code}: ${result.error.message}`)
       return result.value.skills
     })()
     const entry: CatalogFetch = { promise, abort }

+ 1 - 1
packages/client/ui-workspace/src/client/index.ts

@@ -75,7 +75,7 @@ export function apply(ctx: Context): void {
   const workspaces = ctx.get('workspaces') as IWorkspaces
   const hostDescription = connection.hostDescription
   const uiWorkspace = new UiWorkspaceService(
-    ctx, connection.api, ctx.remote.directoryPicker, workspaces, sessions)
+    ctx, ctx.remote.directoryPicker, workspaces, sessions)
   ctx.slots.provideRoot({ hooks: { workspaces: workspaces.list } })
   ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-workspace: dictionaries')
 

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

@@ -1,7 +1,6 @@
 /** Workspace archive and directory UI capability. */
 
 import { Service, type Context } from '@deepseek-ai/cordis'
-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 {
@@ -50,11 +49,6 @@ export interface UiWorkspace {
    * @returns created absolute path.
    */
   createDirectory(path: string, name: string): Promise<string>
-  /**
-   * Open a path with the Host operating system.
-   * @param path - absolute or Host-resolvable path.
-   */
-  openPath(path: string): Promise<void>
 }
 
 declare module '@deepseek-ai/cordis' {
@@ -80,14 +74,12 @@ 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,
@@ -163,13 +155,6 @@ class UiWorkspaceService extends Service implements UiWorkspace {
     return result.value
   }
 
-  async openPath(path: string): Promise<void> {
-    const response = await this.api.host.openPath({ path })
-    if (!response.result.ok) {
-      throw new Error(`path open failed: ${response.result.error.message}`)
-    }
-  }
-
   private watchNavigation(): () => void {
     let initial: 'waiting' | 'connecting' | 'done' = 'waiting'
     let disposed = false