From 2d4393d842139f16f4ae32b8ae31476a597cdd22 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:06:58 +0800 Subject: [PATCH 01/13] refactor(api): expose remaining domain remotes --- packages/api/session-controller/package.json | 6 + .../session-controller/src/file-references.ts | 42 ++++ packages/api/session-controller/src/index.ts | 95 +++++++- .../session-controller/src/skill-catalog.ts | 120 +++++++++++ packages/api/session-controller/src/types.ts | 34 +++ .../api/session-controller/tsconfig.host.json | 8 +- packages/api/settings-controller/package.json | 8 +- packages/api/settings-controller/src/index.ts | 152 ++++++++++++- packages/api/settings-controller/src/types.ts | 10 + .../api/settings-controller/tsconfig.json | 9 + packages/context/file-reference/package.json | 16 +- packages/context/file-reference/src/index.ts | 22 +- packages/llm/llm-pi-ai/src/discovery.ts | 4 +- packages/llm/llm-pi-ai/src/index.ts | 5 +- packages/llm/llm/package.json | 19 +- packages/llm/llm/src/index.ts | 50 ++++- packages/llm/llm/src/types.ts | 14 ++ packages/llm/llm/tsconfig.json | 3 + packages/util/native-command/src/index.ts | 52 ++--- .../util/native-command/src/path-opener.ts | 203 ++++++++++++++++++ packages/util/native-command/src/runner.ts | 41 ++++ 21 files changed, 821 insertions(+), 92 deletions(-) create mode 100644 packages/api/session-controller/src/file-references.ts create mode 100644 packages/api/session-controller/src/skill-catalog.ts create mode 100644 packages/util/native-command/src/path-opener.ts create mode 100644 packages/util/native-command/src/runner.ts diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json index dabfd8fd6a..191ad25909 100644 --- a/packages/api/session-controller/package.json +++ b/packages/api/session-controller/package.json @@ -85,15 +85,18 @@ "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", + "@deepseek-ai/dsh-file-reference": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-jobs": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-native-command": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-session-projection-cache": "workspace:^", "@deepseek-ai/dsh-session-query": "workspace:^", + "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-session-title": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-typert-protocol": "workspace:^", @@ -116,9 +119,11 @@ "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-store": "workspace:^", + "@deepseek-ai/dsh-file-reference": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-jobs": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-native-command": "workspace:^", "@deepseek-ai/dsh-permission-presets": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", @@ -126,6 +131,7 @@ "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-session-projection-cache": "workspace:^", "@deepseek-ai/dsh-session-query": "workspace:^", + "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-session-title": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-typert-protocol": "workspace:^", diff --git a/packages/api/session-controller/src/file-references.ts b/packages/api/session-controller/src/file-references.ts new file mode 100644 index 0000000000..44d3417928 --- /dev/null +++ b/packages/api/session-controller/src/file-references.ts @@ -0,0 +1,42 @@ +/** Session Controller adapter for Agent-scoped file-reference discovery. */ + +import type { Context } from '@deepseek-ai/cordis' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type {} from '@deepseek-ai/dsh-file-reference' +import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types' +import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' + +declare module '@deepseek-ai/cordis' { + interface Context { + /** Host owner of the `fileReferences` Remote namespace. */ + sessionFileReferences: SessionFileReferences + } +} + +/** Host Remote adapter over the composed file-reference provider. */ +export class SessionFileReferences extends TypertRemoteService { + static inject = ['fileReferences', 'typert'] + + /** @param ctx - Host context carrying the selected file-reference provider. */ + constructor(ctx: Context) { + super(ctx, 'sessionFileReferences', { namespace: 'fileReferences' }) + } + + /** + * List file and directory candidates for one Agent's working directory. + * @param agent - target Agent resolved from the Session identity on the wire. + * @param query - path text following `@` or `@"`. + * @param signal - caller cancellation. + * @returns deterministic path-only candidates from the composed provider. + */ + @Remote + list( + agent: Agent, + query: string, + signal: AbortSignal, + ): Promise { + return this.ctx.fileReferences.list(agent, query, signal) + } +} + +export default SessionFileReferences diff --git a/packages/api/session-controller/src/index.ts b/packages/api/session-controller/src/index.ts index a3ce201aaf..dadf1db607 100644 --- a/packages/api/session-controller/src/index.ts +++ b/packages/api/session-controller/src/index.ts @@ -3,10 +3,13 @@ import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { errorChain } from '@deepseek-ai/dsh-llm' +import { openNativePath } from '@deepseek-ai/dsh-native-command' import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session' import type { SessionObservation } from '@deepseek-ai/dsh-session-query' -import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' +import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' +import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path' import { + ApiSessionNotFound, ApiSessionAgentController, inspectApiSession, type ApiSessionAgentResult, @@ -14,9 +17,13 @@ import { import { SessionCommandController } from './commands.ts' import { SessionControlController } from './control.ts' import { SessionHistoryController } from './history.ts' +import { SessionFileReferences } from './file-references.ts' import { ApiSessionList, DEFAULT_COLD_BLANK_PROBE_MAX_BYTES } from './list.ts' +import { buildModelCatalog } from './catalog.ts' import { installModelSelectionProjection } from './model-selection-projection.ts' +import { SessionSkillCatalog } from './skill-catalog.ts' import type { + ModelCatalog, SessionAttachmentRequest, SessionAttachmentValue, SessionCancelRequest, @@ -30,6 +37,8 @@ import type { SessionForkValue, SessionListRequest, SessionListValue, + SessionOpenWorkspacePathRequest, + SessionOpenWorkspacePathValue, SessionPage, SessionPageRequest, SessionPromptRequest, @@ -46,6 +55,8 @@ import type { export type * from './types.ts' export { ApiSessionNotFound } from './agent.ts' +export { SessionFileReferences } from './file-references.ts' +export { SessionSkillCatalog } from './skill-catalog.ts' declare module '@deepseek-ai/cordis' { interface Context { @@ -60,6 +71,12 @@ export interface Config { readonly coldBlankProbeMaxBytes?: number } +/** Host integrations replaceable by direct unit tests. */ +export interface SessionControllerInternals { + /** Native default-application handoff. */ + readonly openPath?: (path: string, signal: AbortSignal) => Promise +} + /** Host service backing the generated `ctx.remote.session` namespace. */ export class SessionController extends TypertRemoteService { static inject = [ @@ -83,13 +100,14 @@ export class SessionController extends TypertRemoteService { private readonly controlState: SessionControlController private readonly history: SessionHistoryController private readonly listState: ApiSessionList + private readonly openPath: (path: string, signal: AbortSignal) => Promise private readonly promotions = new Set>() /** * @param ctx - Host context containing the Session capability assembly. * @param config - cold-list observation policy. */ - constructor(ctx: Context, config: Config) { + constructor(ctx: Context, config: Config, internals: SessionControllerInternals = {}) { super(ctx, 'sessionController', { namespace: 'session' }) installModelSelectionProjection(ctx) this.agents = new ApiSessionAgentController(ctx) @@ -105,6 +123,9 @@ export class SessionController extends TypertRemoteService { ctx, config.coldBlankProbeMaxBytes ?? DEFAULT_COLD_BLANK_PROBE_MAX_BYTES, ) + this.openPath = internals.openPath ?? openNativePath + ctx.plugin(SessionFileReferences) + ctx.plugin(SessionSkillCatalog) ctx.on('session/created', (session) => { ctx.emit('api-session/added', this.listState.summaryFor(session)) @@ -214,6 +235,74 @@ export class SessionController extends TypertRemoteService { return this.commands.selectModel(request) } + /** + * Describe every currently routable model for Host-generation selectors. + * @returns provider-grouped models, the deployment default, and isolated provider failures. + */ + @Remote('modelCatalog') + modelCatalog(): Promise { + return buildModelCatalog(this.ctx) + } + + /** + * Open a path resolved against one Session's workspace on the Host desktop. + * @param request - Session identity and absolute or workspace-relative path. + * @param signal - caller lifetime; abort terminates inspection or the native command. + * @returns confirmation after the native opener accepts the path. + * @throws TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails. + */ + @Remote('openWorkspacePath') + async openWorkspacePath( + request: SessionOpenWorkspacePathRequest, + signal: AbortSignal, + ): Promise { + if (request.path.length === 0) { + throw new TypertRemoteFailure({ + code: 'bad-request', + message: 'session.openWorkspacePath requires a non-empty path', + details: {}, + }) + } + signal.throwIfAborted() + let cwd: string | undefined + try { + cwd = (await this.inspect(request.sessionId, signal)).meta.cwd + } catch (error: unknown) { + if (signal.aborted) { + throw new TypertRemoteFailure({ + code: 'cancelled', message: 'path open was aborted', details: {}, + }) + } + if (error instanceof ApiSessionNotFound) { + throw new TypertRemoteFailure({ + code: 'session-not-found', + message: error.message, + details: { sessionId: request.sessionId }, + }) + } + throw new TypertRemoteFailure({ + code: 'internal', + message: `session "${request.sessionId}" could not be inspected: ${String(error)}`, + details: {}, + }) + } + try { + await this.openPath(resolveWorkspacePath(cwd, request.path), signal) + return { opened: true } + } catch (error: unknown) { + if (signal.aborted) { + throw new TypertRemoteFailure({ + code: 'cancelled', message: 'path open was aborted', details: {}, + }) + } + throw new TypertRemoteFailure({ + code: 'internal', + message: `path open failed: ${error instanceof Error ? error.message : String(error)}`, + details: {}, + }) + } + } + /** * Rename one Session after explicitly resuming it. * @param request - Session identity and proposed title. @@ -310,5 +399,5 @@ export class SessionController extends TypertRemoteService { } -export { buildModelCatalog } from './catalog.ts' +export { buildModelCatalog } export default SessionController diff --git a/packages/api/session-controller/src/skill-catalog.ts b/packages/api/session-controller/src/skill-catalog.ts new file mode 100644 index 0000000000..3cd15669ff --- /dev/null +++ b/packages/api/session-controller/src/skill-catalog.ts @@ -0,0 +1,120 @@ +/** Session-addressed, cold-readable skill catalog Remote. */ + +import type { Context } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-agent-presets/types' +import type { SessionId } from '@deepseek-ai/dsh-session' +import { SessionQueryError } from '@deepseek-ai/dsh-session-query' +import { isUserInvocable } from '@deepseek-ai/dsh-skill' +import type { ScopeKey } from '@deepseek-ai/dsh-scope' +import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' +import type { SkillListRequest, SkillListValue } from './types.ts' + +declare module '@deepseek-ai/cordis' { + interface Context { + /** Host owner of the Session-addressed `skills` Remote namespace. */ + sessionSkillCatalog: SessionSkillCatalog + } +} + +/** Host service backing `ctx.remote.skills` without activating a cold Agent. */ +export class SessionSkillCatalog extends TypertRemoteService { + static inject = ['agents', 'sessionQuery', 'typert'] + + /** @param ctx - Host context carrying Session reads and optional skill/preset services. */ + constructor(ctx: Context) { + super(ctx, 'sessionSkillCatalog', { namespace: 'skills' }) + } + + /** + * List the user-invocable skills visible to one Session composition. + * @param request - Session identity whose cwd and preset select the catalog view. + * @param signal - caller lifetime carried by the Remote transport; admitted catalog reads retain their existing completion semantics. + * @returns user-invocable skill metadata without loading skill bodies. + * @throws TypertRemoteFailure when the Session cannot be inspected or no registry can serve it. + */ + @Remote + async list(request: SkillListRequest, signal: AbortSignal): Promise { + void signal + const { sessionId } = request + let cwd: string | undefined + let agentPreset: string | undefined + try { + using observation = await this.ctx.sessionQuery.observeSession(sessionId) + if (observation.projections === undefined) { + throw new Error('skill catalog requires a projected Session observation') + } + cwd = observation.header.cwd + agentPreset = observation.projections.values.agentPreset ?? undefined + } catch (error: unknown) { + if (error instanceof SessionQueryError + && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') { + throw failure( + 'session-not-found', + `session "${sessionId}" not found`, + { sessionId }, + ) + } + throw failure( + 'internal', + `session "${sessionId}" could not be inspected: ${String(error)}`, + ) + } + if (cwd === undefined) { + throw failure('internal', `session "${sessionId}" has no project cwd`) + } + + const live = this.ctx.agents.get(sessionId) + const presets = this.ctx.get('agentPresets') + const scoped = live === undefined ? undefined : presets?.serviceFor(live, 'skills') + const skillRegistry = scoped ?? this.ctx.get('skills') + if (skillRegistry === undefined) { + throw failure( + 'internal', + 'skill registry is absent: neither this session\'s agent preset nor the host composition mounts @deepseek-ai/dsh-skill', + ) + } + + const scope = await this.scopeFor(sessionId, agentPreset) + try { + const skills = (await skillRegistry.list({ cwd, scope })).filter(isUserInvocable) + return { + skills: skills.map(skill => ({ + name: skill.name, + description: skill.description, + ...skill.whenToUse === undefined ? {} : { whenToUse: skill.whenToUse }, + modelInvocable: skill.invocation.modelInvocable, + })), + } + } catch (error: unknown) { + throw failure('internal', `skill listing failed: ${String(error)}`) + } + } + + /** Resolve a live or standing preset scope without creating an Agent. */ + private async scopeFor( + sessionId: SessionId, + agentPreset: string | undefined, + ): Promise { + const live = this.ctx.agents.get(sessionId) + if (live !== undefined) return live + const presets = this.ctx.get('agentPresets') + if (presets === undefined) return undefined + try { + return await presets.standingKeyFor(agentPreset) + } catch { + // An unknown or unusable recorded preset falls back to the global registry. + return undefined + } + } +} + +/** Build one stable Remote failure with optional typed details. */ +function failure( + code: 'session-not-found' | 'internal', + message: string, + details: { readonly sessionId: SessionId } | Record = {}, +): TypertRemoteFailure { + return new TypertRemoteFailure({ code, message, details }) +} + +export default SessionSkillCatalog diff --git a/packages/api/session-controller/src/types.ts b/packages/api/session-controller/src/types.ts index e9e416777c..24e9232d07 100644 --- a/packages/api/session-controller/src/types.ts +++ b/packages/api/session-controller/src/types.ts @@ -223,6 +223,28 @@ export type SessionError = { } }[keyof SessionErrorDetailsMap] +/** Session-addressed request for the human-invocable skill catalog. */ +export interface SkillListRequest { + readonly sessionId: SessionId +} + +/** One skill available to the Session's human-facing composer. */ +export interface SkillEntry { + /** Kebab-case identifier referenced as `/name`. */ + readonly name: string + /** Short routing description. */ + readonly description: string + /** Optional extra routing guidance. */ + readonly whenToUse?: string + /** Whether the same skill is also advertised to the model. */ + readonly modelInvocable: boolean +} + +/** Human-invocable skills visible through one Session's composition. */ +export interface SkillListValue { + readonly skills: readonly SkillEntry[] +} + /** Session list request. */ export interface SessionListRequest { readonly cursor?: string @@ -340,6 +362,18 @@ export interface SessionCancelValue { readonly accepted: true } +/** Session-addressed request to open one workspace path on the Host desktop. */ +export interface SessionOpenWorkspacePathRequest { + readonly sessionId: SessionId + /** Absolute or Session-workspace-relative path. */ + readonly path: string +} + +/** Confirmation that the Host handed a workspace path to its native opener. */ +export interface SessionOpenWorkspacePathValue { + readonly opened: true +} + /** Client-minted prompt identity used to reconcile optimistic and durable messages. */ export type SessionRequestId = Branded<'session-request-id'> diff --git a/packages/api/session-controller/tsconfig.host.json b/packages/api/session-controller/tsconfig.host.json index d3256426e2..e3f40c885d 100644 --- a/packages/api/session-controller/tsconfig.host.json +++ b/packages/api/session-controller/tsconfig.host.json @@ -14,9 +14,11 @@ "src/catalog.ts", "src/commands.ts", "src/control.ts", + "src/file-references.ts", "src/history.ts", "src/list.ts", - "src/model-selection-projection.ts" + "src/model-selection-projection.ts", + "src/skill-catalog.ts" ], "references": [ { "path": "../../../vendor/cordis" }, @@ -25,10 +27,12 @@ { "path": "../../core/agent-default-model" }, { "path": "../../core/scope" }, { "path": "../../core/session" }, + { "path": "../../context/file-reference" }, { "path": "../../attachment/attachment" }, { "path": "../../interaction/permission-presets" }, { "path": "../../jobs/jobs" }, { "path": "../../llm/llm" }, + { "path": "../../util/native-command" }, { "path": "../../preset/agent-presets" }, { "path": "../../runtime-diagnostics/invariants" }, { "path": "../../session/session-persistence" }, @@ -36,9 +40,11 @@ { "path": "../../session/session-projection-cache" }, { "path": "../../session/session-title" }, { "path": "../../session-query/session-query" }, + { "path": "../../skill/skill" }, { "path": "../../subagent/subagent" }, { "path": "../../typert/protocol" }, { "path": "../../typert/registry" }, + { "path": "../../util/workspace-path" }, { "path": "../../workspace/workspace" } ] } diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json index ed518756fb..4f4326f47e 100644 --- a/packages/api/settings-controller/package.json +++ b/packages/api/settings-controller/package.json @@ -49,22 +49,28 @@ ], "license": "MIT", "dependencies": { + "@deepseek-ai/schemastery": "workspace:^", "zod": "^4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-agent-presets": "workspace:^", "@deepseek-ai/dsh-credentials": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-native-command": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-typert-protocol": "workspace:^" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-agent-presets": "workspace:^", "@deepseek-ai/dsh-credentials": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-native-command": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-settings": "workspace:^", - "@deepseek-ai/dsh-typert-protocol": "workspace:^" + "@deepseek-ai/dsh-typert-protocol": "workspace:^", + "@deepseek-ai/schemastery": "workspace:^" } } diff --git a/packages/api/settings-controller/src/index.ts b/packages/api/settings-controller/src/index.ts index e893d06649..a61dfd329e 100644 --- a/packages/api/settings-controller/src/index.ts +++ b/packages/api/settings-controller/src/index.ts @@ -7,7 +7,20 @@ * @module @deepseek-ai/dsh-api-settings-controller */ +import { dirname } from 'node:path' import { Context } from '@deepseek-ai/cordis' +import Schema from '@deepseek-ai/schemastery' +import { + InvalidPresetIdError, + PresetExistsError, + PresetNotWritableError, + UnknownPresetError, +} from '@deepseek-ai/dsh-agent-presets' +import { + canOpenNativePath, + openNativePath, + openNativeTextFile, +} from '@deepseek-ai/dsh-native-command' import { SettingsConflictError, settingsNamespace } from '@deepseek-ai/dsh-settings' import type { SettingsDescriptor, SettingsPathOp, SettingsProvider } from '@deepseek-ai/dsh-settings' import type { @@ -17,12 +30,26 @@ import type { JsonValue } from '@deepseek-ai/dsh-session/types' import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import { z } from 'zod' import { CredentialsController } from './credentials.ts' +import type { AgentPresetDirectoryOpenValue, SettingsDocumentOpenValue } from './types.ts' export { CredentialsController } from './credentials.ts' export type * from './types.ts' const settingsNamespaceRequestSchema = z.object({ ns: z.string().min(1) }) +/** Native document-opening policy. */ +export interface Config { + /** Override platform desktop-opener detection. */ + readonly nativeOpen?: boolean +} + +/** Host integrations replaceable by direct unit tests. */ +export interface SettingsControllerInternals { + readonly openPath?: (path: string, signal: AbortSignal) => Promise + readonly openTextFile?: (path: string, signal: AbortSignal) => Promise + readonly canOpenPath?: () => boolean +} + /** * Project one redacted descriptor onto its wire view, field by field. The * Gateway returns a business result without decoding it, so a provider whose @@ -59,14 +86,24 @@ declare module '@deepseek-ai/cordis' { * `settings-conflict` or `settings-rejected` with the service's message. */ export class SettingsController extends TypertRemoteService { + static Config: Schema = Schema.object({ nativeOpen: Schema.boolean() }) + + private readonly openPath: (path: string, signal: AbortSignal) => Promise + private readonly openTextFile: (path: string, signal: AbortSignal) => Promise + private readonly canOpenPath: () => boolean + /** * Register the settings namespace and mount the credentials namespace beside * it. Both namespaces stay registered when a provider is absent so calls can * return the configuration API's actionable missing-provider diagnostic. * @param ctx - Host context where settings and credential providers may be mounted. */ - constructor(ctx: Context) { + constructor(ctx: Context, config: Config = {}, internals: SettingsControllerInternals = {}) { super(ctx, 'settingsController', { namespace: 'settings' }) + this.openPath = internals.openPath ?? openNativePath + this.openTextFile = internals.openTextFile ?? openNativeTextFile + this.canOpenPath = internals.canOpenPath + ?? (() => config.nativeOpen ?? (internals.openPath !== undefined || canOpenNativePath())) ctx.plugin(CredentialsController) } @@ -139,6 +176,81 @@ export class SettingsController extends TypertRemoteService { return this.write(ns, 'mutate', ops, expectedRevision) } + /** + * Materialize the provider-owned settings document and open it in a native text editor. + * @param signal - caller lifetime; abort terminates preparation or the native command. + * @returns confirmation after the native opener accepts the document. + * @throws TypertRemoteFailure when no document exists, preparation fails, or opening fails. + */ + @Remote + async openSettingsDocument(signal: AbortSignal): Promise { + const settings = this.provider() + if (signal.aborted) throw cancelled('settings document open was aborted') + let path: string | undefined + try { + path = await settings.prepareDocument() + } catch (error: unknown) { + if (signal.aborted) throw cancelled('settings document preparation was aborted') + throw internal(`settings document preparation failed: ${messageOf(error)}`) + } + if (path === undefined) { + throw internal('settings provider has no local document to open') + } + if (signal.aborted) throw cancelled('settings document open was aborted') + try { + await this.openTextFile(path, signal) + return { opened: true } + } catch (error: unknown) { + if (signal.aborted) throw cancelled('settings document open was aborted') + throw internal(`path open failed: ${messageOf(error)}`) + } + } + + /** + * Open one user-authored Agent preset directory or return its path when no native opener exists. + * @param agentPreset - preset id resolved against Host-owned roots. + * @param signal - caller lifetime; abort terminates the native command. + * @returns an opened confirmation or the resolved directory for text display. + * @throws TypertRemoteFailure when the preset is missing, read-only, invalid, or cannot be opened. + */ + @Remote + async openAgentPresetDirectory( + agentPreset: string, + signal: AbortSignal, + ): Promise { + if (agentPreset.length === 0) { + throw new TypertRemoteFailure({ + code: 'bad-request', message: 'agent preset id must not be empty', details: {}, + }) + } + const presets = this.ctx.get('agentPresets') + if (presets === undefined) { + throw new TypertRemoteFailure({ + code: 'agent-preset-not-found', + message: 'this deployment composes no agent presets', + details: { agentPreset, available: [] }, + }) + } + let directory: string + try { + const preset = await presets.resolve(agentPreset) + if (preset.trust !== 'user') { + throw new PresetNotWritableError(preset.id, 'it ships with the deployment') + } + directory = dirname(preset.path) + } catch (error: unknown) { + throw presetFailure(agentPreset, error) + } + if (!this.canOpenPath()) return { opened: false, path: directory } + try { + await this.openPath(directory, signal) + return { opened: true } + } catch (error: unknown) { + if (signal.aborted) throw cancelled('path open was aborted') + throw internal(`path open failed: ${messageOf(error)}`) + } + } + private async write( ns: string, mode: 'update' | 'replace' | 'mutate', @@ -196,6 +308,44 @@ export class SettingsController extends TypertRemoteService { } } +function messageOf(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +function internal(message: string): TypertRemoteFailure { + return new TypertRemoteFailure({ code: 'internal', message, details: {} }) +} + +function cancelled(message: string): TypertRemoteFailure { + return new TypertRemoteFailure({ code: 'cancelled', message, details: {} }) +} + +function presetFailure(agentPreset: string, error: unknown): TypertRemoteFailure { + if (error instanceof UnknownPresetError) { + return new TypertRemoteFailure({ + code: 'agent-preset-not-found', + message: error.message, + details: { agentPreset: error.presetId, available: [...error.available] }, + }) + } + if (error instanceof PresetNotWritableError) { + return new TypertRemoteFailure({ + code: 'agent-preset-read-only', + message: error.message, + details: { agentPreset, reason: error.message }, + }) + } + if (error instanceof InvalidPresetIdError || error instanceof PresetExistsError) { + return new TypertRemoteFailure({ + code: 'agent-preset-invalid', + message: error.message, + details: { agentPreset, reason: error.message }, + }) + } + if (error instanceof TypertRemoteFailure) return error + return internal(`agent preset "${agentPreset}": ${String(error)}`) +} + /** * Classify one seam refusal. A stale writer is its own outcome, not a malformed * request: the client must re-read and re-apply rather than treat the write as diff --git a/packages/api/settings-controller/src/types.ts b/packages/api/settings-controller/src/types.ts index 81249e3065..5fde28ae89 100644 --- a/packages/api/settings-controller/src/types.ts +++ b/packages/api/settings-controller/src/types.ts @@ -30,6 +30,16 @@ export type SettingsError = { } }[keyof SettingsErrorDetailsMap] +/** Confirmation that the settings document was handed to the native editor. */ +export interface SettingsDocumentOpenValue { + readonly opened: true +} + +/** Result of opening or revealing one locally authored Agent preset directory. */ +export type AgentPresetDirectoryOpenValue = + | { readonly opened: true } + | { readonly opened: false; readonly path: string } + /** Stable credential failure details returned by the `credentials` namespace. */ export interface CredentialErrorDetailsMap { /** diff --git a/packages/api/settings-controller/tsconfig.json b/packages/api/settings-controller/tsconfig.json index 5b854dbe64..763aa37777 100644 --- a/packages/api/settings-controller/tsconfig.json +++ b/packages/api/settings-controller/tsconfig.json @@ -11,6 +11,12 @@ { "path": "../../../vendor/cordis" }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../preset/agent-presets" + }, { "path": "../../credentials/credentials" }, @@ -20,6 +26,9 @@ { "path": "../../runtime-diagnostics/invariants" }, + { + "path": "../../util/native-command" + }, { "path": "../../settings/settings" }, diff --git a/packages/context/file-reference/package.json b/packages/context/file-reference/package.json index 7d34d416cf..f1e45418b6 100644 --- a/packages/context/file-reference/package.json +++ b/packages/context/file-reference/package.json @@ -30,14 +30,6 @@ "types": "./lib/types/types.d.ts", "default": "./lib/types/types.js" }, - "./typert": { - "types": "./lib/typert.host.d.ts", - "default": "./lib/typert.host.js" - }, - "./remote": { - "types": "./lib/typert.remote-client.d.ts", - "default": "./lib/typert.remote-client.js" - }, "./src/*": "./src/*", "./package.json": "./package.json" }, @@ -45,23 +37,17 @@ "lib/index.js", "lib/invariant.js", "lib/types/**/*.js", - "lib/types/**/*.d.ts", - "lib/typert.host.js", - "lib/typert.host.d.ts", - "lib/typert.remote-client.js", - "lib/typert.remote-client.d.ts" + "lib/types/**/*.d.ts" ], "license": "MIT", "peerDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "dependencies": { diff --git a/packages/context/file-reference/src/index.ts b/packages/context/file-reference/src/index.ts index 45da48c929..bf1025714f 100644 --- a/packages/context/file-reference/src/index.ts +++ b/packages/context/file-reference/src/index.ts @@ -4,9 +4,8 @@ * @module @deepseek-ai/dsh-file-reference */ -import type { Context } from '@deepseek-ai/cordis' +import { Service, type Context } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' -import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import type { FileReferenceCandidate } from './types.ts' @@ -24,7 +23,7 @@ declare module '@deepseek-ai/cordis' { } /** Host capability for cancellable file-reference discovery. */ -export abstract class FileReferenceService extends TypertRemoteService { +export abstract class FileReferenceService extends Service { constructor(ctx: Context) { super(ctx, 'fileReferences') } @@ -41,23 +40,6 @@ export abstract class FileReferenceService extends TypertRemoteService { query: string, signal: AbortSignal, ): Promise - - /** - * Remote face of {@link list}; the decorator cannot mark the abstract - * member, so this concrete adapter carries the identical contract. - * @param agent - target agent whose session cwd bounds discovery. - * @param query - path text following `@` or `@"`. - * @param signal - caller cancellation. - * @returns deterministic path-only candidates. - */ - @Remote('list') - remoteExportList( - agent: Agent, - query: string, - signal: AbortSignal, - ): Promise { - return this.list(agent, query, signal) - } } export default FileReferenceService diff --git a/packages/llm/llm-pi-ai/src/discovery.ts b/packages/llm/llm-pi-ai/src/discovery.ts index 014c9c2f3e..bb9915b116 100644 --- a/packages/llm/llm-pi-ai/src/discovery.ts +++ b/packages/llm/llm-pi-ai/src/discovery.ts @@ -23,7 +23,7 @@ */ import { INVALID_CREDENTIAL_CODE, LlmError, normalizeApiKey } from '@deepseek-ai/dsh-llm' -import type { LlmDiscoveredModel, LlmModelDiscoveryRequest } from '@deepseek-ai/dsh-llm' +import type { LlmDiscoveredModel, LlmModelDiscoveryOperation } from '@deepseek-ai/dsh-llm' import { attributionHeaders } from '@deepseek-ai/dsh-llm' import { catalogModels } from './catalog.ts' @@ -193,7 +193,7 @@ function usableProbeKey(raw: string): string { * refuses or fails the request, or the reply is not a model listing. */ export async function discoverModels( - request: LlmModelDiscoveryRequest, + request: LlmModelDiscoveryOperation, storedApiKey?: () => Promise, ): Promise { // A catalog route already has its answer, and a better one: the installed diff --git a/packages/llm/llm-pi-ai/src/index.ts b/packages/llm/llm-pi-ai/src/index.ts index c9752b764e..58d5f620c8 100644 --- a/packages/llm/llm-pi-ai/src/index.ts +++ b/packages/llm/llm-pi-ai/src/index.ts @@ -257,7 +257,10 @@ export function apply(ctx: Context, config: Config): void { // except the credential: a configuration surface edits a redacted descriptor // and never holds a stored secret, so an already-configured route supplies // its own here rather than being interrogated unauthenticated. - ctx.llm.registerModelDiscovery(NS, request => discoverModels(request, () => storedApiKey(request.provider))) + ctx.llm.registerModelDiscovery(NS, (request, signal) => discoverModels( + { ...request, ...signal === undefined ? {} : { signal } }, + () => storedApiKey(request.provider), + )) // Route effects bind to this apply fiber via the stable `ctx` reference, // even when a swap runs inside the scoped settings callback below. A bare // mount (zero routes) is the dormant posture: nothing registers until a diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json index cf032be43e..1ec340226e 100644 --- a/packages/llm/llm/package.json +++ b/packages/llm/llm/package.json @@ -34,6 +34,14 @@ "types": "./lib/types/message.d.ts", "default": "./lib/types/message.js" }, + "./typert": { + "types": "./lib/typert.host.d.ts", + "default": "./lib/typert.host.js" + }, + "./remote": { + "types": "./lib/typert.remote-client.d.ts", + "default": "./lib/typert.remote-client.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, @@ -41,7 +49,11 @@ "lib/index.js", "lib/invariant.js", "lib/types/**/*.js", - "lib/types/**/*.d.ts" + "lib/types/**/*.d.ts", + "lib/typert.host.js", + "lib/typert.host.d.ts", + "lib/typert.remote-client.js", + "lib/typert.remote-client.d.ts" ], "license": "MIT", "peerDependencies": { @@ -49,17 +61,20 @@ "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "dependencies": { "@deepseek-ai/dsh-util-crypto": "workspace:^", - "@deepseek-ai/schemastery": "workspace:^" + "@deepseek-ai/schemastery": "workspace:^", + "zod": "^4.4.3" }, "devDependencies": { "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", + "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/cordis": "workspace:^" } } diff --git a/packages/llm/llm/src/index.ts b/packages/llm/llm/src/index.ts index c1ec12e3c9..37b6795a13 100644 --- a/packages/llm/llm/src/index.ts +++ b/packages/llm/llm/src/index.ts @@ -6,7 +6,8 @@ * @module @deepseek-ai/dsh-llm */ -import { Context, Service } from '@deepseek-ai/cordis' +import { Context } from '@deepseek-ai/cordis' +import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import type { GenerateOptions, LlmConfigurableProvider, @@ -322,12 +323,12 @@ export interface DirectoryRegistrationHandle { * The abstract `llm` service: an adapter registry plus a streaming model-call * API, interceptable via the `llm/stream` waterfall. */ -export class LlmRuntime extends Service { +export class LlmRuntime extends TypertRemoteService { private adapters = new Map() private directory = new Map() private discoveries = new Map< string, - (request: LlmModelDiscoveryRequest) => Promise + (request: LlmModelDiscoveryRequest, signal?: AbortSignal) => Promise >() constructor(ctx: Context) { @@ -457,6 +458,7 @@ export class LlmRuntime extends Service { * Describe provider routes with a registered adapter. * @returns detached provider metadata in registration order. */ + @Remote listProviders(): LlmProviderInfo[] { return [...this.adapters.values()].map(({ provider }) => ({ ...provider })) } @@ -528,6 +530,7 @@ export class LlmRuntime extends Service { * List every declared configurable provider, registered or dormant. * @returns detached directory entries in declaration order. */ + @Remote listConfigurableProviders(): LlmConfigurableProvider[] { return [...this.directory.values()].map(entry => ({ ...entry, settingsPath: [...entry.settingsPath] })) } @@ -539,12 +542,15 @@ export class LlmRuntime extends Service { * directory, and because a provider being *added* has no route to name yet. * Disposed with the fiber. * @param settingsNs - the namespace whose profiles this discovery serves. - * @param discover - interrogates one endpoint; must honor `request.signal`. + * @param discover - interrogates one endpoint and must honor the supplied signal. * @returns the disposer that withdraws the offer. */ registerModelDiscovery( settingsNs: string, - discover: (request: LlmModelDiscoveryRequest) => Promise, + discover: ( + request: LlmModelDiscoveryRequest, + signal?: AbortSignal, + ) => Promise, ): () => void { const dispose = this.ctx.effect(function* (this: LlmRuntime) { if (settingsNs.length === 0) { @@ -568,11 +574,13 @@ export class LlmRuntime extends Service { * candidate metadata a surface may offer for adoption. * @param settingsNs - namespace whose registered discovery serves this draft. * @param request - the endpoint, protocol, and one-shot credential to use. + * @param signal - caller cancellation. * @returns the advertised models, deduplicated in endpoint order. */ async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, + signal?: AbortSignal, ): Promise { const discover = this.discoveries.get(settingsNs) if (discover === undefined) { @@ -583,7 +591,9 @@ export class LlmRuntime extends Service { if ((request.provider ?? '').length === 0 && (request.baseURL ?? '').length === 0) { throw new LlmError('model discovery needs a provider route or a baseURL', 'INVALID_DISCOVERY') } - const discovered = await discover(request) + const discovered = signal === undefined + ? await discover(request) + : await discover(request, signal) const seen = new Set() const models: LlmDiscoveredModel[] = [] for (const model of discovered) { @@ -599,6 +609,34 @@ export class LlmRuntime extends Service { return models } + /** + * Remote adapter for one draft provider interrogation. + * @param settingsNs - namespace whose registered discovery serves this draft. + * @param request - endpoint, protocol, and one-shot credential to use. + * @param signal - caller cancellation supplied by the Remote carrier. + * @returns advertised models in endpoint order. + * @throws TypertRemoteFailure with `model-discovery-failed` when discovery refuses or fails. + */ + @Remote('discoverModels') + async remoteDiscoverModels( + settingsNs: string, + request: LlmModelDiscoveryRequest, + signal: AbortSignal, + ): Promise { + try { + return await this.discoverModels(settingsNs, request, signal) + } catch (error: unknown) { + throw new TypertRemoteFailure({ + code: 'model-discovery-failed', + message: error instanceof Error ? error.message : String(error), + details: { + settingsNs, + ...request.baseURL === undefined ? {} : { baseURL: request.baseURL }, + }, + }) + } + } + /** * Resolve the retry policy captured when one provider route was registered. * @param provider - registered provider route to inspect. diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts index e7dadc6b20..438fcaa67b 100644 --- a/packages/llm/llm/src/types.ts +++ b/packages/llm/llm/src/types.ts @@ -247,10 +247,24 @@ export interface LlmModelDiscoveryRequest { api?: string /** Credential for this interrogation alone; the harness never stores it. */ apiKey?: string +} + +/** Provider-side discovery request with operation-local cancellation attached. */ +export interface LlmModelDiscoveryOperation extends LlmModelDiscoveryRequest { /** Caller cancellation; implementations must settle promptly after it aborts. */ signal?: AbortSignal } +/** Stable failure returned by the `llm/discoverModels` Remote method. */ +export interface LlmModelDiscoveryError { + readonly code: 'model-discovery-failed' + readonly message: string + readonly details: { + readonly settingsNs: string + readonly baseURL?: string + } +} + /** * One model an endpoint reports about itself. Every field but the id is * optional because most provider listings disclose an id and nothing else; diff --git a/packages/llm/llm/tsconfig.json b/packages/llm/llm/tsconfig.json index 2206a6028e..f561d7b5e0 100644 --- a/packages/llm/llm/tsconfig.json +++ b/packages/llm/llm/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "../../util/crypto" + }, + { + "path": "../../typert/protocol" } ] } diff --git a/packages/util/native-command/src/index.ts b/packages/util/native-command/src/index.ts index 8ad8b81749..14100af799 100644 --- a/packages/util/native-command/src/index.ts +++ b/packages/util/native-command/src/index.ts @@ -1,44 +1,16 @@ /** - * Shared no-shell `execFile` runner for host-native OS integrations (the - * native directory chooser, the open-with-default-application hand-off): - * utf8 stdio capture, abort propagation, Windows console hide. A library, - * not a plugin — no ctx, no state, no events. + * Host-native command execution and path-opening utilities. * @module @deepseek-ai/dsh-native-command */ -import { execFile } from 'node:child_process' - -/** Testable command boundary; native implementations never invoke a shell. */ -export type NativeCommandRunner = ( - command: string, - args: readonly string[], - signal: AbortSignal, -) => Promise<{ stdout: string; stderr: string }> - -/** - * Run a host command with utf8 stdio, abort propagation, and Windows hide. - * @param command - executable path or PATH name. - * @param args - argv (never a shell string). - * @param signal - caller/connection lifetime; abort terminates the child. - * @returns captured stdout/stderr on exit 0. - */ -export const runNativeCommand: NativeCommandRunner = (command, args, signal) => - new Promise((resolve, reject) => { - execFile( - command, - [...args], - { encoding: 'utf8', signal, windowsHide: true }, - (error, stdout, stderr) => { - if (error !== null) { - const failure = Object.assign(new Error(error.message, { cause: error }), { - code: error.code, - stdout, - stderr, - }) - reject(failure) - return - } - resolve({ stdout, stderr }) - }, - ) - }) +export { runNativeCommand } from './runner.ts' +export type { NativeCommandRunner } from './runner.ts' +export { + canOpenNativePath, + openNativePath, + openNativeTextFile, +} from './path-opener.ts' +export type { + PathOpenerInternals, + PathOpenerRunner, +} from './path-opener.ts' diff --git a/packages/util/native-command/src/path-opener.ts b/packages/util/native-command/src/path-opener.ts new file mode 100644 index 0000000000..97d2084e61 --- /dev/null +++ b/packages/util/native-command/src/path-opener.ts @@ -0,0 +1,203 @@ +/** + * Cross-platform native path and text-document openers for Host UI + * integrations. + * + * The default intent prefers the default browser for documents it renders when + * the platform can name one, then falls back to the default application. WSL + * translates every path for the Windows desktop instead of assuming a Linux + * GUI. The text-editor intent never consults the browser. + * @module @deepseek-ai/dsh-native-command/path-opener + */ + +import { release as osRelease } from 'node:os' +import { extname } from 'node:path' +import { runNativeCommand, type NativeCommandRunner } from './runner.ts' + +/** Testable command boundary; native implementations never invoke a shell. */ +export type PathOpenerRunner = NativeCommandRunner + +/** Injectable platform facts for deterministic adapter tests. */ +export interface PathOpenerInternals { + platform?: NodeJS.Platform + /** Kernel release override used to distinguish WSL from desktop Linux. */ + osRelease?: string + /** Environment used for WSL markers and the desktop Linux browser convention. */ + env?: NodeJS.ProcessEnv + run?: PathOpenerRunner +} + +/** Documents a browser renders, as opposed to ones an editor merely edits. */ +const BROWSER_DOCUMENTS = new Set(['.html', '.htm', '.xhtml', '.svg']) + +/** + * The macOS bundle registered for `https` — the default browser, as + * LaunchServices records it. The nested version dict is stripped first + * because it carries its own `LSHandlerRoleAll`. + */ +function macBundleForHttps(plist: string): string | undefined { + const stripped = plist.replace(/LSHandlerPreferredVersions\s*=\s*\{[^}]*\};/g, '') + const block = /\{[^{}]*LSHandlerURLScheme\s*=\s*"?https"?;[^{}]*\}/.exec(stripped)?.[0] + if (block === undefined) return undefined + return /LSHandlerRoleAll\s*=\s*"?([\w.-]+)"?;/.exec(block)?.[1] +} + +/** + * Open one browser-renderable document with the default browser. + * @returns true when a browser took it; false when this platform cannot name + * one, or naming it failed — the caller then uses the default application. + */ +async function openInBrowser( + path: string, signal: AbortSignal, platform: NodeJS.Platform, + run: PathOpenerRunner, env: NodeJS.ProcessEnv, +): Promise { + if (platform === 'darwin') { + let bundle: string | undefined + try { + const { stdout } = await run( + 'defaults', ['read', 'com.apple.LaunchServices/com.apple.launchservices.secure'], signal) + bundle = macBundleForHttps(stdout) + } catch { + // No LaunchServices record (a fresh account never changed a default): + // the content-type handler is then the system's own choice anyway. + return false + } + if (bundle === undefined) return false + await run('open', ['-b', bundle, path], signal) + return true + } + if (platform === 'linux') { + // $BROWSER is the portable convention; desktop-entry resolution through + // xdg-settings needs a launcher this package has no business shipping. + const browser = env.BROWSER + if (browser === undefined || browser === '') return false + await run(browser, [path], signal) + return true + } + // Windows names no browser without reading the UserChoice registry, and its + // .html association is the browser in the ordinary case. + return false +} + +/** Native path-open intent; macOS distinguishes text editing from file association. */ +type PathOpenIntent = 'default' | 'text-editor' + +/** PowerShell single-quoted literal (doubles embedded quotes). */ +function powershellLiteral(path: string): string { + return `'${path.replace(/'/g, "''")}'` +} + +/** Whether one environment marker is set to a non-empty value. */ +function present(value: string | undefined): boolean { + return value !== undefined && value !== '' +} + +/** Distinguish WSL from desktop Linux using its process and kernel markers. */ +function isWsl(internals: PathOpenerInternals): boolean { + const env = internals.env ?? process.env + if (present(env.WSL_DISTRO_NAME) || present(env.WSL_INTEROP)) return true + return (internals.osRelease ?? osRelease()).toLowerCase().includes('microsoft') +} + +/** Open one Windows-resolvable path through its registered desktop application. */ +async function openWindowsPath(path: string, signal: AbortSignal, run: PathOpenerRunner): Promise { + await run('powershell.exe', [ + '-NoProfile', + '-Command', + `Invoke-Item -LiteralPath ${powershellLiteral(path)}`, + ], signal) +} + +/** Translate a WSL path before handing it to the Windows desktop. */ +async function openWslPath(path: string, signal: AbortSignal, run: PathOpenerRunner): Promise { + const translated = await run('wslpath', ['-w', path], signal) + signal.throwIfAborted() + const windowsPath = translated.stdout.replace(/[\r\n]+$/, '') + if (windowsPath === '') throw new Error('wslpath returned no Windows path') + await openWindowsPath(windowsPath, signal, run) +} + +/** Dispatch one shell-free platform command for the requested open intent. */ +async function openNativePathWithIntent( + path: string, + signal: AbortSignal, + intent: PathOpenIntent, + internals: PathOpenerInternals = {}, +): Promise { + const platform = internals.platform ?? process.platform + const run = internals.run ?? runNativeCommand + const env = internals.env ?? process.env + const wsl = platform === 'linux' && isWsl(internals) + + if (!wsl && intent === 'default' && BROWSER_DOCUMENTS.has(extname(path).toLowerCase()) + && await openInBrowser(path, signal, platform, run, env)) return + + if (platform === 'darwin') { + await run('open', intent === 'text-editor' ? ['-t', path] : [path], signal) + return + } + + if (platform === 'win32') { + await openWindowsPath(path, signal, run) + return + } + + if (platform === 'linux') { + if (wsl) { + await openWslPath(path, signal, run) + return + } + await run('xdg-open', [path], signal) + return + } + + throw new Error(`native path opener is unsupported on ${platform}`) +} + +/** + * Whether {@link openNativePath} plausibly reaches a desktop on this host. + * + * macOS and Windows always carry a desktop opener; Linux does when it is WSL + * (the Windows desktop takes the path) or a display server is announced. + * A headless or containerised Linux host answers false, which is what lets a + * surface show a path as text instead of offering a button that would spawn + * `xdg-open` into nothing. + * @param internals - platform and environment seam for deterministic tests. + * @returns true when handing a path to the native opener can work at all. + */ +export function canOpenNativePath(internals: PathOpenerInternals = {}): boolean { + const platform = internals.platform ?? process.platform + if (platform === 'darwin' || platform === 'win32') return true + if (platform !== 'linux') return false + const env = internals.env ?? process.env + return isWsl(internals) || present(env.DISPLAY) || present(env.WAYLAND_DISPLAY) +} + +/** + * Open a filesystem path with the operating system's default application, or + * with the default browser when the path names a document a browser renders. + * @param path - absolute or host-resolvable path (caller owns resolution). + * @param signal - caller/connection lifetime; abort terminates the native command. + * @param internals - Platform, environment, and runner hooks for deterministic tests. + */ +export function openNativePath( + path: string, + signal: AbortSignal, + internals: PathOpenerInternals = {}, +): Promise { + return openNativePathWithIntent(path, signal, 'default', internals) +} + +/** + * Open a text document for editing; macOS bypasses the file-type association + * so a YAML association with a browser cannot consume the gesture. + * @param path - absolute or host-resolvable text-document path. + * @param signal - caller/connection lifetime; abort terminates the native command. + * @param internals - Platform and runner hooks for deterministic tests. + */ +export function openNativeTextFile( + path: string, + signal: AbortSignal, + internals: PathOpenerInternals = {}, +): Promise { + return openNativePathWithIntent(path, signal, 'text-editor', internals) +} diff --git a/packages/util/native-command/src/runner.ts b/packages/util/native-command/src/runner.ts new file mode 100644 index 0000000000..58003baa55 --- /dev/null +++ b/packages/util/native-command/src/runner.ts @@ -0,0 +1,41 @@ +/** + * Shared no-shell `execFile` runner for host-native OS integrations. + * @module @deepseek-ai/dsh-native-command/runner + */ + +import { execFile } from 'node:child_process' + +/** Testable command boundary; native implementations never invoke a shell. */ +export type NativeCommandRunner = ( + command: string, + args: readonly string[], + signal: AbortSignal, +) => Promise<{ stdout: string; stderr: string }> + +/** + * Run a host command with utf8 stdio, abort propagation, and Windows hide. + * @param command - executable path or PATH name. + * @param args - argv (never a shell string). + * @param signal - caller/connection lifetime; abort terminates the child. + * @returns captured stdout/stderr on exit 0. + */ +export const runNativeCommand: NativeCommandRunner = (command, args, signal) => + new Promise((resolve, reject) => { + execFile( + command, + [...args], + { encoding: 'utf8', signal, windowsHide: true }, + (error, stdout, stderr) => { + if (error !== null) { + const failure = Object.assign(new Error(error.message, { cause: error }), { + code: error.code, + stdout, + stderr, + }) + reject(failure) + return + } + resolve({ stdout, stderr }) + }, + ) + }) From 5b2f679e4a32f074fad5c2f48164473753184570 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:08:09 +0800 Subject: [PATCH 02/13] refactor(client): consume migrated Remote namespaces --- packages/api/remotes/src/client/index.ts | 20 ++- packages/client/connection/src/client/api.ts | 3 - .../client/connection/src/client/fixture.ts | 157 +++++++++--------- .../client/connection/src/client/index.ts | 3 - .../ui-agent-preset/src/client/index.ts | 2 +- .../src/client/section-store.ts | 17 +- packages/client/ui-chat/package.json | 4 +- packages/client/ui-chat/src/client/apply.ts | 11 +- packages/client/ui-chat/tsconfig.json | 3 - .../client/ui-model-selection/package.json | 3 - .../ui-model-selection/src/client/catalog.ts | 19 +-- .../ui-model-selection/src/client/index.ts | 2 +- .../ui-model-selection/src/client/service.ts | 7 +- .../ui-settings-general/src/client/index.ts | 4 +- .../src/client/settings-document-store.ts | 8 +- .../client/ui-settings-models/package.json | 2 - .../src/client/ModelListEditor.tsx | 19 +-- .../ui-settings-models/src/client/index.ts | 14 +- .../src/client/slot-contract.ts | 6 +- .../ui-settings-models/src/client/store.ts | 76 +++++++-- .../ui-settings-plugins/src/client/index.ts | 8 +- ...ubagent-model-selection-card-controller.ts | 14 +- packages/client/ui-skill/src/client/index.ts | 12 +- .../client/ui-workspace/src/client/index.ts | 2 +- .../ui-workspace/src/client/navigation.ts | 15 -- 25 files changed, 219 insertions(+), 212 deletions(-) diff --git a/packages/api/remotes/src/client/index.ts b/packages/api/remotes/src/client/index.ts index 8638c54b9f..d40e9f65fc 100644 --- a/packages/api/remotes/src/client/index.ts +++ b/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> { const disposers: Array<() => Promise> = [] try { for (const contribution of [ - agentPresetsRemote, commandsRemote, settingsControllerRemote, goalsRemote, dynamicRemote, - fileReferencesRemote, + agentPresetsRemote, commandsRemote, settingsControllerRemote, goalsRemote, llmRemote, dynamicRemote, pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote, subagentsRemote, sessionRemote, workspaceRemote, ]) { diff --git a/packages/client/connection/src/client/api.ts b/packages/client/connection/src/client/api.ts index 4d4826da79..0ae8af9ddf 100644 --- a/packages/client/connection/src/client/api.ts +++ b/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, diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index 6cc57082cb..a5689069c2 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/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(request: RpcRequest

, value: T): Promise> { return Promise.resolve({ rpcId: request.rpcId, result: { ok: true, value } }) } - function err(request: RpcRequest

, error: Extract, { ok: false }>['error']): Promise> { - return Promise.resolve({ rpcId: request.rpcId, result: { ok: false, error } }) - } - function sessionOk(value: T): Promise> { 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> | 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> | 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, signal?: AbortSignal, ): Promise>> { + 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, - signal ?? new AbortController().signal, ) as RpcResponse> 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, - signal: AbortSignal, ): Promise> { 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) } } diff --git a/packages/client/connection/src/client/index.ts b/packages/client/connection/src/client/index.ts index c8b66fbc77..a24d014496 100644 --- a/packages/client/connection/src/client/index.ts +++ b/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, diff --git a/packages/client/ui-agent-preset/src/client/index.ts b/packages/client/ui-agent-preset/src/client/index.ts index dda3c9d26e..4019655d72 100644 --- a/packages/client/ui-agent-preset/src/client/index.ts +++ b/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() }) diff --git a/packages/client/ui-agent-preset/src/client/section-store.ts b/packages/client/ui-agent-preset/src/client/section-store.ts index 099d92b186..70baab32ec 100644 --- a/packages/client/ui-agent-preset/src/client/section-store.ts +++ b/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 = createSnapshotStore(INITIAL) constructor( - private readonly api: SettingsWireFace & Pick, - private readonly remote: Pick, + private readonly api: Pick, + private readonly remote: Pick, /** * 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 { 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 { - const failure = await writeDefaultPreset(this.api, id) + const failure = await writeDefaultPreset(this.remote, id) if (failure !== undefined) { this.set({ error: failure }) return diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index 10382a63bf..fcafb63b8b 100644 --- a/packages/client/ui-chat/package.json +++ b/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" }, diff --git a/packages/client/ui-chat/src/client/apply.ts b/packages/client/ui-chat/src/client/apply.ts index 1fc602e1ac..829b36bd0f 100644 --- a/packages/client/ui-chat/src/client/apply.ts +++ b/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( diff --git a/packages/client/ui-chat/tsconfig.json b/packages/client/ui-chat/tsconfig.json index 4d42320885..0800260fbb 100644 --- a/packages/client/ui-chat/tsconfig.json +++ b/packages/client/ui-chat/tsconfig.json @@ -50,9 +50,6 @@ { "path": "../../runtime-diagnostics/invariants" }, - { - "path": "../../util/workspace-path" - }, { "path": "../../session/session-stats" }, diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json index 0d54a69f01..d14dcd894d 100644 --- a/packages/client/ui-model-selection/package.json +++ b/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:^", diff --git a/packages/client/ui-model-selection/src/client/catalog.ts b/packages/client/ui-model-selection/src/client/catalog.ts index b0ec866a9a..5bfbcb2d1e 100644 --- a/packages/client/ui-model-selection/src/client/catalog.ts +++ b/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 | 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) {} /** * 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) => { diff --git a/packages/client/ui-model-selection/src/client/index.ts b/packages/client/ui-model-selection/src/client/index.ts index d68e0463d3..dd7d4a89fd 100644 --- a/packages/client/ui-model-selection/src/client/index.ts +++ b/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 diff --git a/packages/client/ui-model-selection/src/client/service.ts b/packages/client/ui-model-selection/src/client/service.ts index 7e01ecd5a2..2053d70b85 100644 --- a/packages/client/ui-model-selection/src/client/service.ts +++ b/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() diff --git a/packages/client/ui-settings-general/src/client/index.ts b/packages/client/ui-settings-general/src/client/index.ts index d1342e9b1d..abdf2829b3 100644 --- a/packages/client/ui-settings-general/src/client/index.ts +++ b/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 diff --git a/packages/client/ui-settings-general/src/client/settings-document-store.ts b/packages/client/ui-settings-general/src/client/settings-document-store.ts index b545ec66f3..4e7fec2ecd 100644 --- a/packages/client/ui-settings-general/src/client/settings-document-store.ts +++ b/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, + private readonly remote: Pick, 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 { diff --git a/packages/client/ui-settings-models/package.json b/packages/client/ui-settings-models/package.json index 5366374f39..57fa2abcc6 100644 --- a/packages/client/ui-settings-models/package.json +++ b/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:^", diff --git a/packages/client/ui-settings-models/src/client/ModelListEditor.tsx b/packages/client/ui-settings-models/src/client/ModelListEditor.tsx index 20276412bb..a1be9f9085 100644 --- a/packages/client/ui-settings-models/src/client/ModelListEditor.tsx +++ b/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 + api: Pick /** 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(undefined) - const [candidates, setCandidates] = useState(undefined) + const [candidates, setCandidates] = useState(undefined) const [picked, setPicked] = useState>(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 diff --git a/packages/client/ui-settings-models/src/client/index.ts b/packages/client/ui-settings-models/src/client/index.ts index b975c91f1f..c397d795c9 100644 --- a/packages/client/ui-settings-models/src/client/index.ts +++ b/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()) diff --git a/packages/client/ui-settings-models/src/client/slot-contract.ts b/packages/client/ui-settings-models/src/client/slot-contract.ts index b8360e21c9..a9eef19368 100644 --- a/packages/client/ui-settings-models/src/client/slot-contract.ts +++ b/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). */ diff --git a/packages/client/ui-settings-models/src/client/store.ts b/packages/client/ui-settings-models/src/client/store.ts index 6f664bd068..fe98870321 100644 --- a/packages/client/ui-settings-models/src/client/store.ts +++ b/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 +/** 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 +} + /** - * 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. + * 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 interface ModelsWire extends Pick { +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 Remote wire face the Models page reaches. + */ +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 { 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) { diff --git a/packages/client/ui-settings-plugins/src/client/index.ts b/packages/client/ui-settings-plugins/src/client/index.ts index 8dd09f1d03..40ec376432 100644 --- a/packages/client/ui-settings-plugins/src/client/index.ts +++ b/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 diff --git a/packages/client/ui-settings-plugins/src/client/subagent-model-selection-card-controller.ts b/packages/client/ui-settings-plugins/src/client/subagent-model-selection-card-controller.ts index 053074a1ff..9e1b5c2d2a 100644 --- a/packages/client/ui-settings-plugins/src/client/subagent-model-selection-card-controller.ts +++ b/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, - private readonly api: Pick, + private readonly session: Pick, ) { 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 diff --git a/packages/client/ui-skill/src/client/index.ts b/packages/client/ui-skill/src/client/index.ts index 9b66956c2d..5b6a9bb44e 100644 --- a/packages/client/ui-skill/src/client/index.ts +++ b/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 } diff --git a/packages/client/ui-workspace/src/client/index.ts b/packages/client/ui-workspace/src/client/index.ts index 7f87202365..41a9b80fd1 100644 --- a/packages/client/ui-workspace/src/client/index.ts +++ b/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') diff --git a/packages/client/ui-workspace/src/client/navigation.ts b/packages/client/ui-workspace/src/client/navigation.ts index 4d0f3e3fa9..a4e3eb3880 100644 --- a/packages/client/ui-workspace/src/client/navigation.ts +++ b/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 - /** - * Open a path with the Host operating system. - * @param path - absolute or Host-resolvable path. - */ - openPath(path: string): Promise } 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 { - 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 From ce3391e280e74ef16b9c16286deb55d40bf071aa Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:09:27 +0800 Subject: [PATCH 03/13] refactor(apiproxy): retire migrated unary routes --- knip.json | 5 + packages/host/apiproxy/package.json | 9 +- packages/host/apiproxy/src/api-proxy.ts | 335 +----------------- .../apiproxy/src/api/agent-presets.schema.ts | 19 - .../host/apiproxy/src/api/agent-presets.ts | 25 -- packages/host/apiproxy/src/api/host.schema.ts | 10 - packages/host/apiproxy/src/api/host.ts | 10 - packages/host/apiproxy/src/api/index.ts | 12 - packages/host/apiproxy/src/api/llm.schema.ts | 115 ------ packages/host/apiproxy/src/api/llm.ts | 90 ----- packages/host/apiproxy/src/api/rpc-map.ts | 11 - packages/host/apiproxy/src/api/rpc.schema.ts | 1 - packages/host/apiproxy/src/api/rpc.ts | 9 - .../host/apiproxy/src/api/settings.schema.ts | 16 - packages/host/apiproxy/src/api/settings.ts | 22 -- .../host/apiproxy/src/api/skills.schema.ts | 28 -- packages/host/apiproxy/src/api/skills.ts | 33 -- packages/host/apiproxy/src/fetch/client.ts | 58 +-- packages/host/apiproxy/src/fetch/handler.ts | 19 +- packages/host/apiproxy/src/index.ts | 13 +- .../host/apiproxy/src/native-path-opener.ts | 202 ----------- packages/host/apiproxy/tsconfig.json | 21 -- 22 files changed, 14 insertions(+), 1049 deletions(-) delete mode 100644 packages/host/apiproxy/src/api/agent-presets.schema.ts delete mode 100644 packages/host/apiproxy/src/api/agent-presets.ts delete mode 100644 packages/host/apiproxy/src/api/llm.schema.ts delete mode 100644 packages/host/apiproxy/src/api/llm.ts delete mode 100644 packages/host/apiproxy/src/api/settings.schema.ts delete mode 100644 packages/host/apiproxy/src/api/settings.ts delete mode 100644 packages/host/apiproxy/src/api/skills.schema.ts delete mode 100644 packages/host/apiproxy/src/api/skills.ts delete mode 100644 packages/host/apiproxy/src/native-path-opener.ts diff --git a/knip.json b/knip.json index 026215eb4f..b22ef3caaa 100644 --- a/knip.json +++ b/knip.json @@ -415,6 +415,11 @@ "tests/**/*.ts" ] }, + "packages/llm/llm": { + "ignoreDependencies": [ + "zod" + ] + }, "packages/llm/llm-deepseek": { "entry": [ "tests/**/*.spec.ts", diff --git a/packages/host/apiproxy/package.json b/packages/host/apiproxy/package.json index 9942223158..b1eecd9a6b 100644 --- a/packages/host/apiproxy/package.json +++ b/packages/host/apiproxy/package.json @@ -50,16 +50,10 @@ "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-api-session-controller": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", - "@deepseek-ai/dsh-commands": "workspace:^", - "@deepseek-ai/dsh-host-directory-picker": "workspace:^", - "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-native-command": "workspace:^", - "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", "@deepseek-ai/dsh-session-query": "workspace:^", - "@deepseek-ai/dsh-settings": "workspace:^", - "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-util-crypto": "workspace:^", "@deepseek-ai/schemastery": "workspace:^", "fflate": "^0.8.2", @@ -67,14 +61,13 @@ }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^", - "@deepseek-ai/dsh-agent-presets": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:^", - "@deepseek-ai/dsh-agent-presets": "workspace:^", "@deepseek-ai/dsh-credentials": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/dsh-typert-registry": "workspace:^" } diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 30844d9bbb..29ebd6b732 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -4,19 +4,10 @@ */ import { homedir } from 'node:os' -import { dirname } from 'node:path' import type { Context } from '@deepseek-ai/cordis' import type { ModelSelection } from '@deepseek-ai/dsh-agent' -import type {} from '@deepseek-ai/dsh-agent-presets/types' -import type { SessionId } from '@deepseek-ai/dsh-session' -import { isUserInvocable } from '@deepseek-ai/dsh-skill' -import { - InvalidPresetIdError, PresetExistsError, - PresetNotWritableError, UnknownPresetError, -} from '@deepseek-ai/dsh-agent-presets' -import type { ApiProxy, ConfigurableProviderView } from './api/index.ts' -import { buildModelCatalog } from '@deepseek-ai/dsh-api-session-controller' -import { SessionQueryError } from '@deepseek-ai/dsh-session-query' +import { canOpenNativePath } from '@deepseek-ai/dsh-native-command' +import type { ApiProxy } from './api/index.ts' import { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, flushLiveSessionLog, @@ -27,77 +18,30 @@ import { type SessionLogCompressionLevel, } from './session-export.ts' import type { SessionRawArtifact } from '@deepseek-ai/dsh-session-persistence' -// Type-only edges: resolve the command-change stream and `ctx.get('skills')`. -import type {} from '@deepseek-ai/dsh-commands' -import type {} from '@deepseek-ai/dsh-skill' -import type { ScopeKey } from '@deepseek-ai/dsh-scope' -import type { RpcError, RpcRequest, RpcResponse } from './api/rpc.ts' -import { canOpenNativePath, openNativePath, openNativeTextFile } from './native-path-opener.ts' - -/** Read live abort state across awaits without treating it as synchronously immutable. */ -function isAborted(signal: AbortSignal): boolean { - return signal.aborted -} +import type { RpcRequest, RpcResponse } from './api/rpc.ts' /** Wrap an ok result echoing the request's rpcId. */ function ok(request: RpcRequest, value: T): RpcResponse { return { rpcId: request.rpcId, result: { ok: true, value } } } -/** Wrap an error result echoing the request's rpcId. */ -function err(request: RpcRequest, error: RpcError): RpcResponse { - return { rpcId: request.rpcId, result: { ok: false, error } } -} - /** Deployment metadata and Host integrations consumed by the API implementation. */ export interface ApiProxyDefaults { /** Current deployment model selection reported by `host.describe`. */ defaultModelSelection: () => ModelSelection /** Project hint reported by `host.describe`; must match Session Controller's default cwd. */ cwd: string - /** Native open-with-default-application; injectable for carrier tests. */ - openPath?: (path: string, signal: AbortSignal) => Promise - /** Native text-editor handoff; injectable for settings-document tests. */ - openTextFile?: (path: string, signal: AbortSignal) => Promise /** Validated DEFLATE level for session-log ZIP entries; defaults to 6. */ sessionExportCompressionLevel?: SessionLogCompressionLevel /** * Whether handing a path to the native opener can work at all — the * `hasDocument` capability the preset roster reports, and the switch * between opening a preset directory and answering its path as text. - * Absent, an injected `openPath` counts as openable and everything else - * falls back to platform detection ({@link canOpenNativePath}). + * Absent, platform detection decides ({@link canOpenNativePath}). */ canOpenPath?: () => boolean } -/** The roster is absent: this deployment composes no agent presets at all. */ -function noRoster(agentPreset: string): RpcError { - return { - code: 'agent-preset-not-found', - message: 'this deployment composes no agent presets', - details: { agentPreset, available: [] }, - } -} - -/** Map one authoring/roster failure onto its wire code. */ -function presetError(agentPreset: string, error: unknown): RpcError { - if (error instanceof UnknownPresetError) { - return { - code: 'agent-preset-not-found', - message: error.message, - details: { agentPreset: error.presetId, available: [...error.available] }, - } - } - if (error instanceof PresetNotWritableError) { - return { code: 'agent-preset-read-only', message: error.message, details: { agentPreset, reason: error.message } } - } - if (error instanceof InvalidPresetIdError || error instanceof PresetExistsError) { - return { code: 'agent-preset-invalid', message: error.message, details: { agentPreset, reason: error.message } } - } - return { code: 'internal', message: `agent preset "${agentPreset}": ${String(error)}`, details: {} } -} - /** * Implement ApiProxy over a composed host context. * @param ctx - a context with the Host spine mounted. @@ -107,75 +51,10 @@ function presetError(agentPreset: string, error: unknown): RpcError { export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiProxy { const sessionExportCompressionLevel = defaults.sessionExportCompressionLevel ?? DEFAULT_SESSION_LOG_COMPRESSION_LEVEL - /** Resolve a Session's live or standing preset scope without resuming it. */ - async function sessionScopeFor( - sessionId: SessionId, - agentPreset: string | undefined, - ): Promise { - const live = ctx.get('agents')?.get(sessionId) - if (live !== undefined) return live - const presets = ctx.get('agentPresets') - if (presets === undefined) return undefined - try { - return await presets.standingKeyFor(agentPreset) - } catch { - // An unknown or unusable recorded preset falls back to the global registry. - return undefined - } - } - - /** Missing-service report shared by the settings domain (skills-domain stance). */ - function settingsAbsent(): RpcError { - return { code: 'internal', message: 'settings service is absent: this deployment does not mount a settings provider (e.g. @deepseek-ai/dsh-settings-file) in its composition', details: {} } - } - - /** Open one Host-resolved target and map native failures onto the wire vocabulary. */ - async function openTarget( - request: RpcRequest, path: string, signal: AbortSignal, - open: (path: string, signal: AbortSignal) => Promise, - ): Promise> { - try { - await open(path, signal) - return ok(request, { opened: true as const }) - } catch (error: unknown) { - if (signal.aborted) { - return err(request, { - code: 'cancelled', - message: 'path open was aborted', - details: {}, - }) - } - return err(request, { - code: 'internal', - message: `path open failed: ${error instanceof Error ? error.message : String(error)}`, - details: {}, - }) - } - } - - /** Open one Host-resolved path with its default application. */ - function openPath( - request: RpcRequest, path: string, signal: AbortSignal, - ): Promise> { - const open = defaults.openPath - ?? ((target: string, openSignal: AbortSignal) => openNativePath(target, openSignal)) - return openTarget(request, path, signal, open) - } - - /** Open one Host-resolved text document in a native editor. */ - function openTextFile( - request: RpcRequest, path: string, signal: AbortSignal, - ): Promise> { - const open = defaults.openTextFile - ?? ((target: string, openSignal: AbortSignal) => openNativeTextFile(target, openSignal)) - return openTarget(request, path, signal, open) - } - /** Whether this deployment can hand a path to a native opener at all. */ function canOpenPaths(): boolean { if (defaults.canOpenPath !== undefined) return defaults.canOpenPath() - // An injected opener is by definition usable; otherwise ask the platform. - return defaults.openPath !== undefined || canOpenNativePath() + return canOpenNativePath() } return { @@ -198,210 +77,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro })) }, - async openPath(request, signal) { - return openPath(request, request.payload.path, signal) - }, - }, - - agentPresets: { - // Only the desktop opener remains here: the roster, selection, and - // authoring calls are the AgentPresets service's own Remote namespace. - async openDocument(request, signal) { - const { agentPreset } = request.payload - const presets = ctx.get('agentPresets') - if (presets === undefined) return err(request, noRoster(agentPreset)) - try { - const preset = await presets.resolve(agentPreset) - // Same line as copy/remove draw: the shipped install is not the - // user's to manage, and pointing an editor into it invites edits an - // upgrade will silently overwrite. - if (preset.trust !== 'user') { - throw new PresetNotWritableError(preset.id, 'it ships with the deployment') - } - // The id resolved against the Host's own roots is what selects the - // directory — no browser payload carries a path in either direction - // unless the deployment has no opener to hand it to. - const directory = dirname(preset.path) - if (!canOpenPaths()) return ok(request, { opened: false as const, path: directory }) - return await openPath(request, directory, signal) - } catch (error: unknown) { - return err(request, presetError(agentPreset, error)) - } - }, - }, - - skills: { - // Skill lookup never creates or resumes an agent: the session address - // resolves to a canonical cwd from the host-resident session header, and - // the view scope is the live agent or the preset's standing key. - async list(request) { - const { sessionId } = request.payload - let cwd: string | undefined - let agentPreset: string | undefined - try { - using observation = await ctx.sessionQuery.observeSession(sessionId) - if (observation.projections === undefined) { - throw new Error('skill catalog requires a projected Session observation') - } - cwd = observation.header.cwd - agentPreset = observation.projections.values.agentPreset ?? undefined - } catch (error: unknown) { - if (error instanceof SessionQueryError - && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') { - return err(request, { - code: 'session-not-found', - message: `session "${sessionId}" not found`, - details: { sessionId }, - }) - } - return err(request, { - code: 'internal', - message: `session "${sessionId}" could not be inspected: ${String(error)}`, - details: {}, - }) - } - if (cwd === undefined) { - // Every served session records its project at create time; a - // cwd-less header is a pre-project legacy log (not served). - return err(request, { code: 'internal', message: `session "${sessionId}" has no project cwd`, details: {} }) - } - // The host registry is layered per scope and serves every session. A - // composition may still realm-mount its own registry instead; that - // instance is invisible to host contexts, so address it through the - // live agent (`agents.get` keeps the no-side-effect stance above). - const live = ctx.agents.get(sessionId) - const presets = ctx.get('agentPresets') - const scoped = live === undefined ? undefined : presets?.serviceFor(live, 'skills') - // A missing service means no composition mounts dsh-skill, not an - // empty catalog. `ctx.get` also - // keeps this handler independent of the gateway plugin's inject list - // (an undeclared `ctx.skills` property read fails the reflect proxy). - const skillRegistry = scoped ?? ctx.get('skills') - if (skillRegistry === undefined) { - return err(request, { code: 'internal', message: 'skill registry is absent: neither this session\'s agent preset nor the host composition mounts @deepseek-ai/dsh-skill', details: {} }) - } - // Resolve the live or recorded preset scope so the catalog matches the - // Session composition without resuming its Agent. - const scope = await sessionScopeFor(sessionId, agentPreset) - try { - const skills = (await skillRegistry.list({ cwd, scope })).filter(isUserInvocable) - return ok(request, { - skills: skills.map(skill => ({ - name: skill.name, - description: skill.description, - ...skill.whenToUse === undefined ? {} : { whenToUse: skill.whenToUse }, - modelInvocable: skill.invocation.modelInvocable, - })), - }) - } catch (error: unknown) { - return err(request, { code: 'internal', message: `skill listing failed: ${String(error)}`, details: {} }) - } - }, - }, - - settings: { - async openDocument(request, signal) { - const settings = ctx.get('settings') - if (settings === undefined) return err(request, settingsAbsent()) - if (isAborted(signal)) { - return err(request, { - code: 'cancelled', - message: 'settings document open was aborted', - details: {}, - }) - } - let path: string | undefined - try { - path = await settings.prepareDocument() - } catch (error: unknown) { - if (isAborted(signal)) { - return err(request, { - code: 'cancelled', - message: 'settings document preparation was aborted', - details: {}, - }) - } - return err(request, { - code: 'internal', - message: `settings document preparation failed: ${error instanceof Error ? error.message : String(error)}`, - details: {}, - }) - } - if (path === undefined) { - return err(request, { - code: 'internal', - message: 'settings provider has no local document to open', - details: {}, - }) - } - if (isAborted(signal)) { - return err(request, { - code: 'cancelled', - message: 'settings document open was aborted', - details: {}, - }) - } - return openTextFile(request, path, signal) - }, - }, - - llm: { - providers(request) { - const registered = ctx.llm.listProviders() - const active = new Set(registered.map(provider => provider.id)) - const directory = ctx.llm.listConfigurableProviders() - const declared = new Set(directory.map(entry => entry.provider)) - const views: ConfigurableProviderView[] = 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 }, - })) - // Routes registered without a directory declaration still appear — - // they exist and serve models — just with no settings address. No - // adapter claimed them, so nothing can say whether they are shipped. - for (const provider of registered) { - if (declared.has(provider.id)) continue - views.push({ - provider: provider.id, - displayName: provider.name, - settingsNs: '', - settingsPath: [], - active: true, - }) - } - return Promise.resolve(ok(request, { providers: views })) - }, - - async models(request) { - return ok(request, await buildModelCatalog(ctx, defaults.defaultModelSelection())) - }, - - async discoverModels(request, signal) { - const { settingsNs, provider, baseURL, api, apiKey } = request.payload - try { - const models = await ctx.llm.discoverModels(settingsNs, { - ...provider === undefined ? {} : { provider }, - ...baseURL === undefined ? {} : { baseURL }, - ...api === undefined ? {} : { api }, - ...apiKey === undefined ? {} : { apiKey }, - ...signal === undefined ? {} : { signal }, - }) - return ok(request, { models }) - } catch (error: unknown) { - // Every failure here is the user's next move, not a transport fault: - // a wrong endpoint, a rejected key, or a protocol with no listing all - // end at the same place — fill the models in by hand. The details - // repeat only what the caller already sent, never the credential. - return err(request, { - code: 'model-discovery-failed', - message: error instanceof Error ? error.message : String(error), - details: { settingsNs, ...baseURL === undefined ? {} : { baseURL } }, - }) - } - }, }, downloads: { diff --git a/packages/host/apiproxy/src/api/agent-presets.schema.ts b/packages/host/apiproxy/src/api/agent-presets.schema.ts deleted file mode 100644 index ad15512ae0..0000000000 --- a/packages/host/apiproxy/src/api/agent-presets.schema.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * agent-presets domain zod schemas (names derived from map keys: - * agentPresetOpenDocumentRequestSchema / agentPresetOpenDocumentValueSchema). - */ - -import { z } from 'zod' -import type { RequestPayload, ResponseValue } from './rpc-map.ts' -import type { Wire } from './rpc.schema.ts' - -/** agentPreset.openDocument request payload. */ -export const agentPresetOpenDocumentRequestSchema = z.object({ - agentPreset: z.string().min(1), -}) satisfies z.ZodType>> - -/** agentPreset.openDocument response value. */ -export const agentPresetOpenDocumentValueSchema = z.union([ - z.object({ opened: z.literal(true) }), - z.object({ opened: z.literal(false), path: z.string() }), -]) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/agent-presets.ts b/packages/host/apiproxy/src/api/agent-presets.ts deleted file mode 100644 index a7749ab6aa..0000000000 --- a/packages/host/apiproxy/src/api/agent-presets.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * agent-presets domain contract: handing one preset's directory to the - * platform opener, which is the only agent-preset call still carried here. - * - * The roster and its authoring calls are the AgentPresets service's own Remote - * namespace. This one stays because the opener is a Host desktop integration - * rather than a preset operation. - */ - -import type { RpcRequest, RpcResponse } from './rpc.ts' - -/** agent-preset-domain unary methods (the map key agentPreset.* of RpcMethodMap). */ -export interface AgentPresetsApi { - /** - * Hand one locally authored preset's DIRECTORY to the platform opener, for - * editing the files, which are the only composition editor. The request - * carries an id, never a path — the Host resolves it — so no browser - * payload can select an arbitrary filesystem target. Where the deployment - * has no native opener (`canOpenPath: false` on `host.describe`), the reply - * carries the resolved directory for the surface to show as text instead. - * Shipped presets are refused: their install is not the user's to manage. - */ - openDocument(request: RpcRequest<{ agentPreset: string }>, signal: AbortSignal): - Promise> -} diff --git a/packages/host/apiproxy/src/api/host.schema.ts b/packages/host/apiproxy/src/api/host.schema.ts index 6735e55639..5429b8cab0 100644 --- a/packages/host/apiproxy/src/api/host.schema.ts +++ b/packages/host/apiproxy/src/api/host.schema.ts @@ -19,13 +19,3 @@ export const hostDescribeValueSchema = z.object({ home: z.string(), canOpenPath: z.boolean(), }) satisfies z.ZodType>> - -/** host.openPath request payload. */ -export const hostOpenPathRequestSchema = z.object({ - path: z.string().min(1), -}) satisfies z.ZodType>> - -/** host.openPath response value. */ -export const hostOpenPathValueSchema = z.object({ - opened: z.literal(true), -}) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/host.ts b/packages/host/apiproxy/src/api/host.ts index ceb7998d2b..b256afbc00 100644 --- a/packages/host/apiproxy/src/api/host.ts +++ b/packages/host/apiproxy/src/api/host.ts @@ -27,14 +27,4 @@ export interface HostApi { canOpenPath: boolean }>> - /** - * Open a filesystem path with the operating system's default application - * (Finder / Explorer / xdg-open hand-off). The browser carrier's - * prefix-wide trust and authentication checks cover this method like every - * other `/api` request. - */ - openPath( - request: RpcRequest<{ path: string }>, - signal: AbortSignal, - ): Promise> } diff --git a/packages/host/apiproxy/src/api/index.ts b/packages/host/apiproxy/src/api/index.ts index 8e33371141..7f6e5e8018 100644 --- a/packages/host/apiproxy/src/api/index.ts +++ b/packages/host/apiproxy/src/api/index.ts @@ -5,19 +5,11 @@ */ import type { HostApi } from './host.ts' -import type { AgentPresetsApi } from './agent-presets.ts' -import type { SkillsApi } from './skills.ts' -import type { SettingsApi } from './settings.ts' -import type { LlmApi } from './llm.ts' import type { DownloadsApi } from './downloads.ts' /** Root interface of the unified API. New client-request domain = one new file pair + one field here + one map row. */ export interface ApiProxy { host: HostApi - skills: SkillsApi - agentPresets: AgentPresetsApi - settings: SettingsApi - llm: LlmApi /** Host-only download surfaces (GET, no wire envelope); absent from IApiClient. */ downloads: DownloadsApi } @@ -28,10 +20,6 @@ export type { ModelReasoningEffort, ModelSelection, } from '@deepseek-ai/dsh-api-session-controller/types' export type { HostApi } from './host.ts' -export type { SkillsApi, SkillEntry } from './skills.ts' -export type { AgentPresetsApi } from './agent-presets.ts' -export type { SettingsApi } from './settings.ts' -export type { ConfigurableProviderView, DiscoveredModelView, LlmApi } from './llm.ts' export type { DownloadsApi } from './downloads.ts' // ---- Message layer: narrow forms (domain-signature view) ---- diff --git a/packages/host/apiproxy/src/api/llm.schema.ts b/packages/host/apiproxy/src/api/llm.schema.ts deleted file mode 100644 index 2c1619c8df..0000000000 --- a/packages/host/apiproxy/src/api/llm.schema.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * llm domain zod schemas (names derived from map keys: llmProvidersRequestSchema / - * llmProvidersValueSchema / llmModelsRequestSchema / llmModelsValueSchema). - */ - -import { z } from 'zod' -import type { RequestPayload, ResponseValue } from './rpc-map.ts' -import type { Wire } from './rpc.schema.ts' -import type { ConfigurableProviderView, DiscoveredModelView } from './llm.ts' -import type { - ModelCatalogFailure, - ModelCatalogModel, - ModelSelection, - ModelProviderGroup, - ModelReasoning, - ModelReasoningEffort, -} from '@deepseek-ai/dsh-api-session-controller/types' - -/** One adapter-owned reasoning effort. */ -const modelReasoningEffortSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1), - description: z.string().optional(), -}) satisfies z.ZodType> - -/** Exact-model reasoning metadata. */ -const modelReasoningSchema = z.object({ - efforts: z.array(modelReasoningEffortSchema).min(1), - defaultEffort: z.string().min(1).optional(), -}) satisfies z.ZodType> - -/** One advisory model entry inside a provider group. */ -const modelCatalogModelSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1), - description: z.string().optional(), - reasoning: modelReasoningSchema.optional(), -}) satisfies z.ZodType> - -/** One successfully loaded provider group. */ -const modelProviderGroupSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1), - models: z.array(modelCatalogModelSchema), -}) satisfies z.ZodType> - -/** One provider-local catalog failure. */ -const modelCatalogFailureSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1), - message: z.string(), -}) satisfies z.ZodType> - -/** Complete model selection used as the Host default. */ -const modelSelectionSchema = z.object({ - provider: z.string().min(1), - model: z.string().min(1), - reasoningEffort: z.string().min(1).optional(), -}) satisfies z.ZodType> - -/** ConfigurableProviderView row of llm.providers. */ -export const configurableProviderViewSchema = z.object({ - provider: z.string().min(1), - displayName: z.string().min(1), - settingsNs: z.string(), - settingsPath: z.array(z.string()), - active: z.boolean(), - declared: z.boolean().optional(), -}) satisfies z.ZodType> - -/** llm.providers request payload. */ -export const llmProvidersRequestSchema = z.object({}) satisfies z.ZodType>> - -/** llm.providers response value. */ -export const llmProvidersValueSchema = z.object({ - providers: z.array(configurableProviderViewSchema), -}) satisfies z.ZodType>> - -/** llm.models request payload. */ -export const llmModelsRequestSchema = z.object({}) satisfies z.ZodType>> - -/** llm.models response value. */ -export const llmModelsValueSchema = z.object({ - default: modelSelectionSchema, - routableProviders: z.array(z.string().min(1)), - groups: z.array(modelProviderGroupSchema), - failures: z.array(modelCatalogFailureSchema), -}) satisfies z.ZodType>> - -/** DiscoveredModelView row of llm.discoverModels. */ -export const discoveredModelViewSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1).optional(), - contextWindow: z.number().int().positive().optional(), - maxTokens: z.number().int().positive().optional(), -}) satisfies z.ZodType> - -/** llm.discoverModels request payload. */ -export const llmDiscoverModelsRequestSchema = z.object({ - settingsNs: z.string().min(1), - provider: z.string().min(1).optional(), - baseURL: z.string().min(1).optional(), - api: z.string().min(1).optional(), - // Write-only at the host: used for this one interrogation, never stored and - // never returned. It does ride the client's outgoing envelope like every - // other secret-bearing payload (`settings/update`), which - // `subscribeEnvelopes()` observers can see — redacting that tap is a - // configuration-plane-wide change, not this method's to make alone. - apiKey: z.string().min(1).optional(), -}) satisfies z.ZodType>> - -/** llm.discoverModels response value. */ -export const llmDiscoverModelsValueSchema = z.object({ - models: z.array(discoveredModelViewSchema), -}) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/llm.ts b/packages/host/apiproxy/src/api/llm.ts deleted file mode 100644 index 96b5db874e..0000000000 --- a/packages/host/apiproxy/src/api/llm.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * llm domain contract: host-scoped provider topology for configuration - * surfaces. `llm.providers` merges the configurable-provider directory - * (which providers CAN be configured, and where their settings live) with the - * live route registry; `llm.models` is the session-independent model catalog. - * Clients invalidate from the forwarded `llm/adapters-updated` and - * `settings/document-updated` owner events. - */ - -import type { RpcRequest, RpcResponse } from './rpc.ts' -import type { - ModelCatalog, -} from '@deepseek-ai/dsh-api-session-controller/types' - -/** Wire view of one configurable provider. */ -export interface ConfigurableProviderView { - /** Provider route key (`deepseek-official`, `openai`, …). */ - provider: string - /** Human-readable name for configuration surfaces. */ - displayName: string - /** Settings namespace whose section configures this provider. */ - settingsNs: string - /** Path from that section's root to the provider's profile object (empty = whole section). */ - settingsPath: string[] - /** Whether the route is currently registered (its models are requestable). */ - active: boolean - /** - * Whether the owning adapter knows this route only because configuration - * declared it. Absent when the adapter draws no such distinction, so a - * surface must treat absence as "unknown", not as "shipped". - */ - declared?: boolean -} - -/** Llm-domain unary methods (the map keys llm.* of RpcMethodMap). */ -export interface LlmApi { - /** - * List every configurable provider with its live/dormant state, in - * directory declaration order. Routes registered outside the directory - * (an adapter that never declared configurability) are appended with their - * registration identity and no settings address. - */ - providers(request: RpcRequest<{}>): Promise> - - /** - * Host-scoped model catalog over every registered provider route: the - * settings surface's models view, needing no session. Per-provider listing - * failures ride `failures` without failing the sound groups. - */ - models(request: RpcRequest<{}>): Promise> - - /** - * Interrogate a provider endpoint the configuration surface is still - * drafting, and return the models it advertises for the user to adopt. - * - * The payload is the draft, not a stored route: `settingsNs` selects the - * adapter family that answers, and the rest comes from the form. `provider` - * names the route being edited when there is one — an adapter that already - * describes that route answers from its own registry, with better metadata - * and no network call, and needs no endpoint. A route it does not describe is - * asked over the wire, which is what `baseURL`, `api`, and `apiKey` are for. - * - * Nothing is written — the reply is candidates, and only a later - * `settings.mutate` decides what a route serves. `apiKey` is accepted here - * but never stored or returned; a provider whose key is already stored omits - * it and the endpoint answers unauthenticated or refuses. - */ - discoverModels( - request: RpcRequest<{ - settingsNs: string - provider?: string - baseURL?: string - api?: string - apiKey?: string - }>, - signal?: AbortSignal, - ): Promise> -} - -/** Wire view of one model an interrogated endpoint advertises. */ -export interface DiscoveredModelView { - /** Model id the endpoint accepts. */ - id: string - /** Human-readable name when the endpoint supplies one. */ - name?: string - /** Maximum combined request and response context, when disclosed. */ - contextWindow?: number - /** Maximum output tokens, when disclosed. */ - maxTokens?: number -} diff --git a/packages/host/apiproxy/src/api/rpc-map.ts b/packages/host/apiproxy/src/api/rpc-map.ts index 99c8c7e580..e3e67a9c16 100644 --- a/packages/host/apiproxy/src/api/rpc-map.ts +++ b/packages/host/apiproxy/src/api/rpc-map.ts @@ -4,10 +4,6 @@ */ import type { HostApi } from './host.ts' -import type { AgentPresetsApi } from './agent-presets.ts' -import type { SkillsApi } from './skills.ts' -import type { SettingsApi } from './settings.ts' -import type { LlmApi } from './llm.ts' import type { RpcResponse } from './rpc.ts' /** @@ -17,13 +13,6 @@ import type { RpcResponse } from './rpc.ts' */ export interface RpcMethodMap { 'host.describe': HostApi['describe'] - 'host.openPath': HostApi['openPath'] - 'skill.list': SkillsApi['list'] - 'agentPreset.openDocument': AgentPresetsApi['openDocument'] - 'settings.openDocument': SettingsApi['openDocument'] - 'llm.providers': LlmApi['providers'] - 'llm.models': LlmApi['models'] - 'llm.discoverModels': LlmApi['discoverModels'] } /** Business request payload of method K (reaches through the RpcRequest narrow form to payload). */ diff --git a/packages/host/apiproxy/src/api/rpc.schema.ts b/packages/host/apiproxy/src/api/rpc.schema.ts index 434bbb24dc..c8a322fced 100644 --- a/packages/host/apiproxy/src/api/rpc.schema.ts +++ b/packages/host/apiproxy/src/api/rpc.schema.ts @@ -41,7 +41,6 @@ export const rpcErrorSchema: z.ZodType = z.discriminatedUnion('code', z.object({ code: z.literal('agent-preset-not-found'), message: z.string(), details: z.object({ agentPreset: z.string(), available: z.array(z.string()) }) }), z.object({ code: z.literal('agent-preset-invalid'), message: z.string(), details: z.object({ agentPreset: z.string(), reason: z.string() }) }), z.object({ code: z.literal('agent-busy'), message: z.string(), details: z.object({ reason: z.string() }) }), - z.object({ code: z.literal('model-discovery-failed'), message: z.string(), details: z.object({ settingsNs: z.string(), baseURL: z.string().optional() }) }), z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }), ]) as unknown as z.ZodType diff --git a/packages/host/apiproxy/src/api/rpc.ts b/packages/host/apiproxy/src/api/rpc.ts index 0981944824..d3d8643301 100644 --- a/packages/host/apiproxy/src/api/rpc.ts +++ b/packages/host/apiproxy/src/api/rpc.ts @@ -36,15 +36,6 @@ export interface RpcErrorDetailsMap { 'agent-preset-not-found': { agentPreset: string; available: readonly string[] } 'agent-preset-invalid': { agentPreset: string; reason: string } 'agent-busy': { reason: string } - /** - * Interrogating a draft provider endpoint did not produce a model listing: - * no adapter family serves the namespace, the protocol has no listing this - * build can read, or the endpoint was unreachable, refused the credential, - * or answered with something else. The message is the adapter's own text — - * it is what the form shows before falling back to hand-entry — and the - * details name the endpoint asked, never the credential offered. - */ - 'model-discovery-failed': { settingsNs: string; baseURL?: string } 'internal': {} } diff --git a/packages/host/apiproxy/src/api/settings.schema.ts b/packages/host/apiproxy/src/api/settings.schema.ts deleted file mode 100644 index fcfcb75cea..0000000000 --- a/packages/host/apiproxy/src/api/settings.schema.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * settings domain zod schemas (names derived from map keys: - * settingsOpenDocumentRequestSchema / settingsOpenDocumentValueSchema). - */ - -import { z } from 'zod' -import type { RequestPayload, ResponseValue } from './rpc-map.ts' -import type { Wire } from './rpc.schema.ts' - -/** settings.openDocument request payload. */ -export const settingsOpenDocumentRequestSchema = z.object({}) satisfies z.ZodType>> - -/** settings.openDocument response value. */ -export const settingsOpenDocumentValueSchema = z.object({ - opened: z.literal(true), -}) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/settings.ts b/packages/host/apiproxy/src/api/settings.ts deleted file mode 100644 index 07fc0e72be..0000000000 --- a/packages/host/apiproxy/src/api/settings.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * settings domain contract: what remains of the web face of the user-settings - * seam (`ctx.settings`) once the redacted read and the path-addressed write - * moved to the `settings` Remote namespace. Only the local-document handoff - * stays here, because opening a Host file is a platform action rather than a - * settings read. - */ - -import type { RpcRequest, RpcResponse } from './rpc.ts' - -/** Settings-domain unary methods (the map keys settings.* of RpcMethodMap). */ -export interface SettingsApi { - /** - * Materialize the configured local document when absent and ask the Host to - * hand it to the platform text-document opener. macOS forces a text editor; - * Linux and Windows use the desktop file association. The request carries - * no path, so the browser cannot choose an arbitrary Host filesystem target. - */ - openDocument( - request: RpcRequest<{}>, signal: AbortSignal, - ): Promise> -} diff --git a/packages/host/apiproxy/src/api/skills.schema.ts b/packages/host/apiproxy/src/api/skills.schema.ts deleted file mode 100644 index a0a54b6e9f..0000000000 --- a/packages/host/apiproxy/src/api/skills.schema.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * skills domain zod schemas (names derived from map keys: skillListRequestSchema / - * skillListValueSchema). - */ - -import { z } from 'zod' -import type { RequestPayload, ResponseValue } from './rpc-map.ts' -import type { Wire } from './rpc.schema.ts' -import { sessionIdSchema } from './ids.schema.ts' -import type { SkillEntry } from './skills.ts' - -/** SkillEntry row of skill.list. */ -export const skillEntrySchema = z.object({ - name: z.string().min(1), - description: z.string(), - whenToUse: z.string().optional(), - modelInvocable: z.boolean(), -}) satisfies z.ZodType> - -/** skill.list request payload. */ -export const skillListRequestSchema = z.object({ - sessionId: sessionIdSchema, -}) satisfies z.ZodType>> - -/** skill.list response value. */ -export const skillListValueSchema = z.object({ - skills: z.array(skillEntrySchema), -}) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/skills.ts b/packages/host/apiproxy/src/api/skills.ts deleted file mode 100644 index 61744f05c9..0000000000 --- a/packages/host/apiproxy/src/api/skills.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * skills domain contract: read-only skill catalog lookup addressed by session. - * The session's header cwd resolves to the canonical project root host-side — - * the client never submits a raw path, and skill lookup never creates or - * resumes an Agent. - */ - -import type { SessionId } from '@deepseek-ai/dsh-session/types' -import type { RpcRequest, RpcResponse } from './rpc.ts' - -/** Skill catalog row (wire projection of the host SkillSummary; provider/source vocabulary stays host-side). */ -export interface SkillEntry { - /** Kebab-case identifier the user references as `/name` in the composer. */ - readonly name: string - /** Short routing description. */ - readonly description: string - /** Optional extra routing guidance. */ - readonly whenToUse?: string - /** False marks a user-only skill (`disable-model-invocation`): invocable here, absent from the model catalog. */ - readonly modelInvocable: boolean -} - -/** - * Skill-domain unary methods (the map key skill.* of RpcMethodMap). Listing - * is the domain's only RPC: invocation uses Session Controller's ordinary - * prompt Remote. The host recognizes its leading `/name` token at the pre-step - * boundary (`dsh-tool-skill` injects the rendered body there), so every client - * shares one deterministic path with no dedicated invocation method. - */ -export interface SkillsApi { - /** Lists the user-invocable skill catalog for the session's project. */ - list(request: RpcRequest<{ sessionId: SessionId }>): Promise> -} diff --git a/packages/host/apiproxy/src/fetch/client.ts b/packages/host/apiproxy/src/fetch/client.ts index c33313ece2..4c49240d24 100644 --- a/packages/host/apiproxy/src/fetch/client.ts +++ b/packages/host/apiproxy/src/fetch/client.ts @@ -12,17 +12,7 @@ import type { ClientRequest, RpcMessage, RpcResponse } from '../api/rpc.ts' import { RpcId } from '../api/rpc.ts' import type { Wire } from '../api/rpc.schema.ts' import { serverResponseSchema } from '../api/rpc.schema.ts' -import { - hostDescribeValueSchema, hostOpenPathValueSchema, -} from '../api/host.schema.ts' -import { skillListValueSchema } from '../api/skills.schema.ts' -import { - agentPresetOpenDocumentValueSchema, -} from '../api/agent-presets.schema.ts' -import { - settingsOpenDocumentValueSchema, -} from '../api/settings.schema.ts' -import { llmDiscoverModelsValueSchema, llmModelsValueSchema, llmProvidersValueSchema } from '../api/llm.schema.ts' +import { hostDescribeValueSchema } from '../api/host.schema.ts' /** * Client consumption face of the contract (shape a): same domain tree as ApiProxy, but unary @@ -39,21 +29,6 @@ import { llmDiscoverModelsValueSchema, llmModelsValueSchema, llmProvidersValueSc export interface IApiClient { host: { describe(payload: RequestPayload<'host.describe'>, signal?: AbortSignal): Promise>> - openPath(payload: RequestPayload<'host.openPath'>, signal?: AbortSignal): Promise>> - } - skills: { - list(payload: RequestPayload<'skill.list'>, signal?: AbortSignal): Promise>> - } - agentPresets: { - openDocument(payload: RequestPayload<'agentPreset.openDocument'>, signal?: AbortSignal): Promise>> - } - settings: { - openDocument(payload: RequestPayload<'settings.openDocument'>, signal?: AbortSignal): Promise>> - } - llm: { - providers(payload: RequestPayload<'llm.providers'>, signal?: AbortSignal): Promise>> - models(payload: RequestPayload<'llm.models'>, signal?: AbortSignal): Promise>> - discoverModels(payload: RequestPayload<'llm.discoverModels'>, signal?: AbortSignal): Promise>> } } @@ -63,13 +38,6 @@ export interface IApiClient { */ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType>> } = { 'host.describe': hostDescribeValueSchema, - 'host.openPath': hostOpenPathValueSchema, - 'skill.list': skillListValueSchema, - 'agentPreset.openDocument': agentPresetOpenDocumentValueSchema, - 'settings.openDocument': settingsOpenDocumentValueSchema, - 'llm.providers': llmProvidersValueSchema, - 'llm.models': llmModelsValueSchema, - 'llm.discoverModels': llmDiscoverModelsValueSchema, } /** Default timeout for bounded unary calls (rpc-compare 2026-07-19: a hung host must not leave callers pending forever). */ @@ -195,30 +163,6 @@ export abstract class AbstractApiClient implements IApiClient { readonly host: IApiClient['host'] = { describe: (payload, signal) => this.callUnary('host.describe', payload, signal), - openPath: (payload, signal) => this.callUnary('host.openPath', payload, signal), - } - - readonly skills: IApiClient['skills'] = { - list: (payload, signal) => this.callUnary('skill.list', payload, signal), - } - - // Annotated like every sibling, and load-bearing rather than cosmetic: - // inferring this member inlines `AgentPresetEntry` into the emitted - // declaration by the specifier TS picks — the host `index.ts` — which drags - // the whole gateway, and with it the host `Context` merges, into every - // Client program that imports this carrier. - readonly agentPresets: IApiClient['agentPresets'] = { - openDocument: (payload, signal) => this.callUnary('agentPreset.openDocument', payload, signal), - } - - readonly settings: IApiClient['settings'] = { - openDocument: (payload, signal) => this.callUnary('settings.openDocument', payload, signal), - } - - readonly llm: IApiClient['llm'] = { - providers: (payload, signal) => this.callUnary('llm.providers', payload, signal), - models: (payload, signal) => this.callUnary('llm.models', payload, signal), - discoverModels: (payload, signal) => this.callUnary('llm.discoverModels', payload, signal), } } diff --git a/packages/host/apiproxy/src/fetch/handler.ts b/packages/host/apiproxy/src/fetch/handler.ts index d5fb1d0e29..82142ef923 100644 --- a/packages/host/apiproxy/src/fetch/handler.ts +++ b/packages/host/apiproxy/src/fetch/handler.ts @@ -14,17 +14,7 @@ import type { ClientRequest, RpcError, RpcRequest, RpcResponse, ServerResponse } import { RpcId } from '../api/rpc.ts' import type { Wire } from '../api/rpc.schema.ts' import { clientRequestSchema } from '../api/rpc.schema.ts' -import { - hostDescribeRequestSchema, hostOpenPathRequestSchema, -} from '../api/host.schema.ts' -import { skillListRequestSchema } from '../api/skills.schema.ts' -import { - agentPresetOpenDocumentRequestSchema, -} from '../api/agent-presets.schema.ts' -import { - settingsOpenDocumentRequestSchema, -} from '../api/settings.schema.ts' -import { llmDiscoverModelsRequestSchema, llmModelsRequestSchema, llmProvidersRequestSchema } from '../api/llm.schema.ts' +import { hostDescribeRequestSchema } from '../api/host.schema.ts' /** * Unary dispatch table, keyed by (and compiler-locked to) RpcMethodMap: a map row without a @@ -44,13 +34,6 @@ type UnaryRoutes = { const UNARY_ROUTES: UnaryRoutes = { 'host.describe': { schema: hostDescribeRequestSchema, invoke: (api, r) => api.host.describe(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) }, - 'settings.openDocument': { schema: settingsOpenDocumentRequestSchema, invoke: (api, r, signal) => api.settings.openDocument(r, signal) }, - 'llm.providers': { schema: llmProvidersRequestSchema, invoke: (api, r) => api.llm.providers(r) }, - 'llm.models': { schema: llmModelsRequestSchema, invoke: (api, r) => api.llm.models(r) }, - 'llm.discoverModels': { schema: llmDiscoverModelsRequestSchema, invoke: (api, r, signal) => api.llm.discoverModels(r, signal) }, } /** Route lookup that narrows an arbitrary path segment to a map key (single cast point for the string→key refinement). */ diff --git a/packages/host/apiproxy/src/index.ts b/packages/host/apiproxy/src/index.ts index e8816f99ce..474ad6beef 100644 --- a/packages/host/apiproxy/src/index.ts +++ b/packages/host/apiproxy/src/index.ts @@ -14,8 +14,6 @@ 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 { @@ -63,8 +61,7 @@ export interface Config { */ export class ApiProxyService extends Service implements ApiProxy { static inject = [ - 'agentDefaultModel', 'agents', 'attachments', 'directoryPicker', 'llm', 'sessions', 'sessionQuery', - 'sessionController', + 'agentDefaultModel', 'agents', 'attachments', 'sessions', 'sessionQuery', ] static Config: z = z.object({ @@ -74,10 +71,6 @@ export class ApiProxyService extends Service implements ApiProxy { }) readonly host: ApiProxy['host'] - readonly skills: ApiProxy['skills'] - readonly agentPresets: ApiProxy['agentPresets'] - readonly settings: ApiProxy['settings'] - readonly llm: ApiProxy['llm'] readonly downloads: ApiProxy['downloads'] constructor(ctx: Context, config: Config) { @@ -91,10 +84,6 @@ export class ApiProxyService extends Service implements ApiProxy { : { sessionExportCompressionLevel: config.sessionExportCompressionLevel }), }) this.host = api.host - this.skills = api.skills - this.agentPresets = api.agentPresets - this.settings = api.settings - this.llm = api.llm this.downloads = api.downloads } } diff --git a/packages/host/apiproxy/src/native-path-opener.ts b/packages/host/apiproxy/src/native-path-opener.ts deleted file mode 100644 index f8a065c8e2..0000000000 --- a/packages/host/apiproxy/src/native-path-opener.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Cross-platform native path and text-document openers used by the local GUI - * carrier. - * - * The default intent prefers the default browser for documents it renders when - * the platform can name one, then falls back to the default application. WSL - * translates every path for the Windows desktop instead of assuming a Linux - * GUI. The text-editor intent never consults the browser. - */ - -import { release as osRelease } from 'node:os' -import { extname } from 'node:path' -import { runNativeCommand, type NativeCommandRunner } from '@deepseek-ai/dsh-native-command' - -/** Testable command boundary; native implementations never invoke a shell. */ -export type PathOpenerRunner = NativeCommandRunner - -/** Injectable platform facts for deterministic adapter tests. */ -export interface PathOpenerInternals { - platform?: NodeJS.Platform - /** Kernel release override used to distinguish WSL from desktop Linux. */ - osRelease?: string - /** Environment used for WSL markers and the desktop Linux browser convention. */ - env?: NodeJS.ProcessEnv - run?: PathOpenerRunner -} - -/** Documents a browser renders, as opposed to ones an editor merely edits. */ -const BROWSER_DOCUMENTS = new Set(['.html', '.htm', '.xhtml', '.svg']) - -/** - * The macOS bundle registered for `https` — the default browser, as - * LaunchServices records it. The nested version dict is stripped first - * because it carries its own `LSHandlerRoleAll`. - */ -function macBundleForHttps(plist: string): string | undefined { - const stripped = plist.replace(/LSHandlerPreferredVersions\s*=\s*\{[^}]*\};/g, '') - const block = /\{[^{}]*LSHandlerURLScheme\s*=\s*"?https"?;[^{}]*\}/.exec(stripped)?.[0] - if (block === undefined) return undefined - return /LSHandlerRoleAll\s*=\s*"?([\w.-]+)"?;/.exec(block)?.[1] -} - -/** - * Open one browser-renderable document with the default browser. - * @returns true when a browser took it; false when this platform cannot name - * one, or naming it failed — the caller then uses the default application. - */ -async function openInBrowser( - path: string, signal: AbortSignal, platform: NodeJS.Platform, - run: PathOpenerRunner, env: NodeJS.ProcessEnv, -): Promise { - if (platform === 'darwin') { - let bundle: string | undefined - try { - const { stdout } = await run( - 'defaults', ['read', 'com.apple.LaunchServices/com.apple.launchservices.secure'], signal) - bundle = macBundleForHttps(stdout) - } catch { - // No LaunchServices record (a fresh account never changed a default): - // the content-type handler is then the system's own choice anyway. - return false - } - if (bundle === undefined) return false - await run('open', ['-b', bundle, path], signal) - return true - } - if (platform === 'linux') { - // $BROWSER is the portable convention; desktop-entry resolution through - // xdg-settings needs a launcher this package has no business shipping. - const browser = env.BROWSER - if (browser === undefined || browser === '') return false - await run(browser, [path], signal) - return true - } - // Windows names no browser without reading the UserChoice registry, and its - // .html association is the browser in the ordinary case. - return false -} - -/** Native path-open intent; macOS distinguishes text editing from file association. */ -type PathOpenIntent = 'default' | 'text-editor' - -/** PowerShell single-quoted literal (doubles embedded quotes). */ -function powershellLiteral(path: string): string { - return `'${path.replace(/'/g, "''")}'` -} - -/** Whether one environment marker is set to a non-empty value. */ -function present(value: string | undefined): boolean { - return value !== undefined && value !== '' -} - -/** Distinguish WSL from desktop Linux using its process and kernel markers. */ -function isWsl(internals: PathOpenerInternals): boolean { - const env = internals.env ?? process.env - if (present(env.WSL_DISTRO_NAME) || present(env.WSL_INTEROP)) return true - return (internals.osRelease ?? osRelease()).toLowerCase().includes('microsoft') -} - -/** Open one Windows-resolvable path through its registered desktop application. */ -async function openWindowsPath(path: string, signal: AbortSignal, run: PathOpenerRunner): Promise { - await run('powershell.exe', [ - '-NoProfile', - '-Command', - `Invoke-Item -LiteralPath ${powershellLiteral(path)}`, - ], signal) -} - -/** Translate a WSL path before handing it to the Windows desktop. */ -async function openWslPath(path: string, signal: AbortSignal, run: PathOpenerRunner): Promise { - const translated = await run('wslpath', ['-w', path], signal) - signal.throwIfAborted() - const windowsPath = translated.stdout.replace(/[\r\n]+$/, '') - if (windowsPath === '') throw new Error('wslpath returned no Windows path') - await openWindowsPath(windowsPath, signal, run) -} - -/** Dispatch one shell-free platform command for the requested open intent. */ -async function openNativePathWithIntent( - path: string, - signal: AbortSignal, - intent: PathOpenIntent, - internals: PathOpenerInternals = {}, -): Promise { - const platform = internals.platform ?? process.platform - const run = internals.run ?? runNativeCommand - const env = internals.env ?? process.env - const wsl = platform === 'linux' && isWsl(internals) - - if (!wsl && intent === 'default' && BROWSER_DOCUMENTS.has(extname(path).toLowerCase()) - && await openInBrowser(path, signal, platform, run, env)) return - - if (platform === 'darwin') { - await run('open', intent === 'text-editor' ? ['-t', path] : [path], signal) - return - } - - if (platform === 'win32') { - await openWindowsPath(path, signal, run) - return - } - - if (platform === 'linux') { - if (wsl) { - await openWslPath(path, signal, run) - return - } - await run('xdg-open', [path], signal) - return - } - - throw new Error(`native path opener is unsupported on ${platform}`) -} - -/** - * Whether {@link openNativePath} plausibly reaches a desktop on this host. - * - * macOS and Windows always carry a desktop opener; Linux does when it is WSL - * (the Windows desktop takes the path) or a display server is announced. - * A headless or containerised Linux host answers false, which is what lets a - * surface show a path as text instead of offering a button that would spawn - * `xdg-open` into nothing. - * @param internals - platform and environment seam for deterministic tests. - * @returns true when handing a path to the native opener can work at all. - */ -export function canOpenNativePath(internals: PathOpenerInternals = {}): boolean { - const platform = internals.platform ?? process.platform - if (platform === 'darwin' || platform === 'win32') return true - if (platform !== 'linux') return false - const env = internals.env ?? process.env - return isWsl(internals) || present(env.DISPLAY) || present(env.WAYLAND_DISPLAY) -} - -/** - * Open a filesystem path with the operating system's default application, or - * with the default browser when the path names a document a browser renders. - * @param path - absolute or host-resolvable path (caller owns resolution). - * @param signal - caller/connection lifetime; abort terminates the native command. - * @param internals - Platform, environment, and runner hooks for deterministic tests. - */ -export function openNativePath( - path: string, - signal: AbortSignal, - internals: PathOpenerInternals = {}, -): Promise { - return openNativePathWithIntent(path, signal, 'default', internals) -} - -/** - * Open a text document for editing; macOS bypasses the file-type association - * so a YAML association with a browser cannot consume the gesture. - * @param path - absolute or host-resolvable text-document path. - * @param signal - caller/connection lifetime; abort terminates the native command. - * @param internals - Platform and runner hooks for deterministic tests. - */ -export function openNativeTextFile( - path: string, - signal: AbortSignal, - internals: PathOpenerInternals = {}, -): Promise { - return openNativePathWithIntent(path, signal, 'text-editor', internals) -} diff --git a/packages/host/apiproxy/tsconfig.json b/packages/host/apiproxy/tsconfig.json index 1e1d3e37df..048fffcc72 100644 --- a/packages/host/apiproxy/tsconfig.json +++ b/packages/host/apiproxy/tsconfig.json @@ -8,9 +8,6 @@ "src" ], "references": [ - { - "path": "../../settings/settings" - }, { "path": "../../credentials/credentials" }, @@ -29,39 +26,21 @@ { "path": "../../attachment/attachment" }, - { - "path": "../../llm/llm" - }, { "path": "../../core/agent" }, { "path": "../../core/agent-default-model" }, - { - "path": "../../preset/agent-presets" - }, { "path": "../../core/session" }, - { - "path": "../../core/scope" - }, { "path": "../../session/session-persistence" }, { "path": "../../session-query/session-query" }, - { - "path": "../../skill/skill" - }, - { - "path": "../../interaction/commands" - }, - { - "path": "../directory-picker" - }, { "path": "../../runtime-diagnostics/invariants" }, From 160706be6092b83a70673eba20b63b1fd6778cdc Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:56:10 +0800 Subject: [PATCH 04/13] test(api): refresh Remote migration artifacts --- ...ession-scope-and-provide-channel.i18n.yaml | 4 +- ...lient-session-scope-and-provide-channel.md | 2 +- ...nt-session-scope-and-provide-channel.zh.md | 2 +- ...eb-command-surfaces-and-assembly.i18n.yaml | 4 +- ...07-25-web-command-surfaces-and-assembly.md | 2 +- ...25-web-command-surfaces-and-assembly.zh.md | 2 +- .../2026-07-30-web-config-plane.i18n.yaml | 4 +- .../2026-07-30-web-config-plane.md | 6 +- .../2026-07-30-web-config-plane.zh.md | 6 +- ...-unary-apiproxy-remote-migration.i18n.yaml | 6 + ...6-08-10-unary-apiproxy-remote-migration.md | 59 +++ ...8-10-unary-apiproxy-remote-migration.zh.md | 59 +++ ...6-08-17-settings-describe-mirror.i18n.yaml | 4 +- .../2026-08-17-settings-describe-mirror.md | 2 +- .../2026-08-17-settings-describe-mirror.zh.md | 2 +- ...sion-history-and-event-transport.i18n.yaml | 4 +- ...-18-session-history-and-event-transport.md | 4 +- ...-session-history-and-event-transport.zh.md | 4 +- ...nd-projection-owned-client-state.i18n.yaml | 4 +- ...tions-and-projection-owned-client-state.md | 2 +- ...ns-and-projection-owned-client-state.zh.md | 2 +- ...-onboarding-reads-every-provider.i18n.yaml | 4 +- ...6-08-12-onboarding-reads-every-provider.md | 2 +- ...8-12-onboarding-reads-every-provider.zh.md | 2 +- ...08-18-tool-row-file-open-failure.i18n.yaml | 4 +- .../2026-08-18-tool-row-file-open-failure.md | 6 +- ...026-08-18-tool-row-file-open-failure.zh.md | 6 +- ...26-07-28-skill-invocation-policy.i18n.yaml | 4 +- .../2026-07-28-skill-invocation-policy.md | 2 +- .../2026-07-28-skill-invocation-policy.zh.md | 2 +- ...-07-28-tool-call-file-open-in-os.i18n.yaml | 4 +- .../2026-07-28-tool-call-file-open-in-os.md | 6 +- ...2026-07-28-tool-call-file-open-in-os.zh.md | 6 +- ...seek-onboarding-credential-setup.i18n.yaml | 4 +- ...30-deepseek-onboarding-credential-setup.md | 2 +- ...deepseek-onboarding-credential-setup.zh.md | 2 +- ...6-07-31-web-workspace-file-links.i18n.yaml | 4 +- .../2026-07-31-web-workspace-file-links.md | 4 +- .../2026-07-31-web-workspace-file-links.zh.md | 4 +- ...8-user-explicit-skill-invocation.i18n.yaml | 4 +- ...26-08-08-user-explicit-skill-invocation.md | 4 +- ...08-08-user-explicit-skill-invocation.zh.md | 4 +- ...authorized-subagent-model-routes.i18n.yaml | 4 +- ...4-user-authorized-subagent-model-routes.md | 2 +- ...ser-authorized-subagent-model-routes.zh.md | 2 +- ...08-08-copy-only-preset-authoring.i18n.yaml | 4 +- .../2026-08-08-copy-only-preset-authoring.md | 4 +- ...026-08-08-copy-only-preset-authoring.zh.md | 4 +- ...-unary-apiproxy-remote-migration.i18n.yaml | 6 - ...6-08-10-unary-apiproxy-remote-migration.md | 120 ------ ...8-10-unary-apiproxy-remote-migration.zh.md | 120 ------ .../tests/agent-preset-authoring.overlay.yml | 3 + apps/web/tests/navigation-panes.e2e.ts | 7 +- apps/web/tests/preview-boot.e2e.ts | 17 +- apps/web/tests/produced-files.e2e.ts | 11 +- apps/web/tests/produced-files.overlay.yml | 3 + apps/web/tests/scaffold-hermetic.e2e.ts | 2 +- apps/web/tests/seeded-history.e2e.ts | 19 +- apps/web/tests/settings-chrome.e2e.ts | 8 +- docs/capability-seams.i18n.yaml | 4 +- docs/capability-seams.md | 12 +- docs/capability-seams.zh.md | 12 +- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 21 +- docs/config-catalog.zh.md | 21 +- docs/event-producer-consumer.i18n.yaml | 4 +- docs/event-producer-consumer.md | 12 +- docs/event-producer-consumer.zh.md | 12 +- docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 207 +++++----- docs/module-graph.zh.md | 207 +++++----- docs/subsystems/llm-streaming.i18n.yaml | 4 +- docs/subsystems/llm-streaming.md | 23 +- docs/subsystems/llm-streaming.zh.md | 23 +- docs/subsystems/session-reference.i18n.yaml | 4 +- docs/subsystems/session-reference.md | 31 +- docs/subsystems/session-reference.zh.md | 31 +- docs/subsystems/session.i18n.yaml | 4 +- docs/subsystems/session.md | 21 + docs/subsystems/session.zh.md | 21 + docs/subsystems/settings.i18n.yaml | 4 +- docs/subsystems/settings.md | 21 + docs/subsystems/settings.zh.md | 21 + docs/subsystems/skills.i18n.yaml | 4 +- docs/subsystems/skills.md | 23 ++ docs/subsystems/skills.zh.md | 23 ++ .../api/session-controller/README.i18n.yaml | 4 +- packages/api/session-controller/README.md | 5 +- packages/api/session-controller/README.zh.md | 5 +- .../tests/fake-api.client.ts | 53 +-- .../tests/file-references.host.spec.ts | 20 + .../session-open-workspace-path.host.spec.ts | 95 +++++ .../tests/session-skills.host.spec.ts | 191 +++++++++ .../session-controller/tests/test-remote.ts | 21 +- .../api/settings-controller/README.i18n.yaml | 4 +- packages/api/settings-controller/README.md | 19 +- packages/api/settings-controller/README.zh.md | 19 +- .../tests/settings-controller.host.spec.ts | 109 ++++- .../connection/tests/fake-api.client.ts | 35 +- .../tests/fixture-commands.client.spec.ts | 40 +- .../connection/tests/fixture.client.spec.ts | 12 +- .../connection/tests/node-half.host.spec.ts | 18 +- .../client/ui-agent-preset/README.i18n.yaml | 4 +- packages/client/ui-agent-preset/README.md | 2 +- packages/client/ui-agent-preset/README.zh.md | 2 +- .../tests/apply.client.spec.ts | 12 +- .../tests/section-store.client.spec.ts | 63 ++- .../tests/apply-inject.client.spec.tsx | 20 +- .../ui-chat/tests/chat-apply.client.spec.tsx | 6 +- .../tests/browser-plugin.client.spec.ts | 34 +- .../tests/catalog.client.spec.ts | 16 +- .../ui-settings-general/README.i18n.yaml | 4 +- packages/client/ui-settings-general/README.md | 2 +- .../client/ui-settings-general/README.zh.md | 2 +- .../tests/apply.client.spec.ts | 10 +- .../tests/components.client.spec.tsx | 15 +- .../settings-document-store.client.spec.ts | 34 +- .../tests/shell.client.spec.ts | 4 +- .../ui-settings-models/README.i18n.yaml | 4 +- packages/client/ui-settings-models/README.md | 2 +- .../client/ui-settings-models/README.zh.md | 2 +- .../tests/apply.client.spec.ts | 15 +- .../tests/components.client.spec.tsx | 51 +-- .../tests/onboarding-dialog.client.spec.tsx | 33 +- .../tests/provider-form.client.spec.tsx | 88 ++-- .../tests/store.client.spec.ts | 27 +- packages/client/ui-skill/README.i18n.yaml | 4 +- packages/client/ui-skill/README.md | 4 +- packages/client/ui-skill/README.zh.md | 4 +- .../tests/browser-plugin.client.spec.ts | 29 +- .../tests/workspaces-service.client.spec.ts | 57 +-- .../context/file-reference/README.i18n.yaml | 4 +- packages/context/file-reference/README.md | 8 +- packages/context/file-reference/README.zh.md | 8 +- .../file-reference/tests/service.spec.ts | 6 +- .../src/client/api-catalog.ts | 5 - .../src/client/slot-catalog.ts | 4 +- .../extensions/tool-cordis/src/api-catalog.ts | 137 ++++++- packages/host/apiproxy/README.i18n.yaml | 4 +- packages/host/apiproxy/README.md | 34 +- packages/host/apiproxy/README.zh.md | 34 +- .../tests/api-proxy-agent-preset.spec.ts | 375 ------------------ .../apiproxy/tests/api-proxy-config.spec.ts | 306 +------------- .../apiproxy/tests/api-proxy-host.spec.ts | 27 +- .../tests/api-proxy-skills-cold.spec.ts | 87 ---- .../apiproxy/tests/client-handler.spec.ts | 117 ------ .../host/apiproxy/tests/fetch-carrier.spec.ts | 137 +------ .../host/apiproxy/tests/rpc-schemas.spec.ts | 32 -- .../llm/llm-pi-ai/tests/discovery.spec.ts | 6 +- packages/util/native-command/README.i18n.yaml | 4 +- packages/util/native-command/README.md | 19 +- packages/util/native-command/README.zh.md | 19 +- packages/util/native-command/package.json | 2 +- .../native-command/tests/path-opener.spec.ts} | 3 +- pnpm-lock.yaml | 60 ++- scripts/gen-cordis-catalog.ts | 9 + scripts/gen-cordis-inspect-catalog.ts | 2 +- scripts/gen-doc-graphs.ts | 20 +- 158 files changed, 1754 insertions(+), 2293 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md create mode 100644 .agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md delete mode 100644 .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml delete mode 100644 .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md delete mode 100644 .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md create mode 100644 packages/api/session-controller/tests/file-references.host.spec.ts create mode 100644 packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts create mode 100644 packages/api/session-controller/tests/session-skills.host.spec.ts delete mode 100644 packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts delete mode 100644 packages/host/apiproxy/tests/api-proxy-skills-cold.spec.ts rename packages/{host/apiproxy/tests/native-path-opener.spec.ts => util/native-command/tests/path-opener.spec.ts} (99%) diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml index 64e6bbc289..1548b145b4 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.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/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md -2026-07-25-web-client-session-scope-and-provide-channel.md: 6f159dc16e0e4063f9077c31829a10caca98eae0 -2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 2cb082dce75f1845e52a52289a8e3eaa1a000931 +2026-07-25-web-client-session-scope-and-provide-channel.md: 1fa442e8db2d8b2d2ec66730700c9c88dceddbae +2026-07-25-web-client-session-scope-and-provide-channel.zh.md: ea9a6e247402e6a2d15fb4bfc0ebd6e65fc021df diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md index 6f159dc16e..1fa442e8db 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md +++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md @@ -107,7 +107,7 @@ Slot scope is the closed set `root | session-maybe | session`: - The summary `blank` column and the `host/session-added` frame's `blank` field (see the blank bit above). - The SSE frame `host/commands-changed` (a pure invalidation signal); the client routes it into the typed events `commands/changed` and `connection/reset` (broadcast after each connection generation is established; wire-derived caches uniformly treat prior state as stale). The commands frame and its typed client event were later replaced by verbatim forwarding of `commands/change` through `ctx.remote.$on` ([forwarded Remote events](2026-08-10-remote-event-delivery.md)); `connection/reset` is unchanged, and the invalidation-not-diffing contract this bullet states still holds. -- `command.list/execute` and `skill.list` are uniformly single-addressed by `sessionId` (a session always has an Agent; `agentFor`'s resume semantics come ready-made); the command-surface narrative lives in the [command surfaces note](2026-07-25-web-command-surfaces-and-assembly.md). +- `command.list/execute` and `skills/list` are uniformly single-addressed by `sessionId` (a session always has an Agent; `agentFor`'s resume semantics come ready-made); the command-surface narrative lives in the [command surfaces note](2026-07-25-web-command-surfaces-and-assembly.md). - The `session.create` request shape: workspaceId/cwd as either-or, plus an optional caller-preallocated sessionId (a same-id same-cwd retry is idempotent; a different cwd reports `session-conflict`). ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md index 2cb082dce7..ea9a6e2474 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md @@ -107,7 +107,7 @@ slot scope 是闭集 `root | session-maybe | session`: - summary `blank` 列与 `host/session-added` 帧 `blank` 字段(见上文 blank 位)。 - SSE(Server-Sent Events)帧 `host/commands-changed`(纯失效信号);client 路由为类型事件 `commands/changed` 与 `connection/reset`(连接代建立后广播,wire 派生缓存一律视旧态为陈旧)。 该 commands 帧及其类型化 client 事件后来被「`commands/change` 经 `ctx.remote.$on` 原样转发」取代([转发的 Remote 事件](2026-08-10-remote-event-delivery.zh.md));`connection/reset` 不变;本条陈述的「失效而非差分」契约依然成立。 -- `command.list/execute`、`skill.list` 一律 `sessionId` 单址(会话恒有 Agent,`agentFor` 的恢复语义现成);命令面叙述见[命令业务面 note](2026-07-25-web-command-surfaces-and-assembly.zh.md)。 +- `command.list/execute`、`skills/list` 一律 `sessionId` 单址(会话恒有 Agent,`agentFor` 的恢复语义现成);命令面叙述见[命令业务面 note](2026-07-25-web-command-surfaces-and-assembly.zh.md)。 - `session.create` 请求形状:workspaceId/cwd 二选一 + 可选调用方预分配 sessionId(同 id 同 cwd 重试幂等,异 cwd 报 `session-conflict`)。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.i18n.yaml index 9bbd5cdb6a..c04c301afa 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.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/architecture/2026-07-25-web-command-surfaces-and-assembly.md -2026-07-25-web-command-surfaces-and-assembly.md: a3628a7d99a6ab33652c67f6188933dea213194b -2026-07-25-web-command-surfaces-and-assembly.zh.md: eef93d750c502f5c034b6604774de31d75d6d342 +2026-07-25-web-command-surfaces-and-assembly.md: 943b68416d896c674783156b05997840c6df3255 +2026-07-25-web-command-surfaces-and-assembly.zh.md: c35d47202894af99377932df57e5de118186c1ce diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md b/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md index a3628a7d99..943b68416d 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md +++ b/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md @@ -28,7 +28,7 @@ The pipeline was ready but command knowledge had no landing spot: host-side `ctx ### Reference sources (seeing only projections plus their own apply closures, on the root ctx) -- **ui-skill**: `skill.list({sessionId})` addresses by session (the host resolves the project root from the session header); the directory cache is single-flight keyed by sessionId, prewarmed at birth by the `warm` hook and fully cleared by `connection/reset`. A pick produces a text outcome (the literal `/name ` text, the plain-text-reference decision); `lexicon` supplies the roster from CatalogFetch's settled snapshot (`undefined` while not warm), and `subscribeLexicon` notifies per-session listeners on settle and on invalidation. No match hook (references never enter command adjudication). Skill references ride ordinary prompts as literal text (outside the command plane; tool-skill unchanged, with the session-prefix directory providing the cooperative association). +- **ui-skill**: `skills/list({sessionId})` addresses by session (the host resolves the project root from the session header); the directory cache is single-flight keyed by sessionId, prewarmed at birth by the `warm` hook and fully cleared by `connection/reset`. A pick produces a text outcome (the literal `/name ` text, the plain-text-reference decision); `lexicon` supplies the roster from CatalogFetch's settled snapshot (`undefined` while not warm), and `subscribeLexicon` notifies per-session listeners on settle and on invalidation. No match hook (references never enter command adjudication). Skill references ride ordinary prompts as literal text (outside the command plane; tool-skill unchanged, with the session-prefix directory providing the cooperative association). - **ui-subagent**: candidates are zero-RPC (the sessions.list snapshot filtered by parentId/running); a pick produces a text outcome (the literal `@name ` text); `lexicon` derives from the same snapshot and `subscribeLexicon` forwards the list store's change feed (the model-side representation awaits its business workstream). ### Fixture command routing and assembly diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md b/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md index eef93d750c..c35d472028 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md @@ -28,7 +28,7 @@ Status: implemented ### 引用源(只见投影 + 自家 apply 闭包的 root ctx) -- **ui-skill**:`skill.list({sessionId})` 按会话寻址(host 从会话 header 解析项目根);目录缓存按 sessionId 键控 single-flight,`warm` 钩子出生预热、`connection/reset` 全清。pick 产出 text outcome(`/name ` 原文,纯文本引用决策);`lexicon` 从 CatalogFetch 的 settled 快照给名录(未热 `undefined`),`subscribeLexicon` 在 settle 与失效时按会话通知监听者。无 match 钩子(引用不进命令裁决)。skill 引用以原文随普通提示词走(命令平面之外;tool-skill 不变,会话前缀目录提供协作关联)。 +- **ui-skill**:`skills/list({sessionId})` 按会话寻址(host 从会话 header 解析项目根);目录缓存按 sessionId 键控 single-flight,`warm` 钩子出生预热、`connection/reset` 全清。pick 产出 text outcome(`/name ` 原文,纯文本引用决策);`lexicon` 从 CatalogFetch 的 settled 快照给名录(未热 `undefined`),`subscribeLexicon` 在 settle 与失效时按会话通知监听者。无 match 钩子(引用不进命令裁决)。skill 引用以原文随普通提示词走(命令平面之外;tool-skill 不变,会话前缀目录提供协作关联)。 - **ui-subagent**:候选零 RPC(sessions.list 快照按 parentId/running 过滤);pick 产出 text outcome(`@name ` 原文);`lexicon` 同快照派生,`subscribeLexicon` 转发 list store 的变更通道(模型侧表示待业务立项)。 ### fixture 命令路由与装配 diff --git a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml index 6e02fa3915..16238db465 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.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/architecture/2026-07-30-web-config-plane.md -2026-07-30-web-config-plane.md: c817071ed17554d06249aa6893ed759bea5d72d0 -2026-07-30-web-config-plane.zh.md: 0b15e329340051d0f63e8a5b1f8c70a29a2138d2 +2026-07-30-web-config-plane.md: 81b501db529bf1b2974fd4541045991c5a8bf087 +2026-07-30-web-config-plane.zh.md: 3f02a17e4826bb35ecfd45da25c4b0170be270cb diff --git a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md index c817071ed1..81b501db52 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md +++ b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md @@ -12,11 +12,11 @@ The request-level configuration seam made LLM adapter configuration restart-free ## Decision -**Configuration calls use their owning wire implementation, rejections as codes, and owner events forwarded verbatim.** `@deepseek-ai/dsh-api-settings-controller` owns generated Remote methods for `settings/describe`, `settings/update`, `settings/replace`, `settings/mutate`, and `credentials/describe|set|unset`; `settings.openDocument` and the `llm.*` methods remain in `RpcMethodMap`. Provider absence retains the configuration API's actionable `internal` diagnostic, while seam rejections retain `settings-rejected {ns}` / `settings-conflict {ns, expected, actual}` / `credential-rejected {ref}`. Clients subscribe to forwarded settings, credentials, and LLM owner events and converge without polling ([forwarded Remote events](2026-08-10-remote-event-delivery.md)). Connection authenticates generated Remote methods and API Proxy fallbacks with the same browser session; Host/Origin failures still return 403 before identity is checked. +**Configuration calls use their owning wire implementation, rejections as codes, and owner events forwarded verbatim.** `@deepseek-ai/dsh-api-settings-controller` owns generated Remote methods for `settings/describe`, `settings/update`, `settings/replace`, `settings/mutate`, `settings/openSettingsDocument`, `settings/openAgentPresetDirectory`, and `credentials/describe|set|unset`; `@deepseek-ai/dsh-llm` owns `llm/listProviders`, `llm/listConfigurableProviders`, and `llm/discoverModels`. Provider absence retains the configuration API's actionable `internal` diagnostic, while seam rejections retain `settings-rejected {ns}` / `settings-conflict {ns, expected, actual}` / `credential-rejected {ref}`. Clients subscribe to forwarded settings, credentials, and LLM owner events and converge without polling ([forwarded Remote events](2026-08-10-remote-event-delivery.md)). Connection authenticates generated Remote methods and API Proxy fallbacks with the same browser session; Host/Origin failures still return 403 before identity is checked. **`describe()` grows layers and structural secret redaction.** `SettingsDescriptor` carries `base`/`user` beside the effective value, so the form marks "overridden" by presence in the user layer, not value inequality (an override *equal* to the base is still an override). `describe({ redactSecrets: true })` — mandatory at every wire face — strips `role('secret')` subtrees from all three layers via a pure structural walk of the schema (object/dict/array containers; a secret-role subtree is one opaque leaf) and enumerates the stripped slots as `{path, set}`, so a page can render write-only inputs without ever receiving a value. -**The Host identifies and opens the local settings document.** The settings seam exposes optional `documentPath` provider metadata and a `prepareDocument()` operation; `settings-file` returns its fully resolved custom or `$DSH_HOME/settings.yaml` filename and exclusively creates an absent empty document with owner-only permissions, while non-file providers retain the base `undefined`. The browser-authenticated `settings.describe` response carries only the boolean `hasDocument` capability beside the redacted namespace views. `ui-settings-general` registers a `settings.action` entry only on loopback pages, shows it only after the metadata confirms that a provider-owned local document can be prepared, and invokes pathless `settings.openDocument`; the Host resolves the provider path again before a text-document handoff (`open -t` on macOS so an arbitrary YAML file association cannot redirect the gesture, `xdg-open` on desktop Linux, `Invoke-Item` on Windows, and `wslpath -w` followed by that Windows handoff on WSL). Generic workspace paths retain the default intent, including its browser preference for browser-renderable documents. The browser neither derives `$DSH_HOME` nor receives a filesystem target; non-loopback pages retain the Client policy that makes no Host settings read for this action. +**The Host identifies and opens the local settings document.** The settings seam exposes optional `documentPath` provider metadata and a `prepareDocument()` operation; `settings-file` returns its fully resolved custom or `$DSH_HOME/settings.yaml` filename and exclusively creates an absent empty document with owner-only permissions, while non-file providers retain the base `undefined`. The browser-authenticated `settings/describe` response carries only the boolean `hasDocument` capability beside the redacted namespace views. `ui-settings-general` registers a `settings.action` entry only on loopback pages, shows it only after the metadata confirms that a provider-owned local document can be prepared, and invokes pathless `settings/openSettingsDocument`; the Host resolves the provider path again before a text-document handoff (`open -t` on macOS so an arbitrary YAML file association cannot redirect the gesture, `xdg-open` on desktop Linux, `Invoke-Item` on Windows, and `wslpath -w` followed by that Windows handoff on WSL). Generic workspace paths retain the default intent, including its browser preference for browser-renderable documents. The browser neither derives `$DSH_HOME` nor receives a filesystem target; non-loopback pages retain the Client policy that makes no Host settings read for this action. **The llm seam declares configurability and announces topology.** `registerConfigurableProviders()` is an all-or-nothing, fiber-scoped directory of `{provider, displayName, settingsNs, settingsPath}` — the addressing a config page needs to open the right settings subtree for a route that may not exist yet; `listConfigurableProviders()` merges with live routes in the wire handler so undeclared live routes still report active. The zero-payload `'llm/adapters-updated'` event fires from all four registration/unregistration commit points with contained listener dispatch (INVARIANT rethrow), following the settings/commands precedent. `llm-deepseek`'s route renamed to `deepseek-official` because the pi-ai catalog legitimately owns `deepseek` as an aggregator entry; pre-release stance, no alias. @@ -32,7 +32,7 @@ The request-level configuration seam made LLM adapter configuration restart-free - **Storing the typed key as a literal `apiKey` setting** — the single API key input requirement could have written the literal into the profile, but every UI removal path rebuilds the user section from the *redacted* layers, so any reset or row deletion would silently drop stored sibling keys; deriving a reference keeps the input single-field while keeping `settings.yaml` secret-free and every replace safe. - **A `models` bridge plugin owning provider configuration** — same rejection as in the request-level seam note: per-plugin namespaces plus a four-field directory declaration give the UI everything it needs; the bridge's unified dict re-imports the adapter-mapping indirection. - **Page-side polling instead of pushed frames** — the mux already carries `host/commands-changed`; three more frames cost one shape each and make a second tab, an external `settings.yaml` edit, and a settings-born route converge at event speed. -- **Hard-coding `$DSH_HOME/settings.yaml` or returning `documentPath` through `host.openPath` in the browser** — rejected because `settings-file.path` may select another YAML/JSON document, non-file providers have no Host path, and a general path request makes the browser the authority for a local filesystem target. Provider preparation is the authoritative source, and the Host-owned operation feeds the existing opener. +- **Hard-coding `$DSH_HOME/settings.yaml` or returning `documentPath` through `session/openWorkspacePath` in the browser** — rejected because `settings-file.path` may select another YAML/JSON document, non-file providers have no Host path, and a general path request makes the browser the authority for a local filesystem target. Provider preparation is the authoritative source, and the Host-owned operation feeds the existing opener. ## Consequences diff --git a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md index 0b15e32934..3f02a17e48 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md @@ -12,11 +12,11 @@ Status: implemented ## 决策 -**配置调用使用其所属的 wire 实现,拒绝落为错误码,owner 事件原样转发。**`@deepseek-ai/dsh-api-settings-controller` 持有 `settings/describe`、`settings/update`、`settings/replace`、`settings/mutate` 与 `credentials/describe|set|unset` 的生成 Remote 方法;`settings.openDocument` 和 `llm.*` 方法仍位于 `RpcMethodMap`。provider 缺失时保留配置 API 可操作的 `internal` 诊断,seam 拒绝则保留 `settings-rejected {ns}`/`settings-conflict {ns, expected, actual}`/`credential-rejected {ref}`。Client 订阅转发的 settings、credentials 与 LLM owner 事件,无需轮询即可收敛(见[转发的 Remote 事件](2026-08-10-remote-event-delivery.zh.md))。Connection 使用同一个浏览器会话认证生成的 Remote 方法与 API Proxy 回退;Host/Origin 失败仍会在身份校验前返回 403。 +**配置调用使用其所属的 wire 实现,拒绝落为错误码,owner 事件原样转发。**`@deepseek-ai/dsh-api-settings-controller` 持有 `settings/describe`、`settings/update`、`settings/replace`、`settings/mutate`、`settings/openSettingsDocument`、`settings/openAgentPresetDirectory` 与 `credentials/describe|set|unset` 的生成 Remote 方法;`@deepseek-ai/dsh-llm` 持有 `llm/listProviders`、`llm/listConfigurableProviders` 与 `llm/discoverModels`。provider 缺失时保留配置 API 可操作的 `internal` 诊断,seam 拒绝则保留 `settings-rejected {ns}`/`settings-conflict {ns, expected, actual}`/`credential-rejected {ref}`。Client 订阅转发的 settings、credentials 与 LLM owner 事件,无需轮询即可收敛(见[转发的 Remote 事件](2026-08-10-remote-event-delivery.zh.md))。Connection 使用同一个浏览器会话认证生成的 Remote 方法与 API Proxy 回退;Host/Origin 失败仍会在身份校验前返回 403。 **`describe()` 增加分层与结构化 secret 脱敏。**`SettingsDescriptor` 在生效值之外携带 `base`/`user`,表单据此按「字段是否出现在用户层」来标记「已覆盖」,而非按值是否不等(与 base *相等*的覆盖仍然是覆盖)。`describe({ redactSecrets: true })`——在每个 wire 面都强制启用——经由对 schema 的纯结构遍历(object/dict/array 容器;secret 角色子树整体是一个不透明叶节点)从全部三层剥除 `role('secret')` 子树,并把剥除的槽位枚举为 `{path, set}`,页面因此不必收到任何值就能渲染只写输入框。 -**Host 识别并打开本地设置文档。** settings seam 暴露可选的 `documentPath` 提供方元数据和 `prepareDocument()` 操作;`settings-file` 返回已完全解析的自定义文件名或 `$DSH_HOME/settings.yaml` 文件名,并在文档缺失时以仅属主可访问的权限独占创建空文档,非文件提供方则保留基类的 `undefined`。经浏览器认证的 `settings.describe` 响应会在脱敏 namespace 视图旁只携带布尔型 `hasDocument` 能力。`ui-settings-general` 只在回环页面注册一条 `settings.action` 条目,只有元数据确认可准备好一份由提供方持有的本地文档后才显示,并调用无路径参数的 `settings.openDocument`;Host 会在文本文档交接前再次解析提供方路径(macOS 上使用 `open -t`,使任意 YAML 文件关联无法重定向这次操作;桌面 Linux 上使用 `xdg-open`;Windows 上使用 `Invoke-Item`;WSL 上先执行 `wslpath -w`,再使用同一 Windows 交接)。通用 Workspace 路径仍保留默认意图,包括针对浏览器可渲染文档的浏览器偏好。浏览器既不推导 `$DSH_HOME`,也不会收到文件系统目标;非 loopback 页面保留 Client 策略,不为这项操作发起 Host settings 读取。 +**Host 识别并打开本地设置文档。** settings seam 暴露可选的 `documentPath` 提供方元数据和 `prepareDocument()` 操作;`settings-file` 返回已完全解析的自定义文件名或 `$DSH_HOME/settings.yaml` 文件名,并在文档缺失时以仅属主可访问的权限独占创建空文档,非文件提供方则保留基类的 `undefined`。经浏览器认证的 `settings/describe` 响应会在脱敏 namespace 视图旁只携带布尔型 `hasDocument` 能力。`ui-settings-general` 只在回环页面注册一条 `settings.action` 条目,只有元数据确认可准备好一份由 provider 持有的本地文档后才显示,并调用无路径参数的 `settings/openSettingsDocument`;Host 会在文本文档交接前再次解析 provider 路径(macOS 上使用 `open -t`,使任意 YAML 文件关联无法重定向这次操作;桌面 Linux 上使用 `xdg-open`;Windows 上使用 `Invoke-Item`;WSL 上先执行 `wslpath -w`,再使用同一 Windows 交接)。通用 Workspace 路径仍保留默认意图,包括针对浏览器可渲染文档的浏览器偏好。浏览器既不推导 `$DSH_HOME`,也不会收到文件系统目标;非 loopback 页面保留 Client 策略,不为这项操作发起 Host settings 读取。 **llm seam 声明可配置性并公布拓扑。**`registerConfigurableProviders()` 是一个全有或全无、以 fiber 为作用域的目录,条目为 `{provider, displayName, settingsNs, settingsPath}`——这正是配置页要为一条可能尚不存在的路由打开正确设置子树时所需要的寻址;`listConfigurableProviders()` 在 wire 处理器里与存活路由合并,未声明的存活路由因此仍报告为激活。零负载的 `'llm/adapters-updated'` 事件从全部四个注册/注销提交点触发,listener 派发带异常隔离(INVARIANT 重抛),沿用 settings/commands 的先例。`llm-deepseek` 的路由重命名为 `deepseek-official`,因为 pi-ai catalog 名正言顺地拥有 `deepseek` 这个聚合器条目;依预发布立场,不设别名。 @@ -32,7 +32,7 @@ Status: implemented - **把键入的密钥存成字面 `apiKey` 设置**——单个 API 密钥输入框的需求本可以把字面量直接写进 profile,但 UI 的每条删除路径都会从*脱敏后的*各层重建用户分节,任何重置或整行删除都会静默丢掉已存储的兄弟密钥;派生引用让输入保持单字段,同时让 `settings.yaml` 不含机密、每一次 replace 都安全。 - **由 `models` 桥接插件持有提供方配置**——与请求级 seam note 相同的否决理由:按插件划分的 namespace 加上四字段的目录声明已经给了 UI 需要的一切;桥接层的统一字典会把适配器映射那层间接重新引进来。 - **页面侧轮询而非推送帧**——mux 已经承载 `host/commands-changed`;再加三个帧,每个只需增加一种形状,就能让第二个标签页、外部的 `settings.yaml` 编辑和由设置催生的路由都以事件速度收敛。 -- **在浏览器中硬编码 `$DSH_HOME/settings.yaml`,或经 `host.openPath` 回传 `documentPath`**——否决,因为 `settings-file.path` 可能选择另一份 YAML/JSON 文档、非文件提供方没有 Host 路径,而且通用路径请求会让浏览器成为本地文件系统目标的权威。提供方的准备操作才是权威来源,由 Host 持有的操作会把结果交给现有打开器。 +- **在浏览器中硬编码 `$DSH_HOME/settings.yaml`,或经 `session/openWorkspacePath` 回传 `documentPath`**——否决,因为 `settings-file.path` 可能选择另一份 YAML/JSON 文档、非文件提供方没有 Host 路径,而且通用路径请求会让浏览器成为本地文件系统目标的权威。提供方的准备操作才是权威来源,由 Host 持有的操作会把结果交给现有打开器。 ## 后果 diff --git a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml new file mode 100644 index 0000000000..617d6b07db --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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/architecture/2026-08-10-unary-apiproxy-remote-migration.md +2026-08-10-unary-apiproxy-remote-migration.md: 027376cbf772043cce21f487c94288cad6bece1c +2026-08-10-unary-apiproxy-remote-migration.zh.md: f7507959a992dd17ca60883ada7769c7196fdc3a diff --git a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md new file mode 100644 index 0000000000..027376cbf7 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md @@ -0,0 +1,59 @@ +# Agent Note: Place unary browser operations on owning Remote services + +Status: implemented + +English | [中文](2026-08-10-unary-apiproxy-remote-migration.zh.md) + +## Problem + +The Host API Proxy duplicated simple unary operations across business Services, API Proxy interfaces, Zod schemas, route tables, client stubs, and Client callers. [Typert Remote calls](2026-08-02-typert-remote-method-calls.md) already let a business package own this class of call, but moving an endpoint without its lifecycle and projection policy could change observable behavior. + +Agent-bound calls require particular care. Shared lookup policy reuses live Agents, resumes ordinary cold Sessions with their recorded presets, deduplicates concurrent resumes, and rejects subagent-owned identities. Skill listing instead must inspect a Session without activating its Agent. Native desktop operations must keep the browser from choosing an arbitrary Host path. + +## Decision + +Simple unary operations live on their natural business Remote owner. The business package owns the Remote signature and Host adaptation; `@deepseek-ai/dsh-api-remotes/client` selects its generated contribution; the Client package owns presentation joins. The API Proxy retains only `host.describe` and streamed `GET`/`HEAD /api/session.export`. + +| Legacy RPC | Remote destination | Owner and preserved behavior | +|---|---|---| +| `session.rename` | `sessionTitle/rename` | `SessionTitleService` resolves the Session through the shared lookup policy and returns the title event sequence. | +| `command.list`, `command.execute` | `commands/list`, `commands/execute` | `CommandRuntime` preserves Agent lookup, unmatched commands, and caller cancellation. | +| `llm.providers` | `llm/listProviders`, `llm/listConfigurableProviders` | `LlmRuntime` owns provider facts; Clients join live and configurable rows. | +| `llm.discoverModels` | `llm/discoverModels` | `LlmRuntime` preserves provider discovery, cancellation, and sanitized failures. | +| `llm.models` | `session/modelCatalog` | `SessionController` owns the Host-generation catalog, default selection, and isolated provider failures. | +| `credentials.describe`, `credentials.set`, `credentials.unset` | `credentials/describe`, `credentials/set`, `credentials/unset` | `CredentialsController` preserves reference validation, field projection, provider diagnostics, and refusal mapping. | +| `settings.describe`, `settings.update`, `settings.replace`, `settings.mutate` | Equivalent `settings/*` methods | `SettingsController` preserves redaction, mutation semantics, revision checks, and provider failures. | +| `settings.openDocument` | `settings/openSettingsDocument` | `SettingsController` prepares the provider-owned document and opens it with text-editor intent. | +| `agentPreset.read`, `agentPreset.copy`, `agentPreset.remove` | Equivalent `agentPresets/*` methods | `AgentPresetService` owns document reads, copies, and removals. | +| `agentPreset.openDocument` | `settings/openAgentPresetDirectory` | `SettingsController` resolves the preset directory and returns its path when native opening is unavailable. | +| `subagent.interrupt` | `subagents/interruptByParent` | The subagent service preserves parent authority without activating either Agent. | +| `workspace.list`, `workspace.insertSessionBefore`, `workspace.archiveSession` | Equivalent `workspace/*` methods | The Workspace registry owns detached snapshots and serialized mutations. | +| `skill.list` | `skills/list` | `SessionSkillCatalog` observes the Session and its recorded preset, uses a live Agent only when one already exists, and never activates an Agent for listing. | +| `fileReferences/list` | `fileReferences/list` | `SessionFileReferences` supplies the Session Controller's established Agent lookup to the provider; cold lookup behavior remains unchanged. | +| `host.openPath` | `session/openWorkspacePath` | `SessionController` resolves the path against the addressed Session's workspace before native opening. | + +The shared Agent and Session resolver remains the authority for endpoints that accept those objects. It provides the same live reuse, cold restoration, concurrent deduplication, preset setup, persistence failures, and subagent ownership fence that legacy API Proxy calls used. `TypertLookupFailure` preserves resolver-owned RPC errors instead of collapsing them into `internal`. + +The native path implementation lives in `@deepseek-ai/dsh-native-command`. Session and Settings controllers select the target; the utility only performs platform detection, WSL translation, browser preference, text-editor intent, and shell-free command execution. + +## Browser authentication + +Connection authenticates the complete `/api` request before choosing the Typert interceptor or API Proxy fallback. Remote-owned endpoints and retained API Proxy endpoints therefore require the same browser session and Host/Origin checks. + +## Verification + +Focused Host and Client tests cover Remote calls, lookup and no-activation policy, native opening, error projection, and removal of legacy routes. The repository build generates and consumes the selected Remote contributions before building the Web application. + +## Alternatives considered + +**Keep simple calls in the API Proxy.** Rejected because it preserves duplicate interfaces, schemas, route rows, stubs, and result projections after a business owner exists. + +**Move every unary operation.** Rejected because `host.describe` combines deployment facts and Connection readiness, while Session export is a streamed download rather than a unary business method. + +**Put native opening in one controller.** Rejected because Session, Settings, and the retained Host description consume the same platform operation. A Host utility avoids controller-to-controller imports without making the browser authoritative for filesystem targets. + +## Consequences + +Business owners and Client consumers each define one side of a unary operation, while Connection retains authentication, transport, and response envelopes. Removing the legacy client timeout is the accepted observable transport change; business results, cancellation, lifecycle policy, filtering, and native-path authority remain owned by their existing domains. + +Generated Remote artifacts and the explicit API Remotes assembly become required whenever a Remote signature or selected package changes. diff --git a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md new file mode 100644 index 0000000000..f7507959a9 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md @@ -0,0 +1,59 @@ +# Agent Note: 将一元浏览器操作放到所属 Remote 服务 + +Status: implemented + +[English](2026-08-10-unary-apiproxy-remote-migration.md) | 中文 + +## 问题 + +Host API Proxy 曾在业务 Service、API Proxy interface、Zod schema、路由表、Client stub 与 Client 调用方之间重复定义简单一元操作。[Typert Remote 调用](2026-08-02-typert-remote-method-calls.zh.md)已经允许业务包持有这类调用,但如果迁移 endpoint 时没有一并保留生命周期与投影策略,就会改变可观察行为。 + +与 Agent 绑定的调用需要格外谨慎。共享 lookup 策略会复用 live Agent、用记录的 preset 恢复普通冷 Session、对并发恢复去重,并拒绝由 subagent 持有的 identity。skill 列表则必须检查 Session 而不激活 Agent。原生桌面操作必须避免让浏览器选择任意 Host 路径。 + +## 决策 + +简单一元操作归属其自然的业务 Remote owner。业务包持有 Remote 签名与 Host 适配;`@deepseek-ai/dsh-api-remotes/client` 选择其生成贡献;Client 包持有呈现联接。API Proxy 只保留 `host.describe` 与流式 `GET`/`HEAD /api/session.export`。 + +| 旧 RPC | Remote 目标 | Owner 与保留行为 | +|---|---|---| +| `session.rename` | `sessionTitle/rename` | `SessionTitleService` 通过共享 lookup 策略解析 Session,并返回标题事件序号。 | +| `command.list`、`command.execute` | `commands/list`、`commands/execute` | `CommandRuntime` 保留 Agent lookup、未匹配命令与调用方取消。 | +| `llm.providers` | `llm/listProviders`、`llm/listConfigurableProviders` | `LlmRuntime` 持有 provider 事实;Client 联接 live 与 configurable 行。 | +| `llm.discoverModels` | `llm/discoverModels` | `LlmRuntime` 保留 provider 发现、取消与净化后的失败。 | +| `llm.models` | `session/modelCatalog` | `SessionController` 持有 Host generation 的目录、默认选择与隔离后的 provider 失败。 | +| `credentials.describe`、`credentials.set`、`credentials.unset` | `credentials/describe`、`credentials/set`、`credentials/unset` | `CredentialsController` 保留引用校验、字段投影、provider 诊断与拒绝映射。 | +| `settings.describe`、`settings.update`、`settings.replace`、`settings.mutate` | 对应的 `settings/*` 方法 | `SettingsController` 保留脱敏、mutation 语义、revision 校验与 provider 失败。 | +| `settings.openDocument` | `settings/openSettingsDocument` | `SettingsController` 准备 provider 持有的文档,并按文本编辑器意图打开。 | +| `agentPreset.read`、`agentPreset.copy`、`agentPreset.remove` | 对应的 `agentPresets/*` 方法 | `AgentPresetService` 持有文档读取、复制与删除。 | +| `agentPreset.openDocument` | `settings/openAgentPresetDirectory` | `SettingsController` 解析 preset 目录,并在原生打开不可用时返回其路径。 | +| `subagent.interrupt` | `subagents/interruptByParent` | subagent 服务保留 parent 权限,且不激活任何一方的 Agent。 | +| `workspace.list`、`workspace.insertSessionBefore`、`workspace.archiveSession` | 对应的 `workspace/*` 方法 | Workspace registry 持有脱离可变对象的 snapshot 与串行 mutation。 | +| `skill.list` | `skills/list` | `SessionSkillCatalog` 观察 Session 及其记录的 preset,仅在 live Agent 已存在时使用它,列表查询绝不激活 Agent。 | +| `fileReferences/list` | `fileReferences/list` | `SessionFileReferences` 向 provider 提供 Session Controller 的既有 Agent lookup;冷 lookup 行为保持不变。 | +| `host.openPath` | `session/openWorkspacePath` | `SessionController` 先基于目标 Session 的 workspace 解析路径,再执行原生打开。 | + +共享 Agent 与 Session resolver 仍是接收这些对象的 endpoint 的权威。它提供与旧 API Proxy 调用相同的 live 复用、冷恢复、并发去重、preset setup、持久化失败与 subagent ownership fence。`TypertLookupFailure` 保留 resolver 持有的 RPC error,而不把它们归并为 `internal`。 + +原生路径实现在 `@deepseek-ai/dsh-native-command` 中。Session 与 Settings controller 选择目标;该工具仅负责平台探测、WSL 转换、浏览器偏好、文本编辑器意图与无 shell 命令执行。 + +## 浏览器认证 + +Connection 在选择 Typert interceptor 或 API Proxy fallback 前认证完整的 `/api` 请求。因此 Remote 持有的 endpoint 与保留的 API Proxy endpoint 要求相同的浏览器会话和 Host/Origin 校验。 + +## 验证 + +聚焦的 Host 与 Client 测试覆盖 Remote 调用、lookup 与不激活策略、原生打开、错误投影和 legacy 路由移除。仓库构建会先生成并消费所选 Remote contribution,再构建 Web 应用。 + +## 考虑过的替代方案 + +**将简单调用留在 API Proxy。** 否决,因为业务 owner 已存在后,这仍会保留重复的 interface、schema、路由行、stub 与结果投影。 + +**迁移每一个一元操作。** 否决,因为 `host.describe` 组合部署事实与 Connection readiness,而 Session export 是流式下载,不是一元业务方法。 + +**把原生打开操作放入某个 controller。** 否决,因为 Session、Settings 与保留的 Host 描述都会消费同一平台操作。Host 工具可以避免 controller 间导入,同时不让浏览器成为文件系统目标的权威。 + +## 后果 + +业务 owner 与 Client consumer 各自定义一元操作的一侧,而 Connection 继续持有认证、传输与响应 envelope。删除 legacy Client timeout 是已接受的可观察传输变化;业务结果、取消、生命周期策略、过滤与原生路径权限仍由既有领域持有。 + +每当 Remote 签名或所选包发生变化,都必须更新生成的 Remote 产物和显式 API Remotes assembly。 diff --git a/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.i18n.yaml index 7b2d89cd39..e482014ebc 100644 --- a/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.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/architecture/2026-08-17-settings-describe-mirror.md -2026-08-17-settings-describe-mirror.md: a3774699ff328a44aed192a16dea0fa19d03c83c -2026-08-17-settings-describe-mirror.zh.md: 1fad68ae6346d656cc7868121eac14fdf845f8dc +2026-08-17-settings-describe-mirror.md: 81be16c83e61f8c44b4903b4cc57e26d80fe065e +2026-08-17-settings-describe-mirror.zh.md: 6c37992b7de7c2c225bb2eb965f6f758b8be990e diff --git a/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.md b/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.md index a3774699ff..81be16c83e 100644 --- a/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.md +++ b/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.md @@ -30,5 +30,5 @@ The cold-boot budget is pinned at two reads by `apps/web/tests/startup-rpc-budge - Startup `settings.describe` went 15 → 2, and a new preference-owning plugin adds zero reads. - Every derived surface shows the same document revision at any moment; the per-reader guards (`refreshWelcomeIfLoaded`, `refreshPermissionIfLoaded`, `refreshDocumentIfLoaded`) and their subscriptions are gone. - The mirror refreshes on every document commit regardless of namespace, so an external settings edit now costs one background read even while no settings surface is open — the price of surfaces that open already fresh. The per-namespace `ns !== spec.namespace` filters are gone with the per-scope subscriptions. -- `credentials.describe` (3 startup calls), `agentPreset.list` (2), and `llm.providers` are separate sources and stay direct; the same mirror pattern fits them if they ever need it. +- `credentials/describe` (3 startup calls), `agentPresets/list` (2), `llm/listProviders`, and `llm/listConfigurableProviders` are separate sources and stay direct; the same mirror pattern fits them if they ever need it. - A new direct `settings.describe` caller in client code is a budget regression; the e2e's failure message says to grep for callers outside `ui-settings`. diff --git a/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.zh.md b/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.zh.md index 1fad68ae63..6c37992b7d 100644 --- a/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-17-settings-describe-mirror.zh.md @@ -30,5 +30,5 @@ Status: implemented - 启动期 `settings.describe` 从 15 次降到 2 次,新增持有偏好设置的插件带来零次新增读取。 - 任一时刻每个派生面看到的都是同一份文档 revision;各读取方的防护(`refreshWelcomeIfLoaded`、`refreshPermissionIfLoaded`、`refreshDocumentIfLoaded`)及其订阅随之消失。 - 镜像对任何命名空间的文档提交都会刷新,因此在没有任何设置表面打开时,一次外部设置编辑现在也花费一次后台读取——这是「表面打开即新鲜」的代价。随着各 scope 订阅的删除,按命名空间的 `ns !== spec.namespace` 过滤一并消失。 -- `credentials.describe`(启动 3 次)、`agentPreset.list`(2 次)与 `llm.providers` 是另外的数据源,保持直连;若将来需要,同一镜像模式对它们同样适用。 +- `credentials/describe`(启动 3 次)、`agentPresets/list`(2 次)、`llm/listProviders` 与 `llm/listConfigurableProviders` 是另外的数据源,保持直连;若将来需要,同一镜像模式对它们同样适用。 - 客户端代码中新增直连 `settings.describe` 调用即是预算回归;e2e 的失败信息会提示在 `ui-settings` 之外 grep 调用方。 diff --git a/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.i18n.yaml index 2cd0ed343b..bf8265d9a7 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.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/architecture/2026-08-18-session-history-and-event-transport.md -2026-08-18-session-history-and-event-transport.md: 5f4aba19d147eae3f49fcefc9a8006d0f557dc6c -2026-08-18-session-history-and-event-transport.zh.md: dcf7e7ffc5ad325756b1fb37dd5ad60d2f0b3147 +2026-08-18-session-history-and-event-transport.md: 3d7c1ae262cca410554bcb6b1a8af35686315ba7 +2026-08-18-session-history-and-event-transport.zh.md: cb1e02de580896a6278bb657e2097b823343ad0b diff --git a/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md b/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md index 5f4aba19d1..3d7c1ae262 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md +++ b/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md @@ -136,7 +136,7 @@ If a page request is canceled with its physical carrier generation, the journal `packages/api/session-controller` provides Host `ctx.sessionController` and the generated `ctx.remote.session` namespace. -It owns Session list, search, create, selectModel, rename, fork, prompt, attachment, updateQueue, cancel, page, follow, and control. The Host-generation model catalog is exposed separately through `llm.models` because it is not Session-specific. +It owns Session list, search, create, selectModel, rename, fork, prompt, attachment, updateQueue, cancel, page, follow, and control. The Host-generation model catalog is exposed separately through `session/modelCatalog` because it is not Session-specific. The package separates agent, commands, control, history, and list controllers internally, but Session identity resolution, activation policy, subagent ownership, and Remote error projection have one public owner. @@ -378,4 +378,4 @@ Remote waterfalls preserve first claim across multiple Clients, continuation of This decision extends the allowlist and single Cordis-signature design from [Remote event delivery](2026-08-10-remote-event-delivery.md): ordinary notifications use `emit`, while Agent-scoped async waterfalls use the same `ctx.remote.$on` surface with explicit `waterfall` mode. It creates no second invocation map. -This decision takes over the Session, Workspace, and Host-event carriers retained by [simple unary API Proxy migration](../../proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md) while preserving the complete jobs snapshot, process-local lifecycle, and “observation does not resume an Agent” semantics required by [background job display](../feature/2026-08-08-web-background-job-display.md). +This decision takes over the Session, Workspace, and Host-event carriers retained by [simple unary API Proxy migration](2026-08-10-unary-apiproxy-remote-migration.md) while preserving the complete jobs snapshot, process-local lifecycle, and “observation does not resume an Agent” semantics required by [background job display](../feature/2026-08-08-web-background-job-display.md). diff --git a/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.zh.md b/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.zh.md index dcf7e7ffc5..cb1e02de58 100644 --- a/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.zh.md @@ -136,7 +136,7 @@ repair 期间旧 window 保持可读;page 与期间积累的 live entries 拼 `packages/api/session-controller` 提供 Host `ctx.sessionController` 与生成的 `ctx.remote.session` namespace。 -它拥有 Session list、search、create、selectModel、rename、fork、prompt、attachment、updateQueue、cancel、page、follow 与 control。Host generation 的 model catalog 通过独立的 `llm.models` 公开,因为它不属于特定 Session。 +它拥有 Session list、search、create、selectModel、rename、fork、prompt、attachment、updateQueue、cancel、page、follow 与 control。Host generation 的 model catalog 通过独立的 `session/modelCatalog` 公开,因为它不属于特定 Session。 包内的 agent、commands、control、history 与 list controller 分开实现,但 Session 身份解析、激活策略、subagent ownership 和 Remote 错误投影只有一个公开 owner。 @@ -378,4 +378,4 @@ Remote waterfall 保留多 Client 首个 claim、全体 `next` 后继续 Host ch 本决定扩展[Remote 事件投递](2026-08-10-remote-event-delivery.zh.md)的 allowlist 与单一 Cordis 签名设计:普通通知继续使用 `emit`,Agent-scoped async waterfall 使用同一 `ctx.remote.$on` 面和显式 `waterfall` mode;不建立第二套 invocation map。 -本决定接管[简单一元 API Proxy 迁移](../../proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md)中保留的 Session、Workspace 与 Host event carrier,并保留[后台任务展示](../feature/2026-08-08-web-background-job-display.zh.md)所要求的完整 jobs snapshot、进程内生命周期和“观察不恢复 Agent”语义。 +本决定接管[简单一元 API Proxy 迁移](2026-08-10-unary-apiproxy-remote-migration.zh.md)中保留的 Session、Workspace 与 Host event carrier,并保留[后台任务展示](../feature/2026-08-08-web-background-job-display.zh.md)所要求的完整 jobs snapshot、进程内生命周期和“观察不恢复 Agent”语义。 diff --git a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.i18n.yaml index 6660f864d6..d8e1977615 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.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/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md -2026-08-25-session-observations-and-projection-owned-client-state.md: e47f2fc75ecbca51d01af077f6c6ab98f4e275f9 -2026-08-25-session-observations-and-projection-owned-client-state.zh.md: 527a4eb6b6765cba95d6067f2be60bff8f31a559 +2026-08-25-session-observations-and-projection-owned-client-state.md: 492640385215b059761b17a057328cc5c6d24bff +2026-08-25-session-observations-and-projection-owned-client-state.zh.md: 0b892a9cae2c999b4472dd46f19068e2b4139e60 diff --git a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md index e47f2fc75e..4926403852 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md +++ b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md @@ -114,7 +114,7 @@ The list view reads the same per-Session store as the opened Session. Hints can The per-Session Client projection store accepts list hints, the follow baseline, and later whole-value frames under one higher-sequence-wins rule. It never folds Session events. A baseline or frame may advance a hinted value, while an older cut cannot overwrite a newer row. -Data that is not derived from one Session remains outside projections. `llm.models` owns the Host-generation model catalog, and `agentPreset.list` owns the configurable preset roster. A selector combines the relevant catalog with the Session's `modelSelection` or `agentPreset` projection only when both inputs are ready. During refresh it may retain the last complete catalog; before the first complete pair it reports loading instead of rendering a guessed name or availability verdict. +Data that is not derived from one Session remains outside projections. `session/modelCatalog` owns the Host-generation model catalog, and `agentPresets/list` owns the configurable preset roster. A selector combines the relevant catalog with the Session's `modelSelection` or `agentPreset` projection only when both inputs are ready. During refresh it may retain the last complete catalog; before the first complete pair it reports loading instead of rendering a guessed name or availability verdict. Client-local interaction state also remains local: loading and error status, an open menu, an in-flight selection, and a staged choice for a not-yet-created Session are not replayable Session facts. Once a choice applies to a Session, its durable event and projection become authoritative. diff --git a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.zh.md b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.zh.md index 527a4eb6b6..0b892a9cae 100644 --- a/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.zh.md @@ -114,7 +114,7 @@ List view 与已打开 Session 读取同一个 per-Session store。Hints 可以 每个 Session 的 Client projection store 按一条 higher-sequence-wins 规则接收 list hints、follow baseline 和后续 whole-value frame。它从不折叠 Session event。Baseline 或 frame 可以推进 hinted value,较旧切面不能覆盖较新的 row。 -不由单个 Session 派生的数据不进入 projection。`llm.models` 拥有当前 Host generation 的 model catalog,`agentPreset.list` 拥有可配置 preset roster。Selector 只在相应 catalog 与 Session 的 `modelSelection` 或 `agentPreset` projection 均就绪后组合两者。刷新时可以保留上一份完整 catalog;第一次获得完整输入前显示 loading,而不是展示猜测的名称或可用性结论。 +不由单个 Session 派生的数据不进入 projection。`session/modelCatalog` 持有当前 Host generation 的 model catalog,`agentPresets/list` 持有可配置 preset roster。Selector 只在相应 catalog 与 Session 的 `modelSelection` 或 `agentPreset` projection 均就绪后组合两者。刷新时可以保留上一份完整 catalog;第一次获得完整输入前显示 loading,而不是展示猜测的名称或可用性结论。 Client 本地交互状态也继续留在本地:loading 和 error 状态、打开的菜单、进行中的选择,以及为尚未创建 Session 暂存的选择都不是可回放 Session 事实。选择一旦应用到 Session,其持久事件与 projection 就成为权威。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.i18n.yaml index cc3873f137..377d9ea1aa 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.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/bug-fix/2026-08-12-onboarding-reads-every-provider.md -2026-08-12-onboarding-reads-every-provider.md: 1f247a6c93257c24052f55eb4297ec3c9c3df06d -2026-08-12-onboarding-reads-every-provider.zh.md: fc6e43195a46eaea881f8b4bee3219b5e583b284 +2026-08-12-onboarding-reads-every-provider.md: 43895d6bc317f13ece91a48f9b44c0e8da705dc8 +2026-08-12-onboarding-reads-every-provider.zh.md: 67f12dd9e90435f61776d0eac048d588fcdd7252 diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.md b/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.md index 1f247a6c93..43895d6bc3 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.md +++ b/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.md @@ -24,7 +24,7 @@ Each card kind now owns its own close handler. `closeSetup` records the provider ## Alternatives considered -- **Deriving readiness from the model catalog (`llm.models`) instead of the join.** It answers "can the user talk to something" most directly, but it costs a per-provider listing round trip on a surface that already holds the join, and a provider whose listing fails transiently would re-open onboarding. +- **Deriving readiness from the model catalog (`session/modelCatalog`) instead of the join.** It answers "can the user talk to something" most directly, but it costs a per-provider listing round trip on a surface that already holds the join, and a provider whose listing fails transiently would re-open onboarding. - **Requiring `row.configured` in `providerUsable`.** It reads as the stricter check, and would exclude exactly the routes a deployment mounts through `cordis.yml` without a configurable-provider declaration — live routes serving models that this page cannot configure. Registration, not configurability, is what makes a provider usable. - **Only adding the dismissal, leaving the card auto-opening.** It fixes the Cancel button and nothing else: a user with a working provider would still be handed the DeepSeek form on every visit to Models, which is the same misreading in a quieter form. - **Persisting the dismissal to settings.** A durable "do not ask about DeepSeek" flag is a second fact about first-run state that can disagree with the join. The credential itself already ends the posture permanently, and every other card on this page is session-local. diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.zh.md index fc6e43195a..67f12dd9e9 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-12-onboarding-reads-every-provider.zh.md @@ -24,7 +24,7 @@ Status: implemented ## Alternatives considered -- **从模型目录(`llm.models`)而非联接推导就绪状态。** 它最直接地回答「用户有没有能对话的东西」,但会在一个已经持有联接的界面上多花每提供方一次列举往返,而且某个提供方列举的瞬时失败会让引导重新弹出。 +- **从模型目录(`session/modelCatalog`)而非联接推导就绪状态。** 它最直接地回答「用户有没有能对话的东西」,但会在一个已经持有联接的界面上多花每提供方一次列举往返,而且某个提供方列举的瞬时失败会让引导重新弹出。 - **在 `providerUsable` 中要求 `row.configured`。** 它读起来更严格,却会恰好排除部署通过 `cordis.yml` 挂载、没有可配置提供方声明的那些路由——它们是正在提供模型、只是这个页面配置不了的存活路由。使一个提供方可用的是注册,不是可配置性。 - **只加关闭状态,保留卡片自动展开。** 那只修好取消按钮,别的什么都没修:已有可用提供方的用户每次进入 Models 仍会被塞一张 DeepSeek 表单,那是同一个误读的安静版本。 - **把关闭状态持久化到 settings。** 一个「别再问 DeepSeek」的持久标志,是关于首次运行状态的第二个事实,可能与联接互相矛盾。凭据本身已经永久结束该姿态,而这个页面上其他每一张卡片都是会话内的。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.i18n.yaml index bcd28b5585..762dc3711f 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.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/bug-fix/2026-08-18-tool-row-file-open-failure.md -2026-08-18-tool-row-file-open-failure.md: e36552395b992e688fad35b3163b92c9f6189e43 -2026-08-18-tool-row-file-open-failure.zh.md: f09c41585a538b408f6258f64583f63c0fa57b0d +2026-08-18-tool-row-file-open-failure.md: c1887c834933199d5acf9035632cc83d470777d7 +2026-08-18-tool-row-file-open-failure.zh.md: 8a851b4b1bd40c66de92316967df612f828c8b38 diff --git a/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.md b/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.md index e36552395b..c1887c8349 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.md +++ b/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.md @@ -6,7 +6,7 @@ English | [中文](2026-08-18-tool-row-file-open-failure.zh.md) ## Problem -Tool-row path clicks already call `host.openPath` through the chat view's injected `openFile`. The inject swallowed every Host or OS refusal, so a missing desktop opener, a remote or non-loopback carrier, or a path the Host cannot hand off left the row looking successful. The reader had no reason and no second try. +Tool-row path clicks already call `session/openWorkspacePath` through the chat view's injected `openFile`. The inject swallowed every Host or OS refusal, so a missing desktop opener, a remote or non-loopback carrier, or a path the Host cannot hand off left the row looking successful. The reader had no reason and no second try. The [file-open-in-OS decision](../feature/2026-07-28-tool-call-file-open-in-os.md) still owns the link gesture and the Host handoff. This note owns only the refusal. @@ -16,7 +16,7 @@ The inject returns the `workspaces.openPath` promise. The chat view wraps that o The dialog lives on the view that owns the Host call, not on each tool row. Produced-file chips and closing-message mentions use the same wrapper because they already share that opener. The produced-files folder action opens `.`, and that refusal uses the folder title and unknown-open copy. -The Host message is shown as thrown. `WorkspaceRuntime.openPath` prefixes `path open failed: ` onto the wire error; the dialog does not unwrap that prefix. +The Host message is shown as thrown. The chat view's `openFile` adapter prefixes `path open failed: ` onto the Remote error; the dialog does not unwrap that prefix. ## Alternatives considered @@ -30,4 +30,4 @@ A silent Host refusal is no longer a success from the reader's seat. Headless or ## Testing -Package specs cover inject rejection, the dialog copy (Error, non-Error, empty, workspace folder), retry of the same path, cancel, and a settlement that arrives after dismiss. `apps/web/tests/seeded-history.e2e.ts` stubs `host.openPath` to fail over a cold-resumed read row, pins the assembled dialog in `file-open-failure.expected.md`, and asserts the English reason plus a second call with the same payload. +Package specs cover inject rejection, the dialog copy (Error, non-Error, empty, workspace folder), retry of the same path, cancel, and a settlement that arrives after dismiss. `apps/web/tests/seeded-history.e2e.ts` stubs `session/openWorkspacePath` to fail over a cold-resumed read row, pins the assembled dialog in `file-open-failure.expected.md`, and asserts the English reason plus a second call with the same payload. diff --git a/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.zh.md b/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.zh.md index f09c41585a..8a851b4b1b 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-18-tool-row-file-open-failure.zh.md @@ -6,7 +6,7 @@ Status: implemented ## 问题 -工具行路径点击已经通过聊天视图注入的 `openFile` 调用 `host.openPath`。inject 吞掉了每一次 Host 或操作系统拒绝,因此缺少桌面打开器、远程或非回环载体、或 Host 无法交接的路径,都会让该行看起来像成功。读者看不到原因,也无法再试一次。 +工具行路径点击已经通过聊天视图注入的 `openFile` 调用 `session/openWorkspacePath`。inject 吞掉了每一次 Host 或操作系统拒绝,因此缺少桌面打开器、远程或非回环载体、或 Host 无法交接的路径,都会让该行看起来像成功。读者看不到原因,也无法再试一次。 [用系统应用打开文件的决策](../feature/2026-07-28-tool-call-file-open-in-os.zh.md) 仍然拥有链接手势和 Host 交接。本 Agent Note 只拥有拒绝路径。 @@ -16,7 +16,7 @@ inject 返回 `workspaces.openPath` 的 promise。聊天视图包装该打开器 对话框位于 chat 视图(拥有 Host 调用),而不是每个工具行。产物文件标签和收尾消息中的提及已经共用该打开器,因此走同一包装。产物文件的文件夹操作打开 `.`,该拒绝使用文件夹标题和未知打开回退文案。 -Host 消息按抛出内容展示。`WorkspaceRuntime.openPath` 会在 wire 错误前加上 `path open failed: ` 前缀;对话框不拆掉该前缀。 +Host 消息按抛出内容展示。聊天视图的 `openFile` adapter 会在 Remote 错误前加上 `path open failed: ` 前缀;对话框不拆掉该前缀。 ## 考虑过的替代方案 @@ -30,4 +30,4 @@ Host 消息按抛出内容展示。`WorkspaceRuntime.openPath` 会在 wire 错 ## 测试 -包测试覆盖 inject 拒绝、对话框文案(Error、非 Error、空文本、工作区文件夹)、同一路径重试、取消,以及关闭之后才落到的结果。`apps/web/tests/seeded-history.e2e.ts` 在冷恢复的 read 行上把 `host.openPath` stub 为失败,用 `file-open-failure.expected.md` 钉住组装后的对话框,并断言英文原因以及对同一 payload 的第二次调用。 +包测试覆盖 inject 拒绝、对话框文案(Error、非 Error、空文本、工作区文件夹)、同一路径重试、取消,以及关闭之后才落到的结果。`apps/web/tests/seeded-history.e2e.ts` 在冷恢复的 read 行上把 `session/openWorkspacePath` stub 为失败,用 `file-open-failure.expected.md` 钉住组装后的对话框,并断言英文原因以及对同一 payload 的第二次调用。 diff --git a/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.i18n.yaml index f8d99db60e..ea9bcff6d2 100644 --- a/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.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-07-28-skill-invocation-policy.md -2026-07-28-skill-invocation-policy.md: 7a4f83ecae3dea82ab6864e748c9d8b8599f6bfc -2026-07-28-skill-invocation-policy.zh.md: 8d66af69b7f45c5d76d18963df6921eaec2e1e8c +2026-07-28-skill-invocation-policy.md: 918283bd028ec75faee965c35dfac494e9ceafad +2026-07-28-skill-invocation-policy.zh.md: ddf3a334561d2077c07f479792228a353cb926c0 diff --git a/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.md b/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.md index 7a4f83ecae..918283bd02 100644 --- a/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.md +++ b/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.md @@ -18,7 +18,7 @@ The local parser also exposed an internal camel-case spelling as frontmatter. Su The local provider accepts the exact kebab-case frontmatter keys `disable-model-invocation` and `user-invocable`. It accepts YAML booleans plus case-insensitive `true`/`false`, `yes`/`no`, `on`/`off`, and `1`/`0`, matching the practical boolean forms accepted by Claude skills. It maps `disable-model-invocation` to the inverse positive field and fills both positive fields from their defaults even when neither key is present. A camel-case external spelling or non-boolean invocation value drops the entire skill from discovery with a targeted warning; this pre-release repository does not keep an on-disk compatibility alias. Invocation data fails closed because ignoring it would default to permission and could expose the skill on a disabled surface, while wrong-typed optional `whenToUse` and `metadata` values are omitted because they do not decide invocation. -The model-facing `dsh-tool-skill` catalog and loader enforce `isModelInvocable`. The TUI `/skill:` autocomplete and exact loader enforce the user field locally, so a user-only skill is visible and loadable there even when it is absent from model discovery, without turning the optional skill peer into a runtime import. The launcher-seeded initial skill used by guided `dsh migrate` and `dsh upgrade` sessions follows this same TUI path and must remain user-invocable. The browser `skill.list` RPC serves a user-selected reference that still asks the model to load the skill, so it exposes the intersection of model- and user-invocable skills; no direct browser skill-loading RPC is added. +The model-facing `dsh-tool-skill` catalog and loader enforce `isModelInvocable`. The TUI `/skill:` autocomplete and exact loader enforce the user field locally, so a user-only skill is visible and loadable there even when it is absent from model discovery, without turning the optional skill peer into a runtime import. The launcher-seeded initial skill used by guided `dsh migrate` and `dsh upgrade` sessions follows this same TUI path and must remain user-invocable. The browser `skills/list` RPC serves a user-selected reference that still asks the model to load the skill, so it exposes the intersection of model- and user-invocable skills; no direct browser skill-loading RPC is added. These rules permit all four combinations: diff --git a/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.zh.md b/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.zh.md index 8d66af69b7..ddf3a33456 100644 --- a/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.zh.md @@ -18,7 +18,7 @@ skill 注册表最初将发现操作视为模型目录:`ctx.skills.list()` 会 本地提供方只接受拼写完全一致的 kebab-case frontmatter 键 `disable-model-invocation` 和 `user-invocable`。它接受 YAML 布尔值,以及不区分大小写的 `true`/`false`、`yes`/`no`、`on`/`off` 和 `1`/`0`,与 Claude skills 实际支持的布尔写法一致。它将 `disable-model-invocation` 映射为相反的正向字段,即使两个键都不存在,也会根据默认值填充两个正向字段。若使用外部驼峰式拼写或提供非布尔调用值,发现流程会丢弃整个 skill,并给出有针对性的警告;本仓库尚处于发布前阶段,因此不为磁盘格式保留兼容别名。调用数据校验遵循失败时默认拒绝原则,因为忽略这类数据会默认授予权限,可能使 skill 暴露在已禁用的接口上;与之不同,类型错误的可选 `whenToUse` 和 `metadata` 值会被省略,因为它们不参与调用判定。 -面向模型的 `dsh-tool-skill` 目录和 loader 执行 `isModelInvocable`。TUI 的 `/skill:` 自动补全与精确名称 loader 在本地执行用户字段,因此仅允许用户调用的 skill 即使不出现在模型发现结果中,仍会在此处显示并可加载,同时不会将可选的 skill 对等依赖(peer dependency)变成运行时导入。由 launcher 预置、供引导式 `dsh migrate` 和 `dsh upgrade` 会话使用的初始 skill 沿用同一条 TUI 路径,因此必须保持允许用户调用。浏览器的 `skill.list` RPC 提供的是由用户选择、但仍要求模型加载的引用,因此只公开同时允许模型和用户调用的 skill;本次改动不新增让浏览器直接加载 skill 的 RPC。 +面向模型的 `dsh-tool-skill` 目录和 loader 执行 `isModelInvocable`。TUI 的 `/skill:` 自动补全与精确名称 loader 在本地执行用户字段,因此仅允许用户调用的 skill 即使不出现在模型发现结果中,仍会在此处显示并可加载,同时不会将可选的 skill 对等依赖(peer dependency)变成运行时导入。由 launcher 预置、供引导式 `dsh migrate` 和 `dsh upgrade` 会话使用的初始 skill 沿用同一条 TUI 路径,因此必须保持允许用户调用。浏览器的 `skills/list` RPC 提供的是由用户选择、但仍要求模型加载的引用,因此只公开同时允许模型和用户调用的 skill;本次改动不新增让浏览器直接加载 skill 的 RPC。 这些规则允许以下四种组合: diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml index 824846180a..e97143a911 100644 --- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.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-07-28-tool-call-file-open-in-os.md -2026-07-28-tool-call-file-open-in-os.md: e6e590b2a97654b8b68d5a9842de10818f544088 -2026-07-28-tool-call-file-open-in-os.zh.md: eb600a69cb2a2c3cc0d7463519d3de4dce76047b +2026-07-28-tool-call-file-open-in-os.md: c8ede5c5c2fbdc9797edd9bf80673442c39873c2 +2026-07-28-tool-call-file-open-in-os.zh.md: 1e51dd4dced25bd14272358199baef82f396635a diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md index e6e590b2a9..c8ede5c5c2 100644 --- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md +++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md @@ -10,9 +10,9 @@ Chat tool rows treated the whole summary line as a click target that opened the ## Decision -File-tool path summaries (`read` / `write` / `edit` args carrying `path` or `file_path`) render as links underlined at rest with a pointer cursor. Clicking the path calls `host.openPath` through `WorkspaceRuntime.openPath`, resolving relative paths against the session cwd. File-link rows disable args expand (leading icon is inert); whole-row click, row hover fill, and the click-to-open-details gesture are removed from tool rows (including bash and todo registrations). The details panel and its inject surface remain for programmatic selection; rows no longer drive them. +File-tool path summaries (`read` / `write` / `edit` args carrying `path` or `file_path`) render as links underlined at rest with a pointer cursor. Clicking the path calls `session/openWorkspacePath` through the chat view's `openFile` injection; the Host resolves relative paths against the addressed Session's cwd. File-link rows disable args expand (leading icon is inert); whole-row click, row hover fill, and the click-to-open-details gesture are removed from tool rows (including bash and todo registrations). The details panel and its inject surface remain for programmatic selection; rows no longer drive them. -`host.openPath` is a privileged unary RPC accepted only from loopback, same-origin browser requests (same carrier guard as `host.pickDirectory`). Platform adapters open without a shell: `open` on macOS, PowerShell `Invoke-Item` on Windows, and `xdg-open` on desktop Linux; browser-renderable documents prefer the named default browser on macOS and desktop Linux. WSL is a separate host shape despite Node reporting `linux`: the adapter recognizes its environment or Microsoft kernel release, translates the Linux path with `wslpath -w`, and passes the resulting Windows/UNC path to the same PowerShell handoff. The opener's platform facts and command runner are injectable for tests. URL-only read args (`web_fetch`) are not file links. +`session/openWorkspacePath` uses the authenticated Remote carrier, while the product UI offers the gesture only on a loopback page whose `host.describe.canOpenPath` is true. Platform adapters open without a shell: `open` on macOS, PowerShell `Invoke-Item` on Windows, and `xdg-open` on desktop Linux; browser-renderable documents prefer the named default browser on macOS and desktop Linux. WSL is a separate host shape despite Node reporting `linux`: the adapter recognizes its environment or Microsoft kernel release, translates the Linux path with `wslpath -w`, and passes the resulting Windows/UNC path to the same PowerShell handoff. The opener's platform facts and command runner are injectable for tests. URL-only read args (`web_fetch`) are not file links. ## Alternatives considered @@ -23,7 +23,7 @@ File-tool path summaries (`read` / `write` / `edit` args carrying `path` or `fil ## Consequences -Clicking a file path in a tool row opens that path on the host. Non-file tool rows are inert summaries (expand toggles remain where the row already supported them). The Client withholds `host.openPath` on non-loopback pages; every exposed Host invocation still requires the browser session. A Host or OS refusal is owned by the chat view: it shows the thrown reason and retries the same path ([file-open failure](../bug-fix/2026-08-18-tool-row-file-open-failure.md)). +Clicking a file path in a tool row opens that path on the host. Non-file tool rows are inert summaries (expand toggles remain where the row already supported them). The Client withholds the file-open gesture on non-loopback pages; every exposed Host invocation still requires the browser session. A Host or OS refusal is owned by the chat view: it shows the thrown reason and retries the same path ([file-open failure](../bug-fix/2026-08-18-tool-row-file-open-failure.md)). ## Risks diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md index eb600a69cb..1e51dd4dce 100644 --- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md @@ -10,9 +10,9 @@ Status: implemented ## 决策 -文件工具的路径摘要(`read`/`write`/`edit` 参数中的 `path` 或 `file_path`)渲染为静止状态下即带下划线的链接,并使用 pointer 光标。点击路径会经 `WorkspaceRuntime.openPath` 调用 `host.openPath`,相对路径以会话 cwd 为基准解析。带文件链接的行关闭参数展开(左侧图标不可点);工具行(含 bash 与 todo 注册)去掉整行点击、整行悬停底色,以及点击打开 details 的手势。details 面板及其 inject 面仍保留供程序化选择;工具行不再驱动它们。 +文件工具的路径摘要(`read`/`write`/`edit` 参数中的 `path` 或 `file_path`)渲染为静止状态下即带下划线的链接,并使用 pointer 光标。点击路径会经聊天视图的 `openFile` injection 调用 `session/openWorkspacePath`;Host 以目标 Session 的 cwd 为基准解析相对路径。带文件链接的行关闭参数展开(左侧图标不可点);工具行(含 bash 与 todo 注册)去掉整行点击、整行悬停底色,以及点击打开 details 的手势。details 面板及其 inject 面仍保留供程序化选择;工具行不再驱动它们。 -`host.openPath` 是一元 RPC,与每个 Host API 方法一样要求通过 Host/Origin 校验和浏览器会话认证。平台适配器不经 shell 打开:macOS 为 `open`,Windows 为 PowerShell `Invoke-Item`,桌面 Linux 为 `xdg-open`;浏览器可渲染的文档会在 macOS 与桌面 Linux 上优先使用指定的默认浏览器。尽管 Node 将 WSL 报告为 `linux`,WSL 仍是一种独立的宿主形态:适配器根据其环境或 Microsoft 内核 release 识别它,用 `wslpath -w` 转换 Linux 路径,并将所得 Windows/UNC 路径交给同一 PowerShell 交接。打开器的平台信息和命令运行器可在测试中注入。仅含 URL 的 read 参数(`web_fetch`)不是文件链接。 +`session/openWorkspacePath` 使用经过认证的 Remote carrier,而产品 UI 只在 loopback 页面且 `host.describe.canOpenPath` 为 true 时提供该手势。平台适配器不经 shell 打开:macOS 为 `open`,Windows 为 PowerShell `Invoke-Item`,桌面 Linux 为 `xdg-open`;浏览器可渲染的文档会在 macOS 与桌面 Linux 上优先使用指定的默认浏览器。尽管 Node 将 WSL 报告为 `linux`,WSL 仍是一种独立的宿主形态:适配器根据其环境或 Microsoft 内核 release 识别它,用 `wslpath -w` 转换 Linux 路径,并将所得 Windows/UNC 路径交给同一 PowerShell 交接。打开器的平台信息和命令运行器可在测试中注入。仅含 URL 的 read 参数(`web_fetch`)不是文件链接。 ## 考虑过的替代方案 @@ -23,7 +23,7 @@ Status: implemented ## 后果 -点击工具行中的文件路径会在宿主上打开该路径。非文件工具行只是不可交互的摘要(行内已有的展开开关仍保留)。Client 在非 loopback 页面不提供 `host.openPath`;每次已暴露的 Host 调用仍要求浏览器会话。Host 或操作系统拒绝由聊天视图拥有:它展示抛出的原因,并对同一路径提供重试([打开失败](../bug-fix/2026-08-18-tool-row-file-open-failure.zh.md))。 +点击工具行中的文件路径会在宿主上打开该路径。非文件工具行只是不可交互的摘要(行内已有的展开开关仍保留)。Client 在非 loopback 页面不提供文件打开手势;每次已暴露的 Host 调用仍要求浏览器会话。Host 或操作系统拒绝由聊天视图拥有:它展示抛出的原因,并对同一路径提供重试([打开失败](../bug-fix/2026-08-18-tool-row-file-open-failure.zh.md))。 ## 风险 diff --git a/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.i18n.yaml index 00ee8cb309..fbddedc9e1 100644 --- a/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.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-07-30-deepseek-onboarding-credential-setup.md -2026-07-30-deepseek-onboarding-credential-setup.md: 87533e7a55f9b1f05f6a4ba58c3c9888780c158c -2026-07-30-deepseek-onboarding-credential-setup.zh.md: 0b445d6eccdd9aa1651b64f084a96d4d674a4f12 +2026-07-30-deepseek-onboarding-credential-setup.md: c2e9a1251a4666d9b7109d284a1588640d630332 +2026-07-30-deepseek-onboarding-credential-setup.zh.md: e94f55949eb472d8deb524c33e9760155db0ddcf diff --git a/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.md b/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.md index 87533e7a55..c2e9a1251a 100644 --- a/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.md +++ b/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.md @@ -10,7 +10,7 @@ The [web configuration plane](../architecture/2026-07-30-web-config-plane.md) ma ## Decision -**One readiness projection owns both Models and onboarding facts.** `ui-settings-models` keeps a single store that joins `llm.providers({})`, the redacted namespace views held by the shared settings describe mirror, and batched `credentials.describe({refs})`. The onboarding projection selects the `deepseek-official` configurable-provider entry owned by the `llm-deepseek` namespace and empty settings path, reads the effective `apiKeyEnv`, and evaluates the matching credential descriptor. A live route with the same provider id but no matching configurable-provider declaration is adapter-absent for onboarding. A configured process-environment credential is ready and remains read-only. The later [settings describe mirror decision](../architecture/2026-08-17-settings-describe-mirror.md) owns that settings read and its invalidation ordering. +**One readiness projection owns both Models and onboarding facts.** `ui-settings-models` keeps a single store that joins `llm/listProviders`, `llm/listConfigurableProviders`, the redacted namespace views held by the shared settings describe mirror, and batched `credentials/describe`. The onboarding projection selects the `deepseek-official` configurable-provider entry owned by the `llm-deepseek` namespace and empty settings path, reads the effective `apiKeyEnv`, and evaluates the matching credential descriptor. A live route with the same provider id but no matching configurable-provider declaration is adapter-absent for onboarding. A configured process-environment credential is ready and remains read-only. The later [settings describe mirror decision](../architecture/2026-08-17-settings-describe-mirror.md) owns that settings read and its invalidation ordering. **The settings shell contributes ordering, not provider policy.** `ui-settings` declares a root-scoped `settings.onboarding` list slot and mounts one ordered step at a time while the current surface is the empty Hero. The active registrant receives `complete()` and a private `openSection(id)` callback; completion transfers ownership to the next entry. `ui-settings-models` registers the DeepSeek step, the preceding welcome notice, and its Models section through `slots.inject()`, so every contribution follows one client Cordis plugin's lifecycle and the dialogs cannot stack. Their common presentation is owned by the [shared-modal onboarding decision](2026-08-13-shared-modal-product-onboarding.md). diff --git a/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.zh.md b/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.zh.md index 0b445d6ecc..e94f55949e 100644 --- a/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.zh.md @@ -10,7 +10,7 @@ Status: implemented ## 决策 -**Models 与首次使用引导共享同一个就绪状态投影。**`ui-settings-models` 维护一个 store,把 `llm.providers({})`、共享 settings describe 镜像持有的已脱敏 namespace views 和批量调用的 `credentials.describe({refs})` 联接为同一份状态。首次使用投影选取由 `llm-deepseek` namespace 与空 settings path 持有的 `deepseek-official` 可配置提供方条目,读取生效的 `apiKeyEnv`,并检查对应的凭据描述符。同 provider id 但没有匹配可配置提供方声明的存活路由,在首次使用引导中视为适配器缺失。通过进程环境提供的凭据若已配置,则判定为就绪并保持只读。后续的 [settings describe 镜像决策](../architecture/2026-08-17-settings-describe-mirror.zh.md)持有这次 settings 读取及其失效顺序。 +**Models 与首次使用引导共享同一个就绪状态投影。**`ui-settings-models` 维护一个 store,把 `llm/listProviders`、`llm/listConfigurableProviders`、共享 settings describe 镜像持有的已脱敏 namespace views 和批量调用的 `credentials/describe` 联接为同一份状态。首次使用投影选取由 `llm-deepseek` namespace 与空 settings path 持有的 `deepseek-official` 可配置提供方条目,读取生效的 `apiKeyEnv`,并检查对应的凭据描述符。同 provider id 但没有匹配可配置提供方声明的存活路由,在首次使用引导中视为适配器缺失。通过进程环境提供的凭据若已配置,则判定为就绪并保持只读。后续的 [settings describe 镜像决策](../architecture/2026-08-17-settings-describe-mirror.zh.md)持有这次 settings 读取及其失效顺序。 **设置外壳只贡献排序,不持有提供方策略。** `ui-settings` 声明一个根作用域的 `settings.onboarding` list slot,并在当前界面为空白 Hero 时,每次只挂载一个有序步骤。当前注册方会收到 `complete()` 和私有 `openSection(id)` 回调;完成当前步骤后,所有权转交给下一项。`ui-settings-models` 通过 `slots.inject()` 注册 DeepSeek 步骤、排在它之前的欢迎声明及 Models 分区,因此所有贡献都跟随同一个 client Cordis 插件的生命周期,两个弹窗也无法堆叠。它们的共用展示由[共用弹窗引导决策](2026-08-13-shared-modal-product-onboarding.zh.md)持有。 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml index 902227d012..5a5c329383 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.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-07-31-web-workspace-file-links.md -2026-07-31-web-workspace-file-links.md: 611f012e0201fa9002ea473ef9a107841bf83bcc -2026-07-31-web-workspace-file-links.zh.md: 6e8806ab41de88bf59e4f7bf642f78c3bfe50cb3 +2026-07-31-web-workspace-file-links.md: ddc093e1ce5e646f6b96f1517b2a26c9e10fe423 +2026-07-31-web-workspace-file-links.zh.md: f463de969bab7a47145abbbd46363b8807bcc673 diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md index 611f012e02..ddc093e1ce 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md @@ -10,7 +10,7 @@ English | [中文](2026-07-31-web-workspace-file-links.zh.md) A web session that produced a file had no way to look at it. The agent wrote `deepseek-homepage.html`, said so, and the user's only recourse was to copy an absolute path like `/private/tmp/dsh-client-hotplug.ygPvsm/workspaces/plugin-hotplug/deepseek-homepage.html` into a terminal. -Two distinct defects sat behind that. The transcript never said what a turn had produced: `ToolCallView.locations` — the follow-along vocabulary the file tools already populate — had no consumer in the client, so a reader's only account of the output was whatever the closing message happened to spell. And the affordance that did exist was invisible: `ToolRow` already renders a mutation or read row's path as a real button wired to `host.openPath`, but styled exactly like the surrounding prose and underlined only on hover, so nobody found it. The reported "I can't open what it made" was a discoverability failure sitting on top of a working capability. +Two distinct defects sat behind that. The transcript never said what a turn had produced: `ToolCallView.locations` — the follow-along vocabulary the file tools already populate — had no consumer in the client, so a reader's only account of the output was whatever the closing message happened to spell. And the affordance that did exist was invisible: `ToolRow` already renders a mutation or read row's path as a real button wired to `session/openWorkspacePath`, but styled exactly like the surrounding prose and underlined only on hover, so nobody found it. The reported "I can't open what it made" was a discoverability failure sitting on top of a working capability. ## Decision @@ -18,7 +18,7 @@ Two distinct defects sat behind that. The transcript never said what a turn had **The path link reads as a link.** Underlined at rest, not only on hover. This is the smaller half of the diff and the larger half of the fix. -**Opening stays the Host's job, and prefers the default browser.** `host.openPath` hands the path to the operating system, which yields a `file://` document in a real browser: full page capabilities, and no reachability into `/api`, because a `file://` document is not same-origin with it. Measured on the reported artifact: `localStorage` works, the theme toggle flips, the tabs switch, and `fetch` to the API fails. For documents a browser renders — `.html`, `.htm`, `.xhtml`, `.svg` — the opener resolves the default *browser* rather than the type's default application when the platform can name one, because a developer who binds `.html` to an editor would otherwise click a produced page and get source code. macOS reads the LaunchServices `https` handler and desktop Linux reads `$BROWSER`; either falls back to the default application when no browser can be named. Windows uses its registered association, and WSL first translates the path before using that same Windows handoff. When files are hidden, **Show in folder** passes `.` through the same owner `openFile`; it appears only for a loopback page whose current `host.describe.canOpenPath` permits native opening. Other deployments omit it, with `nativeOpen: false` available when desktop detection would be a false positive. +**Opening stays the Host's job, and prefers the default browser.** `session/openWorkspacePath` hands the path to the operating system, which yields a `file://` document in a real browser: full page capabilities, and no reachability into `/api`, because a `file://` document is not same-origin with it. Measured on the reported artifact: `localStorage` works, the theme toggle flips, the tabs switch, and `fetch` to the API fails. For documents a browser renders — `.html`, `.htm`, `.xhtml`, `.svg` — the opener resolves the default *browser* rather than the type's default application when the platform can name one, because a developer who binds `.html` to an editor would otherwise click a produced page and get source code. macOS reads the LaunchServices `https` handler and desktop Linux reads `$BROWSER`; either falls back to the default application when no browser can be named. Windows uses its registered association, and WSL first translates the path before using that same Windows handoff. When files are hidden, **Show in folder** passes `.` through the same owner `openFile`; it appears only for a loopback page whose current `host.describe.canOpenPath` permits native opening. Other deployments omit it, with `nativeOpen: false` available when desktop detection would be a false positive. **Serving workspace files over HTTP is out of scope, and so are non-local clients.** Serving files from the harness itself — same-origin with `/api`, behind `CSP: sandbox`, or from a second listener whose own port gives served documents their own origin — was rejected with the product scope: previews for a browser that is not on the Host machine are not supported, so the Host opener answers the supported case completely and the HTTP machinery would answer only the unsupported one. diff --git a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md index 6e8806ab41..f463de969b 100644 --- a/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md @@ -10,7 +10,7 @@ Status: implemented 一个产出了文件的 web 会话,没有办法看到那个文件。agent(智能体)写出了 `deepseek-homepage.html` 并如实告知,而用户唯一的办法是把 `/private/tmp/dsh-client-hotplug.ygPvsm/workspaces/plugin-hotplug/deepseek-homepage.html` 这样的绝对路径复制进终端。 -这背后是两个不同的缺陷。transcript(文本记录)从不说明一个轮次产出了什么:`ToolCallView.locations`——文件工具早已填好的跟随文件词汇——在客户端没有任何消费方,因此读者对产出的唯一交代,就是收尾消息恰好拼出来的那点内容。而已经存在的那个交互是隐形的:`ToolRow` 早已把改写行或读取行的路径渲染成一个接到 `host.openPath` 的真按钮,但它的样式与周围正文一模一样、只有悬停才有下划线,于是没人发现。所报告的「做完了打不开」,是一个可发现性失败叠在一项本就可用的能力之上。 +这背后是两个不同的缺陷。transcript(文本记录)从不说明一个轮次产出了什么:`ToolCallView.locations`——文件工具早已填好的跟随文件词汇——在客户端没有任何消费方,因此读者对产出的唯一交代,就是收尾消息恰好拼出来的那点内容。而已经存在的那个交互是隐形的:`ToolRow` 早已把改写行或读取行的路径渲染成一个接到 `session/openWorkspacePath` 的真按钮,但它的样式与周围正文一模一样、只有悬停才有下划线,于是没人发现。所报告的「做完了打不开」,是一个可发现性失败叠在一项本就可用的能力之上。 ## 决策 @@ -18,7 +18,7 @@ Status: implemented **路径链接读得出是链接。** 静止状态下就带下划线,而不只在悬停时。这是本次改动中更小的那一半,却是修复中更大的那一半。 -**打开仍然是 Host 的职责,并且优先选用默认浏览器。** `host.openPath` 把路径交给操作系统,得到的是真实浏览器里的一份 `file://` 文档:页面能力完整,且够不到 `/api`——因为 `file://` 文档与它并不同源。在所报告的那份产物上实测:`localStorage` 可用、主题切换生效、tabs 可切换,而对 API 的 `fetch` 失败。对浏览器能渲染的文档——`.html`、`.htm`、`.xhtml`、`.svg`——平台能够确定默认浏览器时,打开器解析的是默认**浏览器**而非该类型的默认应用,因为把 `.html` 绑给编辑器的开发者,否则点开一个产出的页面得到的会是源码。macOS 读取 LaunchServices 的 `https` 处理程序,桌面 Linux 读取 `$BROWSER`;无法确定浏览器时,两者都会回退到默认应用。Windows 使用其注册的文件关联,WSL 则先转换路径,再使用同一 Windows 交接。存在隐藏文件时,**在文件夹中显示**会把 `.` 经由同一 owner `openFile` 传递;它只在 loopback 页面的当前 `host.describe.canOpenPath` 允许原生打开时出现。其他部署会省略它;桌面探测误报时可配置 `nativeOpen: false`。 +**打开仍然是 Host 的职责,并且优先选用默认浏览器。** `session/openWorkspacePath` 把路径交给操作系统,得到的是真实浏览器里的一份 `file://` 文档:页面能力完整,且够不到 `/api`——因为 `file://` 文档与它并不同源。在所报告的那份产物上实测:`localStorage` 可用、主题切换生效、tabs 可切换,而对 API 的 `fetch` 失败。对浏览器能渲染的文档——`.html`、`.htm`、`.xhtml`、`.svg`——平台能够确定默认浏览器时,打开器解析的是默认**浏览器**而非该类型的默认应用,因为把 `.html` 绑给编辑器的开发者,否则点开一个产出的页面得到的会是源码。macOS 读取 LaunchServices 的 `https` 处理程序,桌面 Linux 读取 `$BROWSER`;无法确定浏览器时,两者都会回退到默认应用。Windows 使用其注册的文件关联,WSL 则先转换路径,再使用同一 Windows 交接。存在隐藏文件时,**在文件夹中显示**会把 `.` 经由同一 owner `openFile` 传递;它只在 loopback 页面的当前 `host.describe.canOpenPath` 允许原生打开时出现。其他部署会省略它;桌面探测误报时可配置 `nativeOpen: false`。 **以 HTTP 提供工作区文件不在范围内,非本机客户端亦然。** 由 harness 自己提供文件——与 `/api` 同源、置于 `CSP: sandbox` 之后、或交给一个以自身端口给所服务文档独立源的第二监听器——随产品范围一并否决:不为「浏览器不在 Host 机器上」的场景提供预览,因此 Host 打开器完整回答受支持的场景,而那套 HTTP 机制只会回答不受支持的那个。 diff --git a/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.i18n.yaml b/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.i18n.yaml index 264571616c..9740b305f9 100644 --- a/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.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-08-user-explicit-skill-invocation.md -2026-08-08-user-explicit-skill-invocation.md: 6e70c67f771afeec881bdd8ad5c5322cc9190dfe -2026-08-08-user-explicit-skill-invocation.zh.md: 00c76ab99a2074b5d7d84c870afd9a9190ec45df +2026-08-08-user-explicit-skill-invocation.md: 87f4d04fb4bc34cba2ecb5e362d9a86270ab5195 +2026-08-08-user-explicit-skill-invocation.zh.md: 7b1aa18808054edc38b6801e028de1f0f0605d4a diff --git a/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.md b/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.md index 6e70c67f77..87f4d04fb4 100644 --- a/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.md +++ b/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.md @@ -6,7 +6,7 @@ English | [中文](2026-08-08-user-explicit-skill-invocation.zh.md) ## Problem -A `disable-model-invocation: true` skill is user-only by design: it never enters the model-facing catalog and the `skill` tool refuses to load it. Its only legitimate entry point is an explicit user gesture — yet the web client had none. `skill.list` filtered to the model-and-user intersection (hiding user-only skills from the menu), an entered `/name` line rode into the default prompt sink as plain text, and the model it reached was forbidden to load the skill — so it degraded to `read`-ing the SKILL.md file or ignoring the gesture (issue #1470). Even for ordinary skills, the plain-text reference made user invocation a collaboration cue the model could ignore, not a guarantee. +A `disable-model-invocation: true` skill is user-only by design: it never enters the model-facing catalog and the `skill` tool refuses to load it. Its only legitimate entry point is an explicit user gesture — yet the web client had none. `skills/list` filtered to the model-and-user intersection (hiding user-only skills from the menu), an entered `/name` line rode into the default prompt sink as plain text, and the model it reached was forbidden to load the skill — so it degraded to `read`-ing the SKILL.md file or ignoring the gesture (issue #1470). Even for ordinary skills, the plain-text reference made user invocation a collaboration cue the model could ignore, not a guarantee. ## Decision @@ -14,7 +14,7 @@ User-explicit invocation is a host-side pre-step injection, uniform for every us - `dsh-tool-skill` registers a second `agent/pre-step` listener (beside its catalog listener, the same seam `agent-instructions` and the runtime-context snapshot ride): it scans the step's claimed messages for whitespace-bounded `/name` tokens — anywhere in the text, the same word-boundary shape the transcript chip decoration uses — collects first-seen-deduplicated names, loads each through `ctx.skills.get`, checks `isUserInvocable` on the loaded definition (the single lookup that produces what is injected), renders it with the shared `renderSkillContent`, and appends the injections after every other injection of the step: background first (workspace rules, runtime policy, catalog), the material the model must act on last, closest to its answer. Registration order pins the placement — the gesture listener registers before the catalog listener, so the waterfall hands it the catalog-bearing list to extend. - Precision is closed-set matching, exactly like slash commands: `/goal` resolves against the command registry, `/name` against the workspace's user-invocable skill directory; a miss stays ordinary prose, so nothing is ever guessed. Only `source.kind === 'user'` messages are scanned — external text cannot forge a gesture. Paths (`/usr/bin`), fractions (`5/8`), and prefixed tokens (`foo/name`) all break the boundary. -- The client keeps the [plain-text-reference decision](../architecture/2026-07-25-web-input-machine-and-slash-pipeline.md): a menu pick lands the literal `/name ` and the prompt ships it verbatim; ui-skill implements no adjudication hooks and no reference codec. `skill.list` (now the domain's only RPC) serves every user-invocable skill with `modelInvocable` so menus mark user-only entries. A name shared with a host command resolves to the command — adjudication claims the line client-side before it becomes a prompt. +- The client keeps the [plain-text-reference decision](../architecture/2026-07-25-web-input-machine-and-slash-pipeline.md): a menu pick lands the literal `/name ` and the prompt ships it verbatim; ui-skill implements no adjudication hooks and no reference codec. `skills/list` (now the domain's only RPC) serves every user-invocable skill with `modelInvocable` so menus mark user-only entries. A name shared with a host command resolves to the command — adjudication claims the line client-side before it becomes a prompt. - The injection is a `user`-role message carrying the `skill-invocation` source (`{ name, form: 'instructions' }`), so `user/message` logging, the context-injection transcript row (labelled with the skill name), and replay all come free; `renderSkillContent` lives in the `dsh-skill` seam, shared verbatim with the `skill` tool result, and the catalog's closing sentence tells the model to follow an injected block instead of re-loading it. Peer-product survey (Pi, OpenCode, Claude Code, Kimi Code, Codex, DeepSeek-Reasonix — local checkouts) was unanimous that user-explicit triggering is programmatic injection with zero model participation; the final shape is closest to Codex's core-side `$name` mention scanning, which likewise frees every entry point from implementing recognition. diff --git a/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.zh.md b/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.zh.md index 00c76ab99a..7b1aa18808 100644 --- a/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.zh.md +++ b/.agents/notes/implemented/feature/2026-08-08-user-explicit-skill-invocation.zh.md @@ -6,7 +6,7 @@ Status: implemented ## 问题 -`disable-model-invocation: true` 的 skill(技能)在设计上就是仅限用户的:它绝不进入面向模型的目录,`skill` 工具也拒绝加载它。它唯一正当的入口是一次显式的用户手势——而 web 客户端此前没有这个入口。`skill.list` 过滤到模型与用户的交集(把仅限用户的 skill 挡在菜单之外),输入的 `/name` 一行以纯文本落入默认提示词 sink,而这行文本到达的模型又被禁止加载该 skill——于是退化为模型去 `read` 那份 SKILL.md 文件,或者干脆无视这次手势(issue #1470)。即使对普通 skill,纯文本引用也让用户调用只是模型可以忽略的协作线索,而不是保证。 +`disable-model-invocation: true` 的 skill(技能)在设计上就是仅限用户的:它绝不进入面向模型的目录,`skill` 工具也拒绝加载它。它唯一正当的入口是一次显式的用户手势——而 web 客户端此前没有这个入口。`skills/list` 过滤到模型与用户的交集(把仅限用户的 skill 挡在菜单之外),输入的 `/name` 一行以纯文本落入默认提示词 sink,而这行文本到达的模型又被禁止加载该 skill——于是退化为模型去 `read` 那份 SKILL.md 文件,或者干脆无视这次手势(issue #1470)。即使对普通 skill,纯文本引用也让用户调用只是模型可以忽略的协作线索,而不是保证。 ## 决策 @@ -14,7 +14,7 @@ Status: implemented - `dsh-tool-skill` 注册第二个 `agent/pre-step` 监听器(与其目录监听器并列,也是 `agent-instructions` 与运行时上下文快照搭乘的同一 seam):它在该步骤已认领的消息中扫描以空白为界的 `/name` token——文本中任意位置均可,与 transcript(文本记录)chip 装饰所用的词边界形状相同——收集按首见去重的名称,逐个经 `ctx.skills.get` 加载,在已加载定义上检查 `isUserInvocable`(产生注入内容的正是这同一次查找),用共享的 `renderSkillContent` 渲染,并把注入追加在该步骤所有其他注入之后:背景在前(工作区规则、运行时策略、目录),模型必须着手处理的材料在最后、最贴近它的回答。注册顺序钉住了这一位置——手势监听器先于目录监听器注册,因此 waterfall(瀑布式事件)会把携带目录的列表交给它来扩展。 - 精确性来自封闭集合匹配,与斜杠命令完全一致:`/goal` 对照命令注册表解析,`/name` 对照工作区的用户可调用 skill 目录解析;未命中即保持为普通行文,因此绝不猜测。只扫描 `source.kind === 'user'` 的消息——外部文本无法伪造手势。路径(`/usr/bin`)、分数(`5/8`)与带前缀的 token(`foo/name`)都会破坏该边界。 -- 客户端沿用[纯文本引用决策](../architecture/2026-07-25-web-input-machine-and-slash-pipeline.zh.md):菜单 pick 落下字面文本 `/name `,该文本随提示词原样提交;ui-skill 不实现任何裁决钩子,也没有引用 codec。`skill.list`(现在是该领域唯一的 RPC)提供每一个用户可调用的 skill 并携带 `modelInvocable`,供菜单标出仅限用户的条目。与宿主命令同名的名称解析为命令——客户端会在该行成为提示词之前完成裁决并将其认领。 +- 客户端沿用[纯文本引用决策](../architecture/2026-07-25-web-input-machine-and-slash-pipeline.zh.md):菜单 pick 落下字面文本 `/name `,该文本随提示词原样提交;ui-skill 不实现任何裁决钩子,也没有引用 codec。`skills/list`(现在是该领域唯一的 RPC)提供每一个用户可调用的 skill 并携带 `modelInvocable`,供菜单标出仅限用户的条目。与宿主命令同名的名称解析为命令——客户端会在该行成为提示词之前完成裁决并将其认领。 - 注入是一条携带 `skill-invocation` 来源(`{ name, form: 'instructions' }`)的 `user` 角色消息,因此 `user/message` 落账、上下文注入的 transcript 行(以 skill 名称标注)与回放全部免费获得;`renderSkillContent` 位于 `dsh-skill` seam,由注入和 `skill` 工具结果共用,二者内容逐字相同,目录的结尾一句会告诉模型遵循注入块而不是重新加载。 同类产品调研(Pi、OpenCode、Claude Code、Kimi Code、Codex、DeepSeek-Reasonix——本地检出)一致表明:用户显式触发都是模型零参与的程序化注入;最终形态最接近 Codex 核心侧的 `$name` mention 扫描——它同样让每一种运行入口免于自行实现识别。 diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml index 4ba77e05b1..2a8c4bbcb5 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.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-24-user-authorized-subagent-model-routes.md -2026-08-24-user-authorized-subagent-model-routes.md: 0f9816ae89562545c267d87713c13faecd9efc66 -2026-08-24-user-authorized-subagent-model-routes.zh.md: ca63215a5ec7ae56ae2f077a7fb19fc1e204527c +2026-08-24-user-authorized-subagent-model-routes.md: fce0026f6504298d2212ae52ad11aac862fc7e89 +2026-08-24-user-authorized-subagent-model-routes.zh.md: 2cdf42dd3394ea69072dd3cc5bcc2c90fd0b5e48 diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md index 0f9816ae89..fce0026f65 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md @@ -10,7 +10,7 @@ Registering an LLM adapter makes its routes reachable, but does not authorize an ## Decision -The Host-owned `subagent-model-selection` settings section stores an explicit `enabled` switch and `allowedModels`, an array of exact `{ provider, model }` routes. Enabling requires at least one route; disabling may retain the selected routes for later reuse. The Plugins settings card reads the live adapter directory through `llm.models`, lets the user stage the switch and routes, and saves both fields in one revision-fenced settings mutation. It stores no adapter-owned display names, descriptions, or reasoning-effort metadata. A stored or staged route absent from the current directory remains visible as unavailable and removable; a provider-local catalog failure does not block other providers or erase saved authorization or an unsaved selection. A connection reset discards the draft because namespace revisions are comparable only within one Host process. +The Host-owned `subagent-model-selection` settings section stores an explicit `enabled` switch and `allowedModels`, an array of exact `{ provider, model }` routes. Enabling requires at least one route; disabling may retain the selected routes for later reuse. The Plugins settings card reads the live adapter directory through `session/modelCatalog`, lets the user stage the switch and routes, and saves both fields in one revision-fenced settings mutation. It stores no adapter-owned display names, descriptions, or reasoning-effort metadata. A stored or staged route absent from the current directory remains visible as unavailable and removable; a provider-local catalog failure does not block other providers or erase saved authorization or an unsaved selection. A connection reset discards the draft because namespace revisions are comparable only within one Host process. A newly composed top-level Session snapshots the route list in `subagent/model-selection-policy` when the setting is enabled, before its model-selectable definitions can reach a request. Event presence means selection was enabled; the event does not store the global switch. Child Sessions inherit that exact list from their live parent, and resumed Sessions use the recorded event instead of current settings. Settings changes therefore affect only subsequently composed top-level Sessions, while a non-empty legacy Session without the event remains disabled. diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md index ca63215a5e..2cdf42dd33 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md @@ -10,7 +10,7 @@ Status: implemented ## Decision -Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` 开关与 `allowedModels`,后者是由精确 `{ provider, model }` 路由组成的数组。启用时必须至少有一条路由;关闭时可以保留已选路由,供以后重新启用。Plugins 设置卡通过 `llm.models` 读取实时适配器目录,让用户暂存开关与路由,再在一次带 revision 限制的设置 mutation 中保存两个字段。它不保存适配器自有的显示名称、描述或推理强度元数据。当前目录中缺失的已存或暂存路由仍显示为不可用并允许移除;某个提供方的目录失败不会阻塞其他提供方,也不会清除已存授权或未保存选择。连接重置会丢弃草稿,因为 namespace revision 只能在同一个 Host 进程内比较。 +Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` 开关与 `allowedModels`,后者是由精确 `{ provider, model }` 路由组成的数组。启用时必须至少有一条路由;关闭时可以保留已选路由,供以后重新启用。Plugins 设置卡通过 `session/modelCatalog` 读取实时适配器目录,让用户暂存开关与路由,再在一次带 revision 限制的设置 mutation 中保存两个字段。它不保存适配器自有的显示名称、描述或推理强度元数据。当前目录中缺失的已存或暂存路由仍显示为不可用并允许移除;某个提供方的目录失败不会阻塞其他提供方,也不会清除已存授权或未保存选择。连接重置会丢弃草稿,因为 namespace revision 只能在同一个 Host 进程内比较。 设置启用时,新组合的顶层 Session 会在模型可选定义进入请求之前,把路由列表快照记录为 `subagent/model-selection-policy`。事件存在就表示模型选择已启用;事件不保存全局开关。子 Session 从在线父级继承同一份精确列表,恢复的 Session 使用已记录事件而不是当前设置。因此,设置修改只影响之后组合的顶层 Session,而已有非空日志但没有该事件的 Session 仍保持禁用。 diff --git a/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.i18n.yaml index d98333c159..2b17c1d803 100644 --- a/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.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/simplification/2026-08-08-copy-only-preset-authoring.md -2026-08-08-copy-only-preset-authoring.md: bfe0d49755abf47314a7b4cf56c738537fca3963 -2026-08-08-copy-only-preset-authoring.zh.md: 71e83d5e17c56e53ce4f4678b5a22baaed02be31 +2026-08-08-copy-only-preset-authoring.md: 54d317a3de236bf2e191424411c25291c52c7edd +2026-08-08-copy-only-preset-authoring.zh.md: ea449a7b73a0b5a7111ff924b6e47e6323bb58f1 diff --git a/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.md b/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.md index bfe0d49755..54d317a3de 100644 --- a/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.md +++ b/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.md @@ -10,7 +10,7 @@ The agent-preset settings page carried a web YAML editor: `agentPreset.write` ac ## Decision -Authoring is a host-side copy, and files are the editor. `agentPreset.write` became `agentPreset.copy { from, agentPreset, name? }`: two ids the host resolves against its own roots plus an optional display name, whole-directory `cp` (symlinks dereferenced, modes re-tightened to owner-only with owner-execute kept), metadata rewritten to keep the source's description but never its name or `order`. The page becomes: read-only viewer over shipped compositions, copy dialog as the only create entry (no blank "new preset" — writing YAML from nothing is not a thing people do), delete for custom rows, and a location action that leads to the files — `agentPreset.openDocument { agentPreset }` resolves the directory host-side and opens it natively, or answers `{ opened: false, path }` for the row to show as text where the deployment has no desktop (`hasDocument` on `list`, pinned by the gateway's `nativeOpen` config where `canOpenNativePath` platform detection would mislead, e.g. e2e and containers). +Authoring is a host-side copy, and files are the editor. `agentPreset.write` became `agentPreset.copy { from, agentPreset, name? }`: two ids the host resolves against its own roots plus an optional display name, whole-directory `cp` (symlinks dereferenced, modes re-tightened to owner-only with owner-execute kept), metadata rewritten to keep the source's description but never its name or `order`. The page becomes: read-only viewer over shipped compositions, copy dialog as the only create entry (no blank "new preset" — writing YAML from nothing is not a thing people do), delete for custom rows, and a location action that leads to the files — `settings/openAgentPresetDirectory { agentPreset }` resolves the directory host-side and opens it natively, or answers `{ opened: false, path }` for the row to show as text where the deployment has no desktop (`hasDocument` on `list`; `host.describe.canOpenPath` gates the row, and Settings Controller's `nativeOpen` pins server behavior where platform detection would mislead). ## Consequences @@ -27,4 +27,4 @@ Authoring is a host-side copy, and files are the editor. `agentPreset.write` bec ## Alternatives considered -Keeping write with a better editor (CodeMirror etc.): still arbitrary capability over the wire, still the race source, and still a worse editor than the user's own. Patch-semantics copies ("standard plus this diff"): no such layer exists below the bundle plane, and the repo's own shipped presets chose full copies deliberately. Browser-side `host.openPath` with a returned path: breaks the README's no-arbitrary-target invariant the moment the path is a request parameter. +Keeping write with a better editor (CodeMirror etc.): still arbitrary capability over the wire, still the race source, and still a worse editor than the user's own. Patch-semantics copies ("standard plus this diff"): no such layer exists below the bundle plane, and the repo's own shipped presets chose full copies deliberately. Browser-side `session/openWorkspacePath` with a returned path: breaks the README's no-arbitrary-target invariant the moment the path is a request parameter. diff --git a/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.zh.md b/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.zh.md index 71e83d5e17..ea449a7b73 100644 --- a/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.zh.md +++ b/.agents/notes/implemented/simplification/2026-08-08-copy-only-preset-authoring.zh.md @@ -10,7 +10,7 @@ agent-preset 设置页带着一个网页 YAML 编辑器:`agentPreset.write` ## 决策 -创作改为宿主端复制,文件就是编辑器。`agentPreset.write` 变为 `agentPreset.copy { from, agentPreset, name? }`:两个由宿主对照自身根目录解析的 id 加一个可选显示名,整目录 `cp`(符号链接解引用,权限收紧为仅属主并保留属主执行位),元数据重写为保留来源描述、但绝不保留其名称与 `order`。页面变为:随附组装的只读查看器、作为唯一创建入口的复制对话框(不再有空白「新建预设」——从零手写 YAML 不是人会做的事)、自定义行的删除,以及通向文件的位置操作——`agentPreset.openDocument { agentPreset }` 在宿主端解析目录并原生打开,部署没有桌面时回答 `{ opened: false, path }` 供该行以文本形式展示(`list` 上的 `hasDocument`;在 `canOpenNativePath` 平台探测会失真处由网关的 `nativeOpen` 配置钉死,例如 e2e 与容器)。 +创作改为宿主端复制,文件就是编辑器。`agentPreset.write` 变为 `agentPreset.copy { from, agentPreset, name? }`:两个由宿主对照自身根目录解析的 id 加一个可选显示名,整目录 `cp`(符号链接解引用,权限收紧为仅属主并保留属主执行位),元数据重写为保留来源描述、但绝不保留其名称与 `order`。页面变为:随附组装的只读查看器、作为唯一创建入口的复制对话框(不再有空白「新建预设」——从零手写 YAML 不是人会做的事)、自定义行的删除,以及通向文件的位置操作——`settings/openAgentPresetDirectory { agentPreset }` 在 Host 侧解析目录并原生打开,部署没有桌面时回答 `{ opened: false, path }` 供该行以文本形式展示(`list` 上的 `hasDocument`;`host.describe.canOpenPath` 控制该行是否显示,Settings Controller 的 `nativeOpen` 则在平台探测可能误判时固定服务端行为)。 ## 后果 @@ -27,4 +27,4 @@ agent-preset 设置页带着一个网页 YAML 编辑器:`agentPreset.write` ## 考虑过的替代方案 -保留 write 换个更好的编辑器(CodeMirror 等):传输层上仍是任意能力,仍是竞态来源,而且仍不如用户自己的编辑器。带 patch 语义的副本(「standard 加这点 diff」):bundle 面之下没有这样的层,仓库自己的随附 preset 也刻意选了完整副本。浏览器端拿返回路径调 `host.openPath`:路径一旦成为请求参数,就打破了 README 的「不可选中任意目标」不变量。 +保留 write 换个更好的编辑器(CodeMirror 等):传输层上仍是任意能力,仍是竞态来源,而且仍不如用户自己的编辑器。带 patch 语义的副本(「standard 加这点 diff」):bundle 面之下没有这样的层,仓库自己的随附 preset 也刻意选了完整副本。浏览器端拿返回路径调 `session/openWorkspacePath`:路径一旦成为请求参数,就打破了 README 的「不可选中任意目标」不变量。 diff --git a/.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml b/.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml deleted file mode 100644 index f7a2725d96..0000000000 --- a/.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each -# 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/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md -2026-08-10-unary-apiproxy-remote-migration.md: b63946b581d3a2afcd159e3b1c0ef44f824aa35c -2026-08-10-unary-apiproxy-remote-migration.zh.md: 92f40fc79f2be44855620b6b6915798834284747 diff --git a/.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md b/.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md deleted file mode 100644 index b63946b581..0000000000 --- a/.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md +++ /dev/null @@ -1,120 +0,0 @@ -# Agent Note: Migrate simple unary API Proxy calls to business Remote services - -Status: proposed - -English | [中文](2026-08-10-unary-apiproxy-remote-migration.zh.md) - -## Problem - -The Host API Proxy still owns many unary methods whose implementation is only service lookup, argument projection, one business call, and response projection. That duplicates the contract across the business Service, API Proxy interface, Zod schemas, route table, client stub, and Client caller even though [Typert Remote calls](../../implemented/architecture/2026-08-02-typert-remote-method-calls.md) already let the business package own this class of call. - -Moving a method mechanically is not sufficient. Agent-bound API Proxy methods call `agentFor()`, which reuses a live Agent, resumes an ordinary cold Session with its recorded preset, deduplicates concurrent resumes, and rejects subagent-owned identities. A Remote method that resolved an `Agent` or `Session` differently would change lifecycle behavior even when the final business call looked identical. - -The API Proxy also contains BFF operations whose contract is not a business method: Session lifecycle and transcript assembly, model-selection state, live-only input control, configuration filtering, skill presentation, Host composition facts, and native desktop operations. Stateful interactions and streams have different lifecycles again. Treating all unary syntax as evidence that a method is simple would move product policy into arbitrary Service packages or force new packages that have no independent business owner. - -Finally, Connection must authenticate a request before choosing the API Proxy fallback or a Typert interceptor. A migration that authenticates only the fallback would let Remote-owned endpoints bypass the browser identity required by every Host operation. - -## Proposal - -Migrate only unary calls whose business operation already has a natural Service owner and whose remaining adaptation is a small parameter or result projection. The Service binds a Typert namespace and decorates an existing method directly with `@Remote` when its signature is the intended consumer contract. A new method is justified only when it performs real adaptation; an identity `remote*` forwarding wrapper is not. - -`@deepseek-ai/dsh-api-remotes/client` will mount each selected business package's generated `/remote` contribution. Client business packages will call `ctx.remote.` and perform Client-owned joins or presentation projection there. The corresponding API Proxy interface member, schema, route, handler, generated client method, fixture implementation, and production invocation will be removed together in that Service's vertical commit. - -Large BFF methods remain in `dsh-host-apiproxy`. A method leaves this migration if implementation discovers endpoint-specific lifecycle policy, substantial orchestration, a Client dependency on a protocol-only error distinction, or a transport shape that cannot be expressed as a small owner-side adapter. - -## Migration set - -| Legacy RPC | Remote destination | Host method | Adaptation | -|---|---|---|---| -| `session.rename` | `ctx.remote.sessionTitle` in `@deepseek-ai/dsh-session-title` | `SessionTitleService.rename(Session, title)` | Direct `@Remote`; Client maps `eventSeq` to its title projection sequence. | -| `command.list`, `command.execute` | `ctx.remote.commands` in `@deepseek-ai/dsh-commands` | `CommandRuntime.list(Agent)`, `execute(Agent, line, signal)` | Direct `@Remote`; Client maps `undefined` to unmatched and preserves caller cancellation. | -| `llm.providers` | `ctx.remote.llm` in `@deepseek-ai/dsh-llm` | `LlmRuntime.listProviders()`, `listConfigurableProviders()` | Direct `@Remote` on both reads; the Client joins registration and configuration-directory rows. | -| `credentials.describe`, `credentials.set`, `credentials.unset` | `ctx.remote.credentials` in `@deepseek-ai/dsh-api-settings-controller` | `CredentialsController.describe(refs)`, `set(ref, value)`, `unset(ref)` | The controller preserves batch size, reference validation, field projection, provider-absence diagnostics, and provider refusal mapping without adding wire behavior to the abstract Definition. | -| `settings.describe`, `settings.update`, `settings.replace`, `settings.mutate` | `ctx.remote.settings` in `@deepseek-ai/dsh-api-settings-controller` | `SettingsController.describe()`, `update(ns, patch, expectedRevision)`, `replace(ns, section, expectedRevision)`, `mutate(ns, ops, expectedRevision)` | The controller preserves redaction, all three write operations, optimistic revision checks, provider-absence diagnostics, and failure details. | -| `agentPreset.read`, `agentPreset.copy`, `agentPreset.remove` | `ctx.remote.agentPresets` in `@deepseek-ai/dsh-agent-presets` | `readDocument(id)`, `copy(from, id, name?)`, `remove(id)` | `copy` and `remove` are direct; `readDocument` combines stored content with metadata from one live discovery. | -| `subagent.interrupt` | `ctx.remote.subagents` in `@deepseek-ai/dsh-subagent` | `interruptByParent(targetSessionId, parentSessionId)` | Adapter constructs the internal user-authority variant without resolving or resuming either Agent. | -| `workspace.list`, `workspace.insertSessionBefore`, `workspace.archiveSession` | `ctx.remote.workspace` in `@deepseek-ai/dsh-workspace` | `snapshot()`, `insertSessionBefore(workspaceId, sessionId, before?)`, `archiveSession(sessionId)` | Registry adapters detach mutable entities and return the settled workspace or archive snapshot. | - -The Remote API deliberately follows Service names rather than preserving dotted legacy names. For example, Session rename becomes `ctx.remote.sessionTitle.rename(...)`. - -## Deferred API Proxy domains - -| Domain | Methods | Reason retained in the API Proxy | -|---|---|---| -| Session Host lifecycle | `session.list`, `search`, `create`, `fork` | Cross-Agent persistence, Workspace assignment, preset composition, and creation policy. | -| Session transcript | `session.history`, `attachment`, `subagent.history` | Cold/live logs, pagination, projections, presenters, and attachment authorization. | -| Agent model selection | `session.models`, `selectModel` | Per-Agent state, model validation, and default persistence are BFF policy. | -| Agent input and control | `session.prompt`, `updateQueue`, `cancel` | Image admission, Inbox mutation, and endpoint-specific live-only semantics. | -| Native settings document | `settings.openDocument` | Host path resolution, document preparation, and native opening remain product policy in API Proxy. | -| Session skill catalog | `skill.list` | Cold Sessions must not resume; preset standing scope and presenter filtering are BFF joins. | -| Host runtime information | `host.describe` | Version, cwd, default model, and attached count combine several Host owners. | -| Host path opening | `host.openPath`, `agentPreset.openDocument` | Native desktop authority and cancellation belong to the Host composition. | -| Remaining preset, subagent, and workspace calls | `agentPreset.list`, `select`; `subagent.list`, `history`, `prompt`; `workspace.create`, `rename`, `delete` | These calls contain roster policy, live/cold joins, authorization, or serialized multi-operation ordering. | -| Stateful and streaming protocol | approvals, questions, responses, mux and Host streams | They are not one-request/one-result business calls. | - -`workspace.delete` stays with `create` and `rename` because all three participate in the same serialized creation/name/delete chain. Splitting one method out would make the Service and API Proxy observe different operation orders. - -## Agent and Session lookup equivalence - -`createApiRemoteAgentResolver()` constructs one resolver and returns it as the API Proxy's `agentFor`. The same closure is installed through `ctx.typert.lookups.configure('agent', ...)`, `ctx.typert.lookups.configure('session', ...)`, and `ctx.typert.contexts.configureHost('agent', ...)`. Therefore a Remote `Agent` or `Session` parameter and a legacy `agentFor()` call share the same live lookup, in-flight resume table, persistence inspection, preset-aware setup, and ownership fence. - -The migration must pin these outcomes with integration tests: - -- a live ordinary Agent is reused without a resume; -- an ordinary cold Session resumes with its persisted header, events, and recorded preset setup; -- concurrent Agent and Session lookups for one id share one resume; -- a live or cold subagent-owned identity fails with `agent-busy` before business invocation; -- an id missing from durable persistence fails with `session-not-found`; -- resolver failures keep their existing `RpcError` through `TypertLookupFailure`. - -Lookup policy is key-wide, not endpoint-specific. Methods such as prompt, queue editing, cancellation, model selection, and skill listing cannot use the shared `agent` or `session` lookup while retaining live-only or no-resume behavior, so they remain in the API Proxy until Typert supports an explicit per-endpoint policy. - -Methods whose signatures contain only branded ids do not invoke Typert object lookup. `subagents.interruptByParent()` must retain the existing process-local Activation lookup and parent-offline behavior: it does not call `agentFor`, read the catalog, inspect persistence, or cold-resume a parent or child. - -## Client and error behavior - -Generated Remote methods return `RemoteResult` values. Client business services adapt them to their current stores and settle successful results immediately exactly as the existing services do, so event frames remain idempotent replays rather than the only update path. The migration preserves domain validation, provider-absence diagnostics, business error codes, structured details, and successful values; only endpoint addressing, the Remote result envelope, and the separately accepted timeout behavior differ from API Proxy transport. - -Resolver-owned `session-not-found` and `agent-busy` errors remain stable because the shared resolver raises `TypertLookupFailure`. Ordinary business exceptions become the Gateway's existing `internal` RPC failure. A selected Client consumer may migrate only if it does not branch on a more specific legacy business error code; if implementation finds such a branch, that RPC leaves this set unless the business package gains a transport-independent typed failure. - -## Browser authentication - -Connection authenticates the complete `/api` request before choosing the Typert interceptor or API Proxy fallback. Legacy dotted names and Remote slash endpoints therefore use the same process-token-established browser session without an endpoint list. This is a non-escalation requirement: endpoint ownership may change, but an unauthenticated request can reach neither dispatch path. - -## Commit boundaries - -The migration lands as an RFC commit, one vertical commit for each Service, and one final integration commit. A Service commit includes its Host binding and decorators, generated-contract package declarations, API Remotes mount, Client business adoption, and removal of that Service's legacy API Proxy route and production client call. Service commits may be temporarily red because generated artifacts and shared fixtures are reconciled once in the final integration commit. - -The final commit generates every `/remote` artifact from a clean state, updates shared fixtures and tests, moves this note to `implemented`, updates the still-authoritative protocol documentation where central unary ownership changed, and runs the selected repository gates. - -## Alternatives considered - -**Keep simple methods in the central API Proxy.** This preserves one transport facade but continues the duplicated interfaces, schemas, route rows, stubs, and business projections that Typert was introduced to remove. - -**Move every unary API Proxy method.** Unary syntax does not imply single-owner behavior. Session orchestration, live-only control, configuration exposure, and native Host operations would either leak BFF policy into generic Services or create ownerless packages. - -**Give Remote methods a separate resume implementation.** A second resolver could drift on preset restoration, concurrent deduplication, or subagent ownership. Sharing the exact closure with legacy `agentFor()` makes equivalence an implementation fact rather than a promise. - -**Preserve every legacy RPC name and response envelope.** That would turn business packages into copies of the old protocol. Service-oriented names and business values let the Client own joins while Connection continues to own the one RPC envelope. - -**Trust the API Proxy fallback to authenticate requests.** Interceptor selection bypasses that fallback, so Remote methods would become anonymously callable. - -## Acceptance criteria - -- Every migration-table method is callable through its listed `ctx.remote` Service and has no production legacy API Proxy route, schema, map row, client stub, or invocation. -- Existing methods with matching signatures carry `@Remote` directly; every added method performs the adaptation stated in the table and no identity `remote*` wrapper remains. -- Agent/Session integration tests prove the shared lookup outcomes, and subagent interrupt tests prove no cold resume occurs. -- Migrated endpoints reject unauthenticated requests and accept the same valid browser session as legacy endpoints before either dispatch path runs. -- Client behavior and immediate state settlement remain equivalent for every migrated call, including cancellation where supported. -- Deferred methods remain on the API Proxy with their existing behavior. -- A clean generation/build produces and consumes every selected Remote contribution, and focused tests plus final repository gates pass. - -## Risks - -Removing legacy schemas also removes their protocol-specific error taxonomy. A hidden Client branch on one of those codes would make the call non-simple and must be discovered before its Service commit is accepted. - -Generated Remote contracts add build ordering and publication entries to each business package. Missing one runtime mount, declaration export, source-map source, package dependency, or Project Reference can pass a narrow source test while failing a clean Client build. - -Composite dispatch changes security-sensitive carrier code. Tests must exercise both a Remote-owned endpoint and a legacy fallback endpoint so neither path can bypass browser authentication. - -This note applies the existing Typert Remote architecture rather than superseding it. It partially supersedes the central unary ownership and five-step extension checklist in the [GUI RPC protocol note](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md) and the central wiring inventory in the [Web configuration plane note](../../implemented/architecture/2026-07-30-web-config-plane.md); those notes remain authoritative for Connection envelopes and configuration behavior outside the migrated methods. The title, command, configuration-boundary, subagent-interrupt, and archive notes continue to own their business behavior and require factual transport updates rather than archival. The [browser trust boundary](../../implemented/architecture/2026-07-28-api-browser-trust-boundary.md), [browser authentication](../../implemented/architecture/2026-08-24-browser-token-authentication.md), and [generated-contract build order](../../implemented/process/2026-08-08-api-remotes-generated-contract-build.md) remain authoritative and require no archival action. diff --git a/.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md b/.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md deleted file mode 100644 index 92f40fc79f..0000000000 --- a/.agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md +++ /dev/null @@ -1,120 +0,0 @@ -# Agent Note: 将简单的一元 API Proxy 调用迁移到业务 Remote 服务 - -Status: proposed - -[English](2026-08-10-unary-apiproxy-remote-migration.md) | 中文 - -## 问题 - -Host API Proxy 仍承载许多一元方法。这些方法的实现仅执行服务查找、参数投影、一次业务调用和响应投影。尽管 [Typert Remote 调用](../../implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md)已经允许业务包承载此类调用,这种做法仍会在业务服务、API Proxy 接口、Zod schema、路由表、客户端 stub 和 Client 调用方之间重复定义同一约定。 - -仅机械迁移方法并不足够。与 Agent 绑定的 API Proxy 方法会调用 `agentFor()`:它复用 live Agent,使用普通冷 Session 中记录的 preset 恢复该 Session,对并发恢复去重,并拒绝由 subagent 拥有的 identity。如果 Remote 方法以不同方式解析 `Agent` 或 `Session`,即使最终业务调用看起来相同,也会改变生命周期行为。 - -API Proxy 还包含一些不以业务方法为约定的 BFF 操作:Session 生命周期与 transcript(文本记录)组装、模型选择状态、仅限 live 的输入控制、配置过滤、skill(技能)呈现、Host 组合信息和原生桌面操作。有状态交互与流又具有不同的生命周期。若把一元调用的语法一概视为方法简单的依据,就会把产品策略移入任意服务包,或者迫使系统新增没有独立业务所有者的包。 - -最后,Connection 必须在选择 API Proxy 回退路径或 Typert interceptor 前认证请求。若迁移只在回退路径执行认证,由 Remote 持有的 endpoint 就能绕过每个 Host 操作都要求的浏览器身份。 - -## 提案 - -只迁移符合以下条件的一元调用:其业务操作已经有自然归属的服务,且其余适配只是少量参数或结果投影。当现有方法的签名就是预期的消费方约定时,服务应绑定 Typert namespace,并直接使用 `@Remote` 装饰现有方法。只有执行实质性适配时才有理由新增方法;不得添加只做恒等转发的 `remote*` 包装层。 - -`@deepseek-ai/dsh-api-remotes/client` 将挂载所选各业务包生成的 `/remote` 贡献。Client 业务包将调用 `ctx.remote.`,并在包内执行归 Client 所有的关联或呈现投影。对应的 API Proxy 接口成员、schema、路由、处理程序、生成的客户端方法、fixture(测试前置数据)实现和生产调用点,将在该服务的纵向提交中一并移除。 - -大型 BFF 方法仍留在 `dsh-host-apiproxy` 中。如果实现过程中发现某个方法包含端点特有的生命周期策略、大量编排、Client 依赖仅存在于协议层的错误区分,或者其传输数据结构无法用归属方的小型适配器表达,则该方法不在此次迁移范围内。 - -## 迁移集合 - -| 旧 RPC | Remote 目标 | Host 方法 | 适配 | -|---|---|---|---| -| `session.rename` | `ctx.remote.sessionTitle`,位于 `@deepseek-ai/dsh-session-title` | `SessionTitleService.rename(Session, title)` | 直接使用 `@Remote`;Client 将 `eventSeq` 映射到自身的标题投影序列。 | -| `command.list`、`command.execute` | `ctx.remote.commands`,位于 `@deepseek-ai/dsh-commands` | `CommandRuntime.list(Agent)`、`execute(Agent, line, signal)` | 直接使用 `@Remote`;Client 将 `undefined` 映射为未匹配结果,并保留调用方的取消行为。 | -| `llm.providers` | `ctx.remote.llm`,位于 `@deepseek-ai/dsh-llm` | `LlmRuntime.listProviders()`、`listConfigurableProviders()` | 两项读取都直接使用 `@Remote`;Client 关联注册行与配置目录行。 | -| `credentials.describe`、`credentials.set`、`credentials.unset` | `ctx.remote.credentials`,位于 `@deepseek-ai/dsh-api-settings-controller` | `CredentialsController.describe(refs)`、`set(ref, value)`、`unset(ref)` | controller 保留批量上限、引用校验、字段投影、provider 缺失诊断与 provider 拒绝映射,不给抽象 Definition 增加 wire 行为。 | -| `settings.describe`、`settings.update`、`settings.replace`、`settings.mutate` | `ctx.remote.settings`,位于 `@deepseek-ai/dsh-api-settings-controller` | `SettingsController.describe()`、`update(ns, patch, expectedRevision)`、`replace(ns, section, expectedRevision)`、`mutate(ns, ops, expectedRevision)` | controller 保留脱敏、三种写入操作、乐观 revision 校验、provider 缺失诊断与失败 details。 | -| `agentPreset.read`、`agentPreset.copy`、`agentPreset.remove` | `ctx.remote.agentPresets`,位于 `@deepseek-ai/dsh-agent-presets` | `readDocument(id)`、`copy(from, id, name?)`、`remove(id)` | `copy` 和 `remove` 直接暴露现有方法;`readDocument` 将存储的内容与一次实时发现取得的元数据组合。 | -| `subagent.interrupt` | `ctx.remote.subagents`,位于 `@deepseek-ai/dsh-subagent` | `interruptByParent(targetSessionId, parentSessionId)` | 适配器构造内部的用户权限变体,不解析也不恢复任一 Agent。 | -| `workspace.list`、`workspace.insertSessionBefore`、`workspace.archiveSession` | `ctx.remote.workspace`,位于 `@deepseek-ai/dsh-workspace` | `snapshot()`、`insertSessionBefore(workspaceId, sessionId, before?)`、`archiveSession(sessionId)` | 注册表适配器分离可变实体,并返回已完成更新的 workspace 或归档快照。 | - -Remote API 有意采用服务名称,而不保留旧 RPC 的点分名称。例如,Session 重命名将变为 `ctx.remote.sessionTitle.rename(...)`。 - -## 暂缓迁移的 API Proxy 领域 - -| 领域 | 方法 | 保留在 API Proxy 中的原因 | -|---|---|---| -| Session Host 生命周期 | `session.list`、`search`、`create`、`fork` | 跨 Agent 持久化、Workspace 分配、preset 组合和创建策略。 | -| Session transcript | `session.history`、`attachment`、`subagent.history` | cold/live 日志、分页、投影、呈现器和附件授权。 | -| Agent 模型选择 | `session.models`、`selectModel` | 各 Agent 的状态、模型校验和默认值持久化属于 BFF 策略。 | -| Agent 输入与控制 | `session.prompt`、`updateQueue`、`cancel` | 图片准入、Inbox 变更和端点特有的仅限 live 语义。 | -| 原生 settings 文档 | `settings.openDocument` | Host 路径解析、文档准备和原生打开仍属于 API Proxy 中的产品策略。 | -| Session skill 目录 | `skill.list` | 不得恢复冷 Session;preset 的常驻 scope 和呈现器过滤属于 BFF 关联操作。 | -| Host 运行时信息 | `host.describe` | 版本、cwd、默认模型和当前已附加的 Session 数量来自多个 Host 所有者。 | -| Host 路径打开 | `host.openPath`、`agentPreset.openDocument` | 原生桌面权限和取消属于 Host 组合。 | -| 其余 preset、subagent 和 workspace 调用 | `agentPreset.list`、`select`;`subagent.list`、`history`、`prompt`;`workspace.create`、`rename`、`delete` | 这些调用包含名单策略、live/cold 关联、授权或多项操作的串行执行顺序。 | -| 有状态协议和流式协议 | 审批、问题、响应、mux 和 Host 流 | 它们不是一次请求/一次结果的业务调用。 | - -`workspace.delete` 与 `create` 和 `rename` 保持在一起,因为三者都参与同一条串行的创建/命名/删除操作链。单独迁出一个方法会使服务与 API Proxy 观察到不同的操作顺序。 - -## Agent 与 Session lookup 等价性 - -`createApiRemoteAgentResolver()` 构造一个 resolver,并将其作为 API Proxy 的 `agentFor` 返回。同一个 closure 通过 `ctx.typert.lookups.configure('agent', ...)`、`ctx.typert.lookups.configure('session', ...)` 和 `ctx.typert.contexts.configureHost('agent', ...)` 安装。因此,Remote `Agent` 或 `Session` 参数与旧版 `agentFor()` 调用共享同一套 live lookup、进行中的恢复表、持久化检查、感知 preset 的 setup 和 ownership fence。 - -迁移必须用集成测试固定以下结果: - -- 直接复用普通的 live Agent,不执行恢复; -- 根据持久化的 header、事件和已记录的 preset setup 恢复普通冷 Session; -- 对同一个 id 并发执行 Agent 与 Session lookup 时,共享同一次恢复; -- 无论 live 还是 cold,由 subagent 拥有的 identity 都会在业务调用前以 `agent-busy` 失败; -- 持久化存储中不存在的 id 以 `session-not-found` 失败; -- resolver 失败会保留现有的 `RpcError`,并通过 `TypertLookupFailure` 传递。 - -Lookup 策略作用于整个 key,而非特定端点。提示词输入、队列编辑、取消、模型选择和 skill 列表等方法如果使用共享 `agent` 或 `session` lookup,就无法保留仅限 live 或禁止恢复的行为,因此在 Typert 支持显式的逐端点策略之前,这些方法仍留在 API Proxy 中。 - -签名只包含 branded id 的方法不会调用 Typert 对象 lookup。`subagents.interruptByParent()` 必须保留现有的进程内 Activation lookup 和父级离线行为:它不会调用 `agentFor`、读取目录、检查持久化,也不会冷恢复父 Agent 或子 Agent。 - -## Client 与错误行为 - -生成的 Remote 方法返回 `RemoteResult` 值。Client 业务服务负责把它们适配到现有 store,并与既有服务一样让成功结果立即生效,使事件帧仍是幂等回放,而非唯一的更新路径。迁移保留领域校验、provider 缺失诊断、业务错误码、结构化 details 与成功值;只有 endpoint 寻址、Remote 结果信封和另行接受的超时行为不同于 API Proxy 传输。 - -Resolver 拥有的 `session-not-found` 和 `agent-busy` 错误保持稳定,因为共享 resolver 会抛出 `TypertLookupFailure`。普通业务异常会变成 Gateway 现有的 `internal` RPC 失败。只有在选定的 Client 消费方不根据更具体的旧版业务错误码进行分支时,才能迁移该调用;如果实现过程中发现这种分支,除非业务包新增与传输无关的类型化失败,否则该 RPC 将退出此集合。 - -## 浏览器认证 - -Connection 在选择 Typert interceptor 或 API Proxy 回退路径前认证完整 `/api` 请求。旧式点分名称和 Remote 斜杠 endpoint 因此无需 endpoint 清单,就能使用同一个由进程令牌建立的浏览器会话。这是一条非提权要求:endpoint 所有权可以变化,但未认证请求不能进入任一分发路径。 - -## 提交边界 - -此次迁移将以一个 RFC 提交、每项服务各一个纵向提交,以及一个最终集成提交落地。服务提交包含其 Host 绑定与装饰器、生成约定所需的包声明、API Remotes 挂载、Client 业务接入,以及移除该服务的旧版 API Proxy 路由和生产客户端调用。服务提交可能暂时无法通过门禁,因为生成产物和共享 fixture 将在最终集成提交中统一调整。 - -最终提交从干净状态生成所有 `/remote` 产物,更新共享 fixture 和测试,将本文移至 `implemented`,更新中央一元调用所有权发生变化之处仍具权威性的协议文档,并运行选定的仓库门禁。 - -## 考虑过的替代方案 - -**将简单方法保留在中央 API Proxy 中。** 这会保留统一的传输外观,但仍会延续 Typert 原本要消除的重复接口、schema、路由行、stub 和业务投影。 - -**迁移每一个一元 API Proxy 方法。** 一元调用形式并不表示行为只有一个所有者。Session 编排、仅限 live 的控制、配置暴露和原生 Host 操作要么会把 BFF 策略泄漏到通用服务中,要么会产生没有所有者的包。 - -**为 Remote 方法提供单独的恢复实现。** 第二个 resolver 可能在 preset 恢复、并发去重或 subagent 所有权方面出现偏差。与旧版 `agentFor()` 共享完全相同的 closure,使等价性成为实现事实,而不只是一项承诺。 - -**保留每一个旧版 RPC 名称和响应 envelope。** 这会使业务包变成旧协议的副本。面向服务的名称和业务值让 Client 负责关联操作,而 Connection 继续负责统一的 RPC envelope。 - -**依赖 API Proxy 回退路径认证请求。** interceptor 选择会绕过该回退路径,使 Remote 方法变成匿名可调用。 - -## 验收标准 - -- 迁移表中的每个方法都可通过表中列出的 `ctx.remote` 服务调用,并且不存在生产环境中的旧版 API Proxy 路由、schema、映射表行、客户端 stub 或调用。 -- 签名匹配的现有方法直接带有 `@Remote`;每个新增方法都执行表中所述的适配,且不保留只做恒等转发的 `remote*` 包装层。 -- Agent/Session 集成测试证明共享 lookup 的各项结果,subagent 中断测试证明不会发生冷恢复。 -- 已迁移 endpoint 拒绝未认证请求,并在任一分发路径运行前接受与旧 endpoint 相同的有效浏览器会话。 -- 每项已迁移调用的 Client 行为和立即提交状态的行为保持等价,包括支持取消之处的取消行为。 -- 暂缓迁移的方法及其现有行为仍保留在 API Proxy 上。 -- 一次从干净状态开始的生成与构建会生成并消费所选的每项 Remote 贡献,且聚焦测试和最终仓库门禁均通过。 - -## 风险 - -移除旧版 schema 也会移除其协议特有的错误分类。如果 Client 中存在依赖其中某个错误码的隐蔽分支,该调用就不是简单调用,必须在接受相应服务提交前发现它。 - -生成的 Remote 约定会为每个业务包引入构建顺序要求和发布条目。如果遗漏运行时挂载、声明导出、source map 来源、包依赖或 Project Reference 中的任何一项,局部源码测试可能仍会通过,但从干净状态开始的 Client 构建会失败。 - -复合分发会改变安全敏感的载体代码。测试必须覆盖一个由 Remote 拥有的 endpoint 和一个旧版回退 endpoint,确保两条路径都无法绕过浏览器认证。 - -本文应用现有 Typert Remote 架构,而非取代它。本文部分取代 [GUI RPC 协议笔记](../../implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md)中的中央一元调用所有权和五步扩展检查清单,以及 [Web 配置平面笔记](../../implemented/architecture/2026-07-30-web-config-plane.zh.md)中的中央接线清单;对于已迁移方法之外的 Connection envelope 和配置行为,这些笔记仍具权威性。标题、命令、配置边界、subagent 中断和归档笔记继续负责各自的业务行为,只需如实更新传输相关事实,无需归档。[浏览器信任边界](../../implemented/architecture/2026-07-28-api-browser-trust-boundary.zh.md)、[浏览器认证](../../implemented/architecture/2026-08-24-browser-token-authentication.zh.md)和[生成约定构建顺序](../../implemented/process/2026-08-08-api-remotes-generated-contract-build.zh.md)仍具权威性,无需执行归档操作。 diff --git a/apps/web/tests/agent-preset-authoring.overlay.yml b/apps/web/tests/agent-preset-authoring.overlay.yml index 6644752bc2..d39b5307ea 100644 --- a/apps/web/tests/agent-preset-authoring.overlay.yml +++ b/apps/web/tests/agent-preset-authoring.overlay.yml @@ -10,3 +10,6 @@ provider: deepseek-official model: deepseek-v4-flash nativeOpen: false +- id: settings-controller + config: + nativeOpen: false diff --git a/apps/web/tests/navigation-panes.e2e.ts b/apps/web/tests/navigation-panes.e2e.ts index 5d734d93b8..d5ecdabc70 100644 --- a/apps/web/tests/navigation-panes.e2e.ts +++ b/apps/web/tests/navigation-panes.e2e.ts @@ -405,11 +405,8 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { // Read summaries are host-open file links; they also must not open details. const fileLink = page.locator('[data-variant="read"] button').first() await fileLink.waitFor({ timeout: 10_000 }) - const openPath = vi.spyOn(scaffold.ctx.apiProxy.host, 'openPath') - .mockImplementation(async (request, _signal) => ({ - rpcId: request.rpcId, - result: { ok: true, value: { opened: true as const } }, - })) + const openPath = vi.spyOn(scaffold.ctx.sessionController, 'openWorkspacePath') + .mockResolvedValue({ opened: true }) try { await fileLink.click() await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true') diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts index 8e361d0ff5..76d71f7fb7 100644 --- a/apps/web/tests/preview-boot.e2e.ts +++ b/apps/web/tests/preview-boot.e2e.ts @@ -315,12 +315,8 @@ async function bootPreview(origin: string, browser: Browser): Promise { const exercised = await page.evaluate(async () => { type Result = { result: { ok: true; value: T } | { ok: false; error: { code: string; message: string } } } - interface PreviewApi { - skills: { list(payload: { sessionId: string }): Promise> } - } interface PreviewTransport { fetch(input: string, init: RequestInit): Promise - createApiClient(): PreviewApi } const transport = (globalThis as typeof globalThis & { __DSH_TRANSPORT__?: PreviewTransport }).__DSH_TRANSPORT__ if (transport === undefined) throw new Error('preview transport is absent after boot') @@ -352,14 +348,15 @@ async function bootPreview(origin: string, browser: Browser): Promise { 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 skills = await remote<{ skills: Array<{ name: string }> }>( + 'skills/list', { request: { sessionId } }, + ) const createDirectory = async (path: string, name: string): Promise => { await remote('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}`) + await remote<{ skills: Array<{ name: string }> }>( + 'skills/list', { request: { sessionId } }, + ) } await createDirectory('/dsh/workspace/.agents/skills', 'runtime-created') // Settings and credentials both answer over the Remote carrier, so this @@ -383,7 +380,7 @@ async function bootPreview(origin: string, browser: Browser): Promise { await remote('credentials/unset', { ref: 'PREVIEW_TEST_SECRET' }) await new Promise((resolve) => { setTimeout(resolve, 250) }) return { - skillCount: skills.result.value.skills.length, + skillCount: skills.skills.length, credentialConfigured: credentials.PREVIEW_TEST_SECRET?.configured, } }) diff --git a/apps/web/tests/produced-files.e2e.ts b/apps/web/tests/produced-files.e2e.ts index 81259cf365..cdc3296693 100644 --- a/apps/web/tests/produced-files.e2e.ts +++ b/apps/web/tests/produced-files.e2e.ts @@ -149,19 +149,16 @@ describe('web e2e: a finished turn ends with the files it produced', () => { expect(await showFolder.count()).toBe(1) expect(await page.getByText('Produced', { exact: true }).count()).toBe(1) - const openPath = vi.spyOn(scaffold.ctx.apiProxy.host, 'openPath') - .mockImplementation(async (request, _signal) => ({ - rpcId: request.rpcId, - result: { ok: true, value: { opened: true as const } }, - })) + const openPath = vi.spyOn(scaffold.ctx.sessionController, 'openWorkspacePath') + .mockResolvedValue({ opened: true }) try { const [response] = await Promise.all([ - page.waitForResponse(response => new URL(response.url()).pathname === '/api/host.openPath'), + page.waitForResponse(response => new URL(response.url()).pathname === '/api/session/openWorkspacePath'), showFolder.click({ clickCount: 1 }), ]) expect(response.status()).toBe(200) expect(openPath).toHaveBeenCalledTimes(1) - expect(openPath.mock.calls[0]![0].payload).toEqual({ path: `${scaffold.workspaceCwd}/.` }) + expect(openPath.mock.calls[0]![0]).toMatchObject({ path: '.' }) } finally { openPath.mockRestore() } diff --git a/apps/web/tests/produced-files.overlay.yml b/apps/web/tests/produced-files.overlay.yml index 0afe201f71..ceac487918 100644 --- a/apps/web/tests/produced-files.overlay.yml +++ b/apps/web/tests/produced-files.overlay.yml @@ -4,3 +4,6 @@ - id: api-gateway config: nativeOpen: true +- id: settings-controller + config: + nativeOpen: true diff --git a/apps/web/tests/scaffold-hermetic.e2e.ts b/apps/web/tests/scaffold-hermetic.e2e.ts index c504913b9b..585d10ab98 100644 --- a/apps/web/tests/scaffold-hermetic.e2e.ts +++ b/apps/web/tests/scaffold-hermetic.e2e.ts @@ -42,7 +42,7 @@ it('isolates replay skill discovery from every ambient host root', async () => { const ctx = scaffold.ctx // Local skill discovery belongs to the agent's preset LAYER of the host // registry, so the roots under test are only reachable through a composed - // agent's view — the same scope the gateway's `skill.list` resolves for a + // agent's view — the same scope the `skills/list` Remote resolves for a // browser request about a session. const handle = await ctx.agents.create({ sessionId: SessionId('hermetic-skills'), diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts index 44d6a10254..368beeb9e1 100644 --- a/apps/web/tests/seeded-history.e2e.ts +++ b/apps/web/tests/seeded-history.e2e.ts @@ -417,11 +417,8 @@ describe('web e2e: seeded history renders through cold resume', () => { await fileLink.waitFor({ timeout: 10_000 }) const frame = page.locator('[style*="grid-template-columns"]').first() expect(await frame.getAttribute('data-details-collapsed')).toBe('true') - const openPath = vi.spyOn(scaffold.ctx.apiProxy.host, 'openPath') - .mockImplementation(async (request, _signal) => ({ - rpcId: request.rpcId, - result: { ok: true, value: { opened: true as const } }, - })) + const openPath = vi.spyOn(scaffold.ctx.sessionController, 'openWorkspacePath') + .mockResolvedValue({ opened: true }) try { await fileLink.click() await expect.poll(() => frame.getAttribute('data-details-collapsed'), { timeout: 5_000 }).toBe('true') @@ -436,14 +433,8 @@ describe('web e2e: seeded history renders through cold resume', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-file-open-failure')) const fileLink = page.locator('[data-variant="read"] button').first() await fileLink.waitFor({ timeout: 10_000 }) - const openPath = vi.spyOn(scaffold.ctx.apiProxy.host, 'openPath') - .mockImplementation(async (request, _signal) => ({ - rpcId: request.rpcId, - result: { - ok: false as const, - error: { code: 'internal', message: 'xdg-open is not available', details: {} }, - }, - })) + const openPath = vi.spyOn(scaffold.ctx.sessionController, 'openWorkspacePath') + .mockRejectedValue(new Error('xdg-open is not available')) try { await fileLink.click() const dialog = page.getByRole('dialog', { name: 'Couldn’t open file' }) @@ -454,7 +445,7 @@ describe('web e2e: seeded history renders through cold resume', () => { .toContain('path open failed: xdg-open is not available') await page.getByRole('button', { name: 'Retry' }).click() await expect.poll(() => openPath.mock.calls.length, { timeout: 5_000 }).toBe(2) - expect(openPath.mock.calls[0]![0].payload).toEqual(openPath.mock.calls[1]![0].payload) + expect(openPath.mock.calls[0]![0]).toEqual(openPath.mock.calls[1]![0]) await page.getByRole('button', { name: 'Cancel' }).click() await expect.poll(() => page.getByRole('dialog', { name: 'Couldn’t open file' }).count(), { timeout: 5_000, diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index 695dc36d4e..96673df5f8 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -68,12 +68,12 @@ describe('web e2e: settings modal and General preferences', () => { const openDocument = dialog.getByRole('button', { name: '打开配置文件' }) await openDocument.waitFor({ timeout: 10_000 }) let openRequests = 0 - await page.route('**/api/settings.openDocument', async (route) => { + await page.route('**/api/settings/openSettingsDocument', async (route) => { const envelope = route.request().postDataJSON() as { rpcId: string - payload: Record + payload: { args: Record } } - expect(envelope.payload).toEqual({}) + expect(envelope.payload).toEqual({ args: {} }) openRequests += 1 await route.fulfill({ status: 200, @@ -88,7 +88,7 @@ describe('web e2e: settings modal and General preferences', () => { await openDocument.click() await expect.poll(() => openRequests, { timeout: 5_000 }).toBe(1) await expect.poll(() => openDocument.isEnabled(), { timeout: 5_000 }).toBe(true) - await page.unroute('**/api/settings.openDocument') + await page.unroute('**/api/settings/openSettingsDocument') // Golden of the freshly opened dialog (default zh, General active). const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd) await compareOrRefreshGolden(DIALOG_EXPECTED, snapshot, MODE) diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index 4a4d691ac6..4033b29587 100644 --- a/docs/capability-seams.i18n.yaml +++ b/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: 3886ca582ea934c51fc20dfec01fd9f2af829597 -capability-seams.zh.md: a79b93bb8d6fff36e0828dbba8f7e20b885bcbfe +capability-seams.md: 4e9109294c3b02476af9bf97ecf280b1ea84b128 +capability-seams.zh.md: a4d6dd1b7d4111736d532aa36a4b806aef7d8dc0 diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 3886ca582e..4e9109294c 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -38,6 +38,8 @@ flowchart LR pkg_invariants["invariants"] pkg_message_feedback["message-feedback"] svc_sessionController["ctx.sessionController
Host Session Remote controller"] + svc_sessionFileReferences["ctx.sessionFileReferences
Session-addressed file-reference Remote adapter"] + svc_sessionSkillCatalog["ctx.sessionSkillCatalog
Session-addressed skill Remote adapter"] pkg_api_settings_controller["api-settings-controller"] svc_credentialsController["ctx.credentialsController
Host credential-surface Remote controller"] svc_settingsController["ctx.settingsController
Host settings-surface Remote controller"] @@ -223,6 +225,8 @@ flowchart LR pkg_agent_presets --> svc_agentPresets pkg_api_gateway --> svc_typertGateway pkg_api_session_controller --> svc_sessionController + pkg_api_session_controller --> svc_sessionFileReferences + pkg_api_session_controller --> svc_sessionSkillCatalog pkg_api_settings_controller --> svc_credentialsController pkg_api_settings_controller --> svc_settingsController pkg_api_workspace_controller --> svc_directoryPickerController @@ -362,6 +366,7 @@ flowchart LR svc_dynamicCordisRunner --> pkg_tool_cordis svc_e2b --> pkg_fs_e2b svc_e2b --> pkg_subprocess_e2b + svc_fileReferences --> pkg_api_session_controller svc_fs --> pkg_tool_fs svc_invariants --> pkg_agent svc_invariants --> pkg_agent_loop @@ -379,7 +384,6 @@ flowchart LR svc_sandboxPolicy --> pkg_bash_sandbox svc_sandboxPolicy --> pkg_fs_sandbox svc_sandboxPolicy --> pkg_terminal_bash - svc_sessionController --> pkg_host_apiproxy svc_sessionPersistence --> pkg_agent_loop svc_sessionPersistence --> pkg_hooks_claude_code svc_sessionPersistence --> pkg_hooks_codex @@ -467,7 +471,9 @@ flowchart LR | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. | | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. | | `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.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | Owns Session commands, cold reads, durable-event following, live control state, model catalogs, workspace opening, and Agent activation policy. | +| `ctx.sessionFileReferences` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | Delegates file-reference discovery through the Session Controller's established Agent lookup policy. | +| `ctx.sessionSkillCatalog` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | Lists the Session composition's user-invocable skills without activating a cold Agent. | | `ctx.credentialsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | Projects the credential-reference seam onto the generated Remote namespace: batch fan-out, view projection, and refusal mapping live here, not on the seam Definition. | | `ctx.settingsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | Projects the user-settings seam onto the generated Remote namespace: the read is always redacted and every refusal is classified here, not on the seam Definition. | | `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Owns Workspace commands and reconnect-safe Workspace state delivery through the generated Remote namespace. | @@ -486,7 +492,7 @@ flowchart LR | `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry. | | `ctx.workspaceRegistry` | `core` | [`workspace`](../packages/workspace/workspace) | - | [`api-workspace-controller`](../packages/api/workspace-controller), [`api-session-controller`](../packages/api/session-controller) | - | Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections. | | `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering. | -| `ctx.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | - | - | The interface returns path-only completion candidates within the addressed Agent cwd through its unary Remote contract; providers own namespace access and ranking without reading file contents. | +| `ctx.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | [`api-session-controller`](../packages/api/session-controller) | - | The interface returns path-only completion candidates within an Agent cwd; providers own namespace access and ranking without reading file contents. | | `ctx.sessionReferenceResolver` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. | | `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. | | `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index a79b93bb8d..a4d6dd1b7d 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -40,6 +40,8 @@ flowchart LR pkg_invariants["invariants"] pkg_message_feedback["message-feedback"] svc_sessionController["ctx.sessionController
Host Session Remote controller"] + svc_sessionFileReferences["ctx.sessionFileReferences
Session-addressed file-reference Remote adapter"] + svc_sessionSkillCatalog["ctx.sessionSkillCatalog
Session-addressed skill Remote adapter"] pkg_api_settings_controller["api-settings-controller"] svc_credentialsController["ctx.credentialsController
Host credential-surface Remote controller"] svc_settingsController["ctx.settingsController
Host settings-surface Remote controller"] @@ -225,6 +227,8 @@ flowchart LR pkg_agent_presets --> svc_agentPresets pkg_api_gateway --> svc_typertGateway pkg_api_session_controller --> svc_sessionController + pkg_api_session_controller --> svc_sessionFileReferences + pkg_api_session_controller --> svc_sessionSkillCatalog pkg_api_settings_controller --> svc_credentialsController pkg_api_settings_controller --> svc_settingsController pkg_api_workspace_controller --> svc_directoryPickerController @@ -364,6 +368,7 @@ flowchart LR svc_dynamicCordisRunner --> pkg_tool_cordis svc_e2b --> pkg_fs_e2b svc_e2b --> pkg_subprocess_e2b + svc_fileReferences --> pkg_api_session_controller svc_fs --> pkg_tool_fs svc_invariants --> pkg_agent svc_invariants --> pkg_agent_loop @@ -381,7 +386,6 @@ flowchart LR svc_sandboxPolicy --> pkg_bash_sandbox svc_sandboxPolicy --> pkg_fs_sandbox svc_sandboxPolicy --> pkg_terminal_bash - svc_sessionController --> pkg_host_apiproxy svc_sessionPersistence --> pkg_agent_loop svc_sessionPersistence --> pkg_hooks_claude_code svc_sessionPersistence --> pkg_hooks_codex @@ -469,7 +473,9 @@ flowchart LR | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 | | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 | | `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.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | 负责 Session 命令、冷读取、持久事件跟随、实时控制状态、模型目录、workspace 打开与 Agent 激活策略。 | +| `ctx.sessionFileReferences` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | 通过 Session Controller 的既有 Agent lookup 策略委托文件引用发现。 | +| `ctx.sessionSkillCatalog` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | - | - | 在不激活冷 Agent 的前提下列出 Session 组合中允许用户调用的 skill。 | | `ctx.credentialsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | 把凭据引用 seam 投影到生成的 Remote namespace:批量扇出、视图投影与拒绝映射都在这里,而不在 seam Definition 上。 | | `ctx.settingsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | 把用户设置 seam 投影到生成的 Remote namespace:读取一律脱敏,所有拒绝在这里分类,而不在 seam Definition 上。 | | `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 通过生成的 Remote namespace 负责 Workspace 命令和可在重连后收敛的 Workspace 状态投递。 | @@ -488,7 +494,7 @@ flowchart LR | `ctx.messageFeedback` | `core` | [`message-feedback`](../packages/feedback/message-feedback) | - | - | - | 拥有本地逐 assistant 消息反馈、生命周期与目标校验、逐条目 compare-and-set 及 Host 一元 Remote 契约,且不进入 Session 历史或遥测。 | | `ctx.workspaceRegistry` | `core` | [`workspace`](../packages/workspace/workspace) | - | [`api-workspace-controller`](../packages/api/workspace-controller), [`api-session-controller`](../packages/api/session-controller) | - | 通过领域设施拥有带 WorkspaceId 品牌类型的记录;稳定的 sessionIds 账户驱动 Host RPC 与 GUI 投影。 | | `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | 该接口提供精确读取、过滤和追踪;具体后端还提供全文协调、排序、摘要片段和游标世代,而模型消费方负责工作区权限与不含游标的渲染。 | -| `ctx.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | - | - | 该接口通过其一元 Remote 契约返回指定 Agent cwd 内仅含路径的补全候选;提供方负责命名空间访问和排序,但不会读取文件内容。 | +| `ctx.fileReferences` | `seam` | [`file-reference`](../packages/context/file-reference) | [`file-reference-local`](../packages/context/file-reference-local) | [`api-session-controller`](../packages/api/session-controller) | - | 该接口返回 Agent cwd 内仅含路径的补全候选;提供方负责命名空间访问与排序,但不读取文件内容。 | | `ctx.sessionReferenceResolver` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | 将当前表层中有界的对话快照投影为持久但不可信的消息上下文;Host 适配器负责提及语法。 | | `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-prompt-llm`](../packages/session/session-title-first-prompt-llm), [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | - | - | 负责确定性回退、最新标题折叠区,以及唯一的可选异步提供方注册。 | | `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-terminal`](../packages/terminal/tool-terminal), [`tool-web`](../packages/web/tool-web) | - | 为每个步骤收集提示词各部分和面向模型的工具 schema。 | diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 638000d0ef..2416c74af9 100644 --- a/docs/config-catalog.i18n.yaml +++ b/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: 4331c0a5153f32f0e6af5b6ec6fd182ee9b335b4 -config-catalog.zh.md: 54f6ddde10053d422af2c5ebfd88a367597adc89 +config-catalog.md: 0a08455a27fa94f1bb69628b61dd8eb23d318ded +config-catalog.zh.md: ad6a9c0481750ec41c0701d3a9e3989fd0e9ebf4 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 4331c0a515..0a08455a27 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -304,7 +304,21 @@ export interface Config { } ``` -Source: [`packages/api/session-controller/src/index.ts:58`](../packages/api/session-controller/src/index.ts) +Source: [`packages/api/session-controller/src/index.ts:69`](../packages/api/session-controller/src/index.ts) + + + +## `@deepseek-ai/dsh-api-settings-controller` + +```ts config-catalog +/** Native document-opening policy. */ +export interface Config { + /** Override platform desktop-opener detection. */ + readonly nativeOpen?: boolean +} +``` + +Source: [`packages/api/settings-controller/src/index.ts:41`](../packages/api/settings-controller/src/index.ts) @@ -858,7 +872,7 @@ Source: [`packages/hooks/hooks-codex/src/index.ts:44`](../packages/hooks/hooks-c ## `@deepseek-ai/dsh-host-apiproxy` -Requires: `agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `sessionQuery` · `sessionController` +Requires: `agentDefaultModel` · `agents` · `attachments` · `sessions` · `sessionQuery` ```ts config-catalog /** Gateway plugin configuration. */ @@ -880,7 +894,7 @@ export interface Config { } ``` -Source: [`packages/host/apiproxy/src/index.ts:42`](../packages/host/apiproxy/src/index.ts) +Source: [`packages/host/apiproxy/src/index.ts:40`](../packages/host/apiproxy/src/index.ts) @@ -3399,7 +3413,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-acp-app` — requires `cmdlineArgs` ([`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts)) - `@deepseek-ai/dsh-agent` ([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts)) - `@deepseek-ai/dsh-api-remotes` — requires `typertGateway` ([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts)) -- `@deepseek-ai/dsh-api-settings-controller` ([`packages/api/settings-controller/src/index.ts`](../packages/api/settings-controller/src/index.ts)) - `@deepseek-ai/dsh-api-workspace-controller` — requires `typert` · `workspaceRegistry` ([`packages/api/workspace-controller/src/index.ts`](../packages/api/workspace-controller/src/index.ts)) - `@deepseek-ai/dsh-authorization` — requires `credentials` ([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts)) - `@deepseek-ai/dsh-client-locale` ([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 54f6ddde10..ad6a9c0481 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -306,7 +306,21 @@ export interface Config { } ``` -来源:[`packages/api/session-controller/src/index.ts:58`](../packages/api/session-controller/src/index.ts) +来源:[`packages/api/session-controller/src/index.ts:69`](../packages/api/session-controller/src/index.ts) + + + +## `@deepseek-ai/dsh-api-settings-controller` + +```ts config-catalog +/** Native document-opening policy. */ +export interface Config { + /** Override platform desktop-opener detection. */ + readonly nativeOpen?: boolean +} +``` + +来源:[`packages/api/settings-controller/src/index.ts:41`](../packages/api/settings-controller/src/index.ts) @@ -860,7 +874,7 @@ export interface Config { ## `@deepseek-ai/dsh-host-apiproxy` -需要:`agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `sessionQuery` · `sessionController` +需要:`agentDefaultModel` · `agents` · `attachments` · `sessions` · `sessionQuery` ```ts config-catalog /** Gateway plugin configuration. */ @@ -882,7 +896,7 @@ export interface Config { } ``` -来源:[`packages/host/apiproxy/src/index.ts:42`](../packages/host/apiproxy/src/index.ts) +来源:[`packages/host/apiproxy/src/index.ts:40`](../packages/host/apiproxy/src/index.ts) @@ -3401,7 +3415,6 @@ export interface Config { - `@deepseek-ai/dsh-acp-app` — 需要 `cmdlineArgs`([`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts)) - `@deepseek-ai/dsh-agent`([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts)) - `@deepseek-ai/dsh-api-remotes` — 需要 `typertGateway`([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts)) -- `@deepseek-ai/dsh-api-settings-controller`([`packages/api/settings-controller/src/index.ts`](../packages/api/settings-controller/src/index.ts)) - `@deepseek-ai/dsh-api-workspace-controller` — 需要 `typert` · `workspaceRegistry`([`packages/api/workspace-controller/src/index.ts`](../packages/api/workspace-controller/src/index.ts)) - `@deepseek-ai/dsh-authorization` — 需要 `credentials`([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts)) - `@deepseek-ai/dsh-client-locale`([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts)) diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index e5cf853414..468327d243 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.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/event-producer-consumer.md -event-producer-consumer.md: e849cb84265c0781e4a8680d0bb247e9955b5c2e -event-producer-consumer.zh.md: b5835c56b26f3a75fd792d3a71c3ab2dc688ea42 +event-producer-consumer.md: 2c443095b796eb274fa099b4b7d91b4454311a37 +event-producer-consumer.zh.md: b35e1c56d7f51d3aed7f3f81aec228708cb952e3 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index e849cb8426..2c443095b7 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -21,11 +21,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:185`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:504`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:484`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:511`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:490`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:497`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:538`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:518`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:545`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:524`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:531`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) | | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` | @@ -43,7 +43,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` | -| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:66`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | +| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index b5835c56b2..b35e1c56d7 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -23,11 +23,11 @@ | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:185`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:504`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:484`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:511`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:490`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:497`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:538`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:518`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:545`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:524`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:531`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) | | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` | @@ -45,7 +45,7 @@ | `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) | | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` | -| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:66`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | +| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:67`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) | diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index c69c9d86b9..73c0c75403 100644 --- a/docs/module-graph.i18n.yaml +++ b/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: a6f70aa1ff1616acf305ae386b148c56d8ebcf85 -module-graph.zh.md: 3b33bc68e67c19c03ec4d212c864371b27b31278 +module-graph.md: e0376d865fac1505cce48f4f3a678a11730ecd0e +module-graph.zh.md: 72af3b9a52764e0ae8ed2b7cef910eeedefa6c6c diff --git a/docs/module-graph.md b/docs/module-graph.md index a6f70aa1ff..e0376d865f 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -384,6 +384,7 @@ flowchart TD pkg_experimental_agent_team_profile --> pkg_invariants pkg_experimental_agent_team_web_profile --> pkg_invariants pkg_experimental_webworker_packer --> pkg_invariants + pkg_host_apiproxy --> pkg_invariants pkg_host_directory_picker --> pkg_invariants pkg_host_directory_picker_browse --> pkg_invariants pkg_host_directory_picker_native --> pkg_invariants @@ -427,6 +428,7 @@ flowchart TD pkg_llm --> pkg_brand pkg_llm --> pkg_invariants pkg_llm --> pkg_timeout + pkg_llm --> pkg_typert_protocol pkg_attachment_local --> pkg_attachment pkg_attachment_local --> pkg_home_paths pkg_attachment_local --> pkg_invariants @@ -441,6 +443,10 @@ flowchart TD pkg_experimental_inspector --> pkg_client_modules pkg_experimental_inspector --> pkg_host_webserver pkg_experimental_inspector --> pkg_invariants + pkg_experimental_webworker_runtime --> pkg_client_modules + pkg_experimental_webworker_runtime --> pkg_host_apiproxy + pkg_experimental_webworker_runtime --> pkg_host_webserver + pkg_experimental_webworker_runtime --> pkg_invariants pkg_session --> pkg_brand pkg_session --> pkg_invariants pkg_session --> pkg_llm @@ -539,14 +545,8 @@ flowchart TD pkg_web_search_deepseek --> pkg_web pkg_spill_local --> pkg_invariants pkg_spill_local --> pkg_spill - pkg_api_settings_controller --> pkg_credentials - pkg_api_settings_controller --> pkg_invariants - pkg_api_settings_controller --> pkg_session - pkg_api_settings_controller --> pkg_settings - pkg_api_settings_controller --> pkg_typert_protocol pkg_file_reference --> pkg_agent pkg_file_reference --> pkg_invariants - pkg_file_reference --> pkg_typert_protocol pkg_time_context --> pkg_agent pkg_time_context --> pkg_invariants pkg_time_context --> pkg_session @@ -1011,9 +1011,27 @@ flowchart TD pkg_acp --> pkg_session_persistence pkg_acp --> pkg_token_meter pkg_acp --> pkg_user_approval + pkg_api_settings_controller --> pkg_agent_presets + pkg_api_settings_controller --> pkg_credentials + pkg_api_settings_controller --> pkg_invariants + pkg_api_settings_controller --> pkg_native_command + pkg_api_settings_controller --> pkg_session + pkg_api_settings_controller --> pkg_settings + pkg_api_settings_controller --> pkg_typert_protocol pkg_web_app --> pkg_invariants pkg_web_app --> pkg_shell_env pkg_web_app --> pkg_system_prompt + pkg_client_connection --> pkg_attachment + 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 + pkg_client_connection --> pkg_session + pkg_client_connection --> pkg_settings + pkg_client_connection --> pkg_tool_todo pkg_compaction_tool_result_pruner --> pkg_compaction pkg_compaction_tool_result_pruner --> pkg_invariants pkg_compaction_tool_result_pruner --> pkg_llm @@ -1027,8 +1045,6 @@ flowchart TD pkg_tool_cordis --> pkg_session pkg_tool_cordis --> pkg_system_prompt pkg_tool_cordis --> pkg_tools - pkg_host_apiproxy --> pkg_agent_presets - pkg_host_apiproxy --> pkg_invariants pkg_tool_bash --> pkg_agent pkg_tool_bash --> pkg_invariants pkg_tool_bash --> pkg_jobs @@ -1090,17 +1106,11 @@ flowchart TD pkg_tool_session_query --> pkg_system_prompt pkg_tool_session_query --> pkg_timeout pkg_tool_session_query --> pkg_tools - pkg_client_connection --> pkg_attachment - 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 - pkg_client_connection --> pkg_session - pkg_client_connection --> pkg_settings - pkg_client_connection --> pkg_tool_todo + pkg_api_gateway --> pkg_brand + pkg_api_gateway --> pkg_client_connection + pkg_api_gateway --> pkg_host_webserver + pkg_api_gateway --> pkg_invariants + pkg_api_gateway --> pkg_typert_registry pkg_compaction_basic --> pkg_agent pkg_compaction_basic --> pkg_commands pkg_compaction_basic --> pkg_compaction @@ -1142,10 +1152,9 @@ flowchart TD pkg_agent_spine_demo --> pkg_tool_jobs pkg_agent_spine_demo --> pkg_tool_skill pkg_agent_spine_demo --> pkg_tools - pkg_experimental_webworker_runtime --> pkg_client_modules - pkg_experimental_webworker_runtime --> pkg_host_apiproxy - pkg_experimental_webworker_runtime --> pkg_host_webserver - pkg_experimental_webworker_runtime --> pkg_invariants + pkg_host_frontend_static --> pkg_client_connection + pkg_host_frontend_static --> pkg_host_webserver + pkg_host_frontend_static --> pkg_invariants pkg_webhook_github --> pkg_credentials pkg_webhook_github --> pkg_host_webserver pkg_webhook_github --> pkg_invariants @@ -1205,11 +1214,39 @@ flowchart TD pkg_hooks_claude_code --> pkg_session_persistence pkg_hooks_claude_code --> pkg_subagent pkg_hooks_claude_code --> pkg_tools - pkg_api_gateway --> pkg_brand - pkg_api_gateway --> pkg_client_connection - pkg_api_gateway --> pkg_host_webserver - pkg_api_gateway --> pkg_invariants - pkg_api_gateway --> pkg_typert_registry + pkg_api_session_controller --> pkg_agent + pkg_api_session_controller --> pkg_agent_default_model + pkg_api_session_controller --> pkg_agent_presets + pkg_api_session_controller --> pkg_api_gateway + pkg_api_session_controller --> pkg_attachment + pkg_api_session_controller --> pkg_brand + pkg_api_session_controller --> pkg_client_connection + pkg_api_session_controller --> pkg_file_reference + pkg_api_session_controller --> pkg_invariants + pkg_api_session_controller --> pkg_jobs + pkg_api_session_controller --> pkg_llm + pkg_api_session_controller --> pkg_native_command + pkg_api_session_controller --> pkg_scope + pkg_api_session_controller --> pkg_session + pkg_api_session_controller --> pkg_session_persistence + pkg_api_session_controller --> pkg_session_projection + pkg_api_session_controller --> pkg_session_projection_cache + pkg_api_session_controller --> pkg_session_query + pkg_api_session_controller --> pkg_session_title + pkg_api_session_controller --> pkg_skill + pkg_api_session_controller --> pkg_subagent + pkg_api_session_controller --> pkg_typert_protocol + pkg_api_session_controller --> pkg_typert_registry + pkg_api_session_controller --> pkg_util_workspace_path + 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 + pkg_api_workspace_controller --> pkg_typert_protocol + pkg_api_workspace_controller --> pkg_workspace pkg_experimental_agent_team --> pkg_agent pkg_experimental_agent_team --> pkg_brand pkg_experimental_agent_team --> pkg_invariants @@ -1218,9 +1255,6 @@ flowchart TD pkg_experimental_agent_team --> pkg_session_persistence pkg_experimental_agent_team --> pkg_subagent pkg_experimental_agent_team --> pkg_typert_protocol - pkg_host_frontend_static --> pkg_client_connection - pkg_host_frontend_static --> pkg_host_webserver - pkg_host_frontend_static --> pkg_invariants pkg_sdk_protocol --> pkg_invariants pkg_sdk_protocol --> pkg_llm pkg_sdk_protocol --> pkg_session @@ -1248,36 +1282,30 @@ flowchart TD pkg_subagent_spawn_in_process --> pkg_invariants pkg_subagent_spawn_in_process --> pkg_subagent pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver - pkg_api_session_controller --> pkg_agent - pkg_api_session_controller --> pkg_agent_default_model - pkg_api_session_controller --> pkg_agent_presets - pkg_api_session_controller --> pkg_api_gateway - pkg_api_session_controller --> pkg_attachment - pkg_api_session_controller --> pkg_brand - pkg_api_session_controller --> pkg_client_connection - pkg_api_session_controller --> pkg_invariants - pkg_api_session_controller --> pkg_jobs - pkg_api_session_controller --> pkg_llm - pkg_api_session_controller --> pkg_scope - pkg_api_session_controller --> pkg_session - pkg_api_session_controller --> pkg_session_persistence - pkg_api_session_controller --> pkg_session_projection - pkg_api_session_controller --> pkg_session_projection_cache - pkg_api_session_controller --> pkg_session_query - pkg_api_session_controller --> pkg_session_title - pkg_api_session_controller --> pkg_subagent - pkg_api_session_controller --> pkg_typert_protocol - pkg_api_session_controller --> pkg_typert_registry - pkg_api_session_controller --> pkg_util_workspace_path - 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 - pkg_api_workspace_controller --> pkg_typert_protocol - pkg_api_workspace_controller --> pkg_workspace + pkg_api_remotes --> pkg_agent_presets + pkg_api_remotes --> pkg_api_gateway + pkg_api_remotes --> pkg_api_session_controller + pkg_api_remotes --> pkg_api_settings_controller + pkg_api_remotes --> pkg_api_workspace_controller + pkg_api_remotes --> pkg_commands + pkg_api_remotes --> pkg_cordis_host_runner + pkg_api_remotes --> pkg_credentials + pkg_api_remotes --> pkg_file_reference + pkg_api_remotes --> pkg_goal + pkg_api_remotes --> pkg_host_plugin_inventory + pkg_api_remotes --> pkg_invariants + pkg_api_remotes --> pkg_llm + pkg_api_remotes --> pkg_message_feedback + pkg_api_remotes --> pkg_session + pkg_api_remotes --> pkg_session_reference + pkg_api_remotes --> pkg_settings + pkg_api_remotes --> pkg_subagent + pkg_api_remotes --> pkg_user_approval + pkg_api_remotes --> pkg_user_questions + pkg_client_ui_session --> pkg_api_session_controller + pkg_client_ui_session --> pkg_client_ui_renderer + pkg_client_ui_session --> pkg_invariants + pkg_client_ui_session --> pkg_session pkg_experimental_tool_agent_team --> pkg_agent pkg_experimental_tool_agent_team --> pkg_experimental_agent_team pkg_experimental_tool_agent_team --> pkg_invariants @@ -1304,30 +1332,6 @@ flowchart TD pkg_subagent_dsh_sdk --> pkg_session pkg_subagent_dsh_sdk --> pkg_subagent pkg_subagent_dsh_sdk --> pkg_subprocess - pkg_api_remotes --> pkg_agent_presets - pkg_api_remotes --> pkg_api_gateway - pkg_api_remotes --> pkg_api_session_controller - pkg_api_remotes --> pkg_api_settings_controller - pkg_api_remotes --> pkg_api_workspace_controller - pkg_api_remotes --> pkg_commands - pkg_api_remotes --> pkg_cordis_host_runner - pkg_api_remotes --> pkg_credentials - pkg_api_remotes --> pkg_file_reference - pkg_api_remotes --> pkg_goal - pkg_api_remotes --> pkg_host_plugin_inventory - pkg_api_remotes --> pkg_invariants - pkg_api_remotes --> pkg_llm - pkg_api_remotes --> pkg_message_feedback - pkg_api_remotes --> pkg_session - pkg_api_remotes --> pkg_session_reference - pkg_api_remotes --> pkg_settings - pkg_api_remotes --> pkg_subagent - pkg_api_remotes --> pkg_user_approval - pkg_api_remotes --> pkg_user_questions - pkg_client_ui_session --> pkg_api_session_controller - pkg_client_ui_session --> pkg_client_ui_renderer - pkg_client_ui_session --> pkg_invariants - pkg_client_ui_session --> pkg_session pkg_client_ui_settings --> pkg_api_remotes pkg_client_ui_settings --> pkg_client_connection pkg_client_ui_settings --> pkg_invariants @@ -1339,7 +1343,6 @@ flowchart TD pkg_client_locale --> pkg_invariants pkg_client_locale --> pkg_settings pkg_client_ui_settings_models --> pkg_api_remotes - pkg_client_ui_settings_models --> pkg_client_connection pkg_client_ui_settings_models --> pkg_client_locale pkg_client_ui_settings_models --> pkg_client_ui_renderer pkg_client_ui_settings_models --> pkg_client_ui_settings @@ -1541,7 +1544,6 @@ flowchart TD pkg_client_ui_chat --> pkg_settings pkg_client_ui_chat --> pkg_token_meter pkg_client_ui_chat --> pkg_tools - pkg_client_ui_chat --> pkg_util_workspace_path pkg_client_ui_commands --> pkg_api_remotes pkg_client_ui_commands --> pkg_api_session_controller pkg_client_ui_commands --> pkg_client_locale @@ -1625,7 +1627,6 @@ flowchart TD pkg_client_ui_message_feedback --> pkg_typert_protocol pkg_client_ui_model_selection --> pkg_api_remotes pkg_client_ui_model_selection --> pkg_api_session_controller - pkg_client_ui_model_selection --> pkg_client_connection pkg_client_ui_model_selection --> pkg_client_locale pkg_client_ui_model_selection --> pkg_client_ui_commands pkg_client_ui_model_selection --> pkg_client_ui_conversation @@ -1730,6 +1731,7 @@ flowchart TD | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`experimental-agent-team-web-profile`](../packages/experimental/agent-team-web-profile) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | @@ -1753,11 +1755,12 @@ flowchart TD | [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) | | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) | -| [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`timeout`](../packages/util/timeout) | +| [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`timeout`](../packages/util/timeout), [`typert-protocol`](../packages/typert/protocol) | | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment) | | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-modules`](../packages/client/modules), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`typert-protocol`](../packages/typert/protocol) | | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | @@ -1785,8 +1788,7 @@ flowchart TD | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) | | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) | -| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) | -| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) | +| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) | | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | @@ -1870,21 +1872,22 @@ flowchart TD | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`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-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) | | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) | +| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) | | [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) | +| [`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), [`settings`](../packages/settings/settings), [`tool-todo`](../packages/todo/tool-todo) | | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) | | [`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-directory-picker`](../packages/host/directory-picker), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`tool-todo`](../packages/todo/tool-todo) | +| [`api-gateway`](../packages/api/gateway) | `api` | [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) | | [`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-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), [`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) | -| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-modules`](../packages/client/modules), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`webhook-github`](../packages/webhook/webhook-github) | `webhook` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`webhook`](../packages/webhook/webhook) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | @@ -1894,25 +1897,23 @@ flowchart TD | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | -| [`api-gateway`](../packages/api/gateway) | `api` | [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) | +| [`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), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`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), [`skill`](../packages/skill/skill), [`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), [`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-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | -| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`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), [`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) | +| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-settings-controller`](../packages/api/settings-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) | +| [`client-ui-session`](../packages/client/ui-session) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`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) | | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | -| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-settings-controller`](../packages/api/settings-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) | -| [`client-ui-session`](../packages/client/ui-session) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) | | [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) | -| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) | @@ -1933,7 +1934,7 @@ flowchart TD | [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`attachment`](../packages/attachment/attachment), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`client-ui-user-questions`](../packages/client/ui-user-questions) | `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), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions) | | [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`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-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | -| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`util-workspace-path`](../packages/util/workspace-path) | +| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | | [`client-ui-commands`](../packages/client/ui-commands) | `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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) | | [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) | @@ -1943,7 +1944,7 @@ flowchart TD | [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`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), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | -| [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | +| [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) | | [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`api-remotes`](../packages/api/remotes), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`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), [`util-workspace-path`](../packages/util/workspace-path) | | [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`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), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 3b33bc68e6..72af3b9a52 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -386,6 +386,7 @@ flowchart TD pkg_experimental_agent_team_profile --> pkg_invariants pkg_experimental_agent_team_web_profile --> pkg_invariants pkg_experimental_webworker_packer --> pkg_invariants + pkg_host_apiproxy --> pkg_invariants pkg_host_directory_picker --> pkg_invariants pkg_host_directory_picker_browse --> pkg_invariants pkg_host_directory_picker_native --> pkg_invariants @@ -429,6 +430,7 @@ flowchart TD pkg_llm --> pkg_brand pkg_llm --> pkg_invariants pkg_llm --> pkg_timeout + pkg_llm --> pkg_typert_protocol pkg_attachment_local --> pkg_attachment pkg_attachment_local --> pkg_home_paths pkg_attachment_local --> pkg_invariants @@ -443,6 +445,10 @@ flowchart TD pkg_experimental_inspector --> pkg_client_modules pkg_experimental_inspector --> pkg_host_webserver pkg_experimental_inspector --> pkg_invariants + pkg_experimental_webworker_runtime --> pkg_client_modules + pkg_experimental_webworker_runtime --> pkg_host_apiproxy + pkg_experimental_webworker_runtime --> pkg_host_webserver + pkg_experimental_webworker_runtime --> pkg_invariants pkg_session --> pkg_brand pkg_session --> pkg_invariants pkg_session --> pkg_llm @@ -541,14 +547,8 @@ flowchart TD pkg_web_search_deepseek --> pkg_web pkg_spill_local --> pkg_invariants pkg_spill_local --> pkg_spill - pkg_api_settings_controller --> pkg_credentials - pkg_api_settings_controller --> pkg_invariants - pkg_api_settings_controller --> pkg_session - pkg_api_settings_controller --> pkg_settings - pkg_api_settings_controller --> pkg_typert_protocol pkg_file_reference --> pkg_agent pkg_file_reference --> pkg_invariants - pkg_file_reference --> pkg_typert_protocol pkg_time_context --> pkg_agent pkg_time_context --> pkg_invariants pkg_time_context --> pkg_session @@ -1013,9 +1013,27 @@ flowchart TD pkg_acp --> pkg_session_persistence pkg_acp --> pkg_token_meter pkg_acp --> pkg_user_approval + pkg_api_settings_controller --> pkg_agent_presets + pkg_api_settings_controller --> pkg_credentials + pkg_api_settings_controller --> pkg_invariants + pkg_api_settings_controller --> pkg_native_command + pkg_api_settings_controller --> pkg_session + pkg_api_settings_controller --> pkg_settings + pkg_api_settings_controller --> pkg_typert_protocol pkg_web_app --> pkg_invariants pkg_web_app --> pkg_shell_env pkg_web_app --> pkg_system_prompt + pkg_client_connection --> pkg_attachment + 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 + pkg_client_connection --> pkg_session + pkg_client_connection --> pkg_settings + pkg_client_connection --> pkg_tool_todo pkg_compaction_tool_result_pruner --> pkg_compaction pkg_compaction_tool_result_pruner --> pkg_invariants pkg_compaction_tool_result_pruner --> pkg_llm @@ -1029,8 +1047,6 @@ flowchart TD pkg_tool_cordis --> pkg_session pkg_tool_cordis --> pkg_system_prompt pkg_tool_cordis --> pkg_tools - pkg_host_apiproxy --> pkg_agent_presets - pkg_host_apiproxy --> pkg_invariants pkg_tool_bash --> pkg_agent pkg_tool_bash --> pkg_invariants pkg_tool_bash --> pkg_jobs @@ -1092,17 +1108,11 @@ flowchart TD pkg_tool_session_query --> pkg_system_prompt pkg_tool_session_query --> pkg_timeout pkg_tool_session_query --> pkg_tools - pkg_client_connection --> pkg_attachment - 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 - pkg_client_connection --> pkg_session - pkg_client_connection --> pkg_settings - pkg_client_connection --> pkg_tool_todo + pkg_api_gateway --> pkg_brand + pkg_api_gateway --> pkg_client_connection + pkg_api_gateway --> pkg_host_webserver + pkg_api_gateway --> pkg_invariants + pkg_api_gateway --> pkg_typert_registry pkg_compaction_basic --> pkg_agent pkg_compaction_basic --> pkg_commands pkg_compaction_basic --> pkg_compaction @@ -1144,10 +1154,9 @@ flowchart TD pkg_agent_spine_demo --> pkg_tool_jobs pkg_agent_spine_demo --> pkg_tool_skill pkg_agent_spine_demo --> pkg_tools - pkg_experimental_webworker_runtime --> pkg_client_modules - pkg_experimental_webworker_runtime --> pkg_host_apiproxy - pkg_experimental_webworker_runtime --> pkg_host_webserver - pkg_experimental_webworker_runtime --> pkg_invariants + pkg_host_frontend_static --> pkg_client_connection + pkg_host_frontend_static --> pkg_host_webserver + pkg_host_frontend_static --> pkg_invariants pkg_webhook_github --> pkg_credentials pkg_webhook_github --> pkg_host_webserver pkg_webhook_github --> pkg_invariants @@ -1207,11 +1216,39 @@ flowchart TD pkg_hooks_claude_code --> pkg_session_persistence pkg_hooks_claude_code --> pkg_subagent pkg_hooks_claude_code --> pkg_tools - pkg_api_gateway --> pkg_brand - pkg_api_gateway --> pkg_client_connection - pkg_api_gateway --> pkg_host_webserver - pkg_api_gateway --> pkg_invariants - pkg_api_gateway --> pkg_typert_registry + pkg_api_session_controller --> pkg_agent + pkg_api_session_controller --> pkg_agent_default_model + pkg_api_session_controller --> pkg_agent_presets + pkg_api_session_controller --> pkg_api_gateway + pkg_api_session_controller --> pkg_attachment + pkg_api_session_controller --> pkg_brand + pkg_api_session_controller --> pkg_client_connection + pkg_api_session_controller --> pkg_file_reference + pkg_api_session_controller --> pkg_invariants + pkg_api_session_controller --> pkg_jobs + pkg_api_session_controller --> pkg_llm + pkg_api_session_controller --> pkg_native_command + pkg_api_session_controller --> pkg_scope + pkg_api_session_controller --> pkg_session + pkg_api_session_controller --> pkg_session_persistence + pkg_api_session_controller --> pkg_session_projection + pkg_api_session_controller --> pkg_session_projection_cache + pkg_api_session_controller --> pkg_session_query + pkg_api_session_controller --> pkg_session_title + pkg_api_session_controller --> pkg_skill + pkg_api_session_controller --> pkg_subagent + pkg_api_session_controller --> pkg_typert_protocol + pkg_api_session_controller --> pkg_typert_registry + pkg_api_session_controller --> pkg_util_workspace_path + 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 + pkg_api_workspace_controller --> pkg_typert_protocol + pkg_api_workspace_controller --> pkg_workspace pkg_experimental_agent_team --> pkg_agent pkg_experimental_agent_team --> pkg_brand pkg_experimental_agent_team --> pkg_invariants @@ -1220,9 +1257,6 @@ flowchart TD pkg_experimental_agent_team --> pkg_session_persistence pkg_experimental_agent_team --> pkg_subagent pkg_experimental_agent_team --> pkg_typert_protocol - pkg_host_frontend_static --> pkg_client_connection - pkg_host_frontend_static --> pkg_host_webserver - pkg_host_frontend_static --> pkg_invariants pkg_sdk_protocol --> pkg_invariants pkg_sdk_protocol --> pkg_llm pkg_sdk_protocol --> pkg_session @@ -1250,36 +1284,30 @@ flowchart TD pkg_subagent_spawn_in_process --> pkg_invariants pkg_subagent_spawn_in_process --> pkg_subagent pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver - pkg_api_session_controller --> pkg_agent - pkg_api_session_controller --> pkg_agent_default_model - pkg_api_session_controller --> pkg_agent_presets - pkg_api_session_controller --> pkg_api_gateway - pkg_api_session_controller --> pkg_attachment - pkg_api_session_controller --> pkg_brand - pkg_api_session_controller --> pkg_client_connection - pkg_api_session_controller --> pkg_invariants - pkg_api_session_controller --> pkg_jobs - pkg_api_session_controller --> pkg_llm - pkg_api_session_controller --> pkg_scope - pkg_api_session_controller --> pkg_session - pkg_api_session_controller --> pkg_session_persistence - pkg_api_session_controller --> pkg_session_projection - pkg_api_session_controller --> pkg_session_projection_cache - pkg_api_session_controller --> pkg_session_query - pkg_api_session_controller --> pkg_session_title - pkg_api_session_controller --> pkg_subagent - pkg_api_session_controller --> pkg_typert_protocol - pkg_api_session_controller --> pkg_typert_registry - pkg_api_session_controller --> pkg_util_workspace_path - 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 - pkg_api_workspace_controller --> pkg_typert_protocol - pkg_api_workspace_controller --> pkg_workspace + pkg_api_remotes --> pkg_agent_presets + pkg_api_remotes --> pkg_api_gateway + pkg_api_remotes --> pkg_api_session_controller + pkg_api_remotes --> pkg_api_settings_controller + pkg_api_remotes --> pkg_api_workspace_controller + pkg_api_remotes --> pkg_commands + pkg_api_remotes --> pkg_cordis_host_runner + pkg_api_remotes --> pkg_credentials + pkg_api_remotes --> pkg_file_reference + pkg_api_remotes --> pkg_goal + pkg_api_remotes --> pkg_host_plugin_inventory + pkg_api_remotes --> pkg_invariants + pkg_api_remotes --> pkg_llm + pkg_api_remotes --> pkg_message_feedback + pkg_api_remotes --> pkg_session + pkg_api_remotes --> pkg_session_reference + pkg_api_remotes --> pkg_settings + pkg_api_remotes --> pkg_subagent + pkg_api_remotes --> pkg_user_approval + pkg_api_remotes --> pkg_user_questions + pkg_client_ui_session --> pkg_api_session_controller + pkg_client_ui_session --> pkg_client_ui_renderer + pkg_client_ui_session --> pkg_invariants + pkg_client_ui_session --> pkg_session pkg_experimental_tool_agent_team --> pkg_agent pkg_experimental_tool_agent_team --> pkg_experimental_agent_team pkg_experimental_tool_agent_team --> pkg_invariants @@ -1306,30 +1334,6 @@ flowchart TD pkg_subagent_dsh_sdk --> pkg_session pkg_subagent_dsh_sdk --> pkg_subagent pkg_subagent_dsh_sdk --> pkg_subprocess - pkg_api_remotes --> pkg_agent_presets - pkg_api_remotes --> pkg_api_gateway - pkg_api_remotes --> pkg_api_session_controller - pkg_api_remotes --> pkg_api_settings_controller - pkg_api_remotes --> pkg_api_workspace_controller - pkg_api_remotes --> pkg_commands - pkg_api_remotes --> pkg_cordis_host_runner - pkg_api_remotes --> pkg_credentials - pkg_api_remotes --> pkg_file_reference - pkg_api_remotes --> pkg_goal - pkg_api_remotes --> pkg_host_plugin_inventory - pkg_api_remotes --> pkg_invariants - pkg_api_remotes --> pkg_llm - pkg_api_remotes --> pkg_message_feedback - pkg_api_remotes --> pkg_session - pkg_api_remotes --> pkg_session_reference - pkg_api_remotes --> pkg_settings - pkg_api_remotes --> pkg_subagent - pkg_api_remotes --> pkg_user_approval - pkg_api_remotes --> pkg_user_questions - pkg_client_ui_session --> pkg_api_session_controller - pkg_client_ui_session --> pkg_client_ui_renderer - pkg_client_ui_session --> pkg_invariants - pkg_client_ui_session --> pkg_session pkg_client_ui_settings --> pkg_api_remotes pkg_client_ui_settings --> pkg_client_connection pkg_client_ui_settings --> pkg_invariants @@ -1341,7 +1345,6 @@ flowchart TD pkg_client_locale --> pkg_invariants pkg_client_locale --> pkg_settings pkg_client_ui_settings_models --> pkg_api_remotes - pkg_client_ui_settings_models --> pkg_client_connection pkg_client_ui_settings_models --> pkg_client_locale pkg_client_ui_settings_models --> pkg_client_ui_renderer pkg_client_ui_settings_models --> pkg_client_ui_settings @@ -1543,7 +1546,6 @@ flowchart TD pkg_client_ui_chat --> pkg_settings pkg_client_ui_chat --> pkg_token_meter pkg_client_ui_chat --> pkg_tools - pkg_client_ui_chat --> pkg_util_workspace_path pkg_client_ui_commands --> pkg_api_remotes pkg_client_ui_commands --> pkg_api_session_controller pkg_client_ui_commands --> pkg_client_locale @@ -1627,7 +1629,6 @@ flowchart TD pkg_client_ui_message_feedback --> pkg_typert_protocol pkg_client_ui_model_selection --> pkg_api_remotes pkg_client_ui_model_selection --> pkg_api_session_controller - pkg_client_ui_model_selection --> pkg_client_connection pkg_client_ui_model_selection --> pkg_client_locale pkg_client_ui_model_selection --> pkg_client_ui_commands pkg_client_ui_model_selection --> pkg_client_ui_conversation @@ -1732,6 +1733,7 @@ flowchart TD | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`experimental-agent-team-web-profile`](../packages/experimental/agent-team-web-profile) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) | @@ -1755,11 +1757,12 @@ flowchart TD | [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) | | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) | -| [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`timeout`](../packages/util/timeout) | +| [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`timeout`](../packages/util/timeout), [`typert-protocol`](../packages/typert/protocol) | | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment) | | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-modules`](../packages/client/modules), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`typert-protocol`](../packages/typert/protocol) | | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | @@ -1787,8 +1790,7 @@ flowchart TD | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) | | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) | -| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) | -| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) | +| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) | | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | @@ -1872,21 +1874,22 @@ flowchart TD | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`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-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) | | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) | +| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) | | [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) | +| [`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), [`settings`](../packages/settings/settings), [`tool-todo`](../packages/todo/tool-todo) | | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`host-apiproxy`](../packages/host/apiproxy) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) | | [`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-directory-picker`](../packages/host/directory-picker), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`tool-todo`](../packages/todo/tool-todo) | +| [`api-gateway`](../packages/api/gateway) | `api` | [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) | | [`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-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), [`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) | -| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-modules`](../packages/client/modules), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`webhook-github`](../packages/webhook/webhook-github) | `webhook` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`webhook`](../packages/webhook/webhook) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | @@ -1896,25 +1899,23 @@ flowchart TD | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | -| [`api-gateway`](../packages/api/gateway) | `api` | [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) | +| [`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), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`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), [`skill`](../packages/skill/skill), [`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), [`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-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | -| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`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), [`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) | +| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-settings-controller`](../packages/api/settings-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) | +| [`client-ui-session`](../packages/client/ui-session) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`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) | | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | -| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-settings-controller`](../packages/api/settings-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) | -| [`client-ui-session`](../packages/client/ui-session) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) | | [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) | -| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | +| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) | | [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) | @@ -1935,7 +1936,7 @@ flowchart TD | [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`attachment`](../packages/attachment/attachment), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`client-ui-user-questions`](../packages/client/ui-user-questions) | `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), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions) | | [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`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-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | -| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`util-workspace-path`](../packages/util/workspace-path) | +| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | | [`client-ui-commands`](../packages/client/ui-commands) | `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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) | | [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) | @@ -1945,7 +1946,7 @@ flowchart TD | [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`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), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | -| [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | +| [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) | | [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`api-remotes`](../packages/api/remotes), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`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), [`util-workspace-path`](../packages/util/workspace-path) | | [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`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), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) | diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml index 7b91ec4729..19974d41f8 100644 --- a/docs/subsystems/llm-streaming.i18n.yaml +++ b/docs/subsystems/llm-streaming.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/llm-streaming.md -llm-streaming.md: 331c30e1039741462df7ae3e7a9e6497281a5df0 -llm-streaming.zh.md: f05c67bc7552a4bc5a9359416d98b62f238f1c65 +llm-streaming.md: e37356d91e75334987b6fc707744f2376df1b2fc +llm-streaming.zh.md: 05f24bed6beff9508db88e60af7b259eda33f474 diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index 331c30e103..e37356d91e 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -659,8 +659,6 @@ interface LlmModelDiscoveryRequest { api?: string /** Credential for this interrogation alone; the harness never stores it. */ apiKey?: string - /** Caller cancellation; implementations must settle promptly after it aborts. */ - signal?: AbortSignal } ``` @@ -883,7 +881,7 @@ registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHa * Describe provider routes with a registered adapter. * @returns detached provider metadata in registration order. */ -listProviders(): LlmProviderInfo[] +@Remote listProviders(): LlmProviderInfo[] /** * Declare provider routes an adapter plugin can activate through @@ -899,7 +897,7 @@ registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): Dire * List every declared configurable provider, registered or dormant. * @returns detached directory entries in declaration order. */ -listConfigurableProviders(): LlmConfigurableProvider[] +@Remote listConfigurableProviders(): LlmConfigurableProvider[] /** * Offer to interrogate provider endpoints on behalf of the settings @@ -908,10 +906,10 @@ listConfigurableProviders(): LlmConfigurableProvider[] * directory, and because a provider being *added* has no route to name yet. * Disposed with the fiber. * @param settingsNs - the namespace whose profiles this discovery serves. - * @param discover - interrogates one endpoint; must honor `request.signal`. + * @param discover - interrogates one endpoint and must honor the supplied signal. * @returns the disposer that withdraws the offer. */ -registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise, ): () => void +registerModelDiscovery( settingsNs: string, discover: ( request: LlmModelDiscoveryRequest, signal?: AbortSignal, ) => Promise, ): () => void /** * Interrogate one provider endpoint for the models it advertises. The @@ -920,9 +918,20 @@ registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscover * candidate metadata a surface may offer for adoption. * @param settingsNs - namespace whose registered discovery serves this draft. * @param request - the endpoint, protocol, and one-shot credential to use. + * @param signal - caller cancellation. * @returns the advertised models, deduplicated in endpoint order. */ -async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, ): Promise +async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, signal?: AbortSignal, ): Promise + +/** + * Remote adapter for one draft provider interrogation. + * @param settingsNs - namespace whose registered discovery serves this draft. + * @param request - endpoint, protocol, and one-shot credential to use. + * @param signal - caller cancellation supplied by the Remote carrier. + * @returns advertised models in endpoint order. + * @throws TypertRemoteFailure with `model-discovery-failed` when discovery refuses or fails. + */ +@Remote('discoverModels') async remoteDiscoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, signal: AbortSignal, ): Promise /** * Resolve the retry policy captured when one provider route was registered. diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index f05c67bc75..05f24bed6b 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -665,8 +665,6 @@ interface LlmModelDiscoveryRequest { api?: string /** Credential for this interrogation alone; the harness never stores it. */ apiKey?: string - /** Caller cancellation; implementations must settle promptly after it aborts. */ - signal?: AbortSignal } ``` @@ -889,7 +887,7 @@ registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHa * Describe provider routes with a registered adapter. * @returns detached provider metadata in registration order. */ -listProviders(): LlmProviderInfo[] +@Remote listProviders(): LlmProviderInfo[] /** * Declare provider routes an adapter plugin can activate through @@ -905,7 +903,7 @@ registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): Dire * List every declared configurable provider, registered or dormant. * @returns detached directory entries in declaration order. */ -listConfigurableProviders(): LlmConfigurableProvider[] +@Remote listConfigurableProviders(): LlmConfigurableProvider[] /** * Offer to interrogate provider endpoints on behalf of the settings @@ -914,10 +912,10 @@ listConfigurableProviders(): LlmConfigurableProvider[] * directory, and because a provider being *added* has no route to name yet. * Disposed with the fiber. * @param settingsNs - the namespace whose profiles this discovery serves. - * @param discover - interrogates one endpoint; must honor `request.signal`. + * @param discover - interrogates one endpoint and must honor the supplied signal. * @returns the disposer that withdraws the offer. */ -registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise, ): () => void +registerModelDiscovery( settingsNs: string, discover: ( request: LlmModelDiscoveryRequest, signal?: AbortSignal, ) => Promise, ): () => void /** * Interrogate one provider endpoint for the models it advertises. The @@ -926,9 +924,20 @@ registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscover * candidate metadata a surface may offer for adoption. * @param settingsNs - namespace whose registered discovery serves this draft. * @param request - the endpoint, protocol, and one-shot credential to use. + * @param signal - caller cancellation. * @returns the advertised models, deduplicated in endpoint order. */ -async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, ): Promise +async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, signal?: AbortSignal, ): Promise + +/** + * Remote adapter for one draft provider interrogation. + * @param settingsNs - namespace whose registered discovery serves this draft. + * @param request - endpoint, protocol, and one-shot credential to use. + * @param signal - caller cancellation supplied by the Remote carrier. + * @returns advertised models in endpoint order. + * @throws TypertRemoteFailure with `model-discovery-failed` when discovery refuses or fails. + */ +@Remote('discoverModels') async remoteDiscoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, signal: AbortSignal, ): Promise /** * Resolve the retry policy captured when one provider route was registered. diff --git a/docs/subsystems/session-reference.i18n.yaml b/docs/subsystems/session-reference.i18n.yaml index 9f48576308..29ba36ab67 100644 --- a/docs/subsystems/session-reference.i18n.yaml +++ b/docs/subsystems/session-reference.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/session-reference.md -session-reference.md: 429459f37132a379d18251af646181bc29d97d40 -session-reference.zh.md: b505498e560c2c5b8f211be7650cc39a16b578c3 +session-reference.md: 1dd5cc1ee8c594b34015f9bf2d765f68621b86a1 +session-reference.zh.md: 75b5018a6afbd1bbe21f303013e0e7fa0d1f89ab diff --git a/docs/subsystems/session-reference.md b/docs/subsystems/session-reference.md index 429459f371..1dd5cc1ee8 100644 --- a/docs/subsystems/session-reference.md +++ b/docs/subsystems/session-reference.md @@ -119,22 +119,33 @@ Host capability for cancellable file-reference discovery. * @returns deterministic path-only candidates. */ abstract list( agent: Agent, query: string, signal: AbortSignal, ): Promise - -/** - * Remote face of {@link list}; the decorator cannot mark the abstract - * member, so this concrete adapter carries the identical contract. - * @param agent - target agent whose session cwd bounds discovery. - * @param query - path text following `@` or `@"`. - * @param signal - caller cancellation. - * @returns deterministic path-only candidates. - */ -@Remote('list') remoteExportList( agent: Agent, query: string, signal: AbortSignal, ): Promise ``` Types: [Agent](core.md) Source: [`packages/context/file-reference/src/index.ts`](../../packages/context/file-reference/src/index.ts) + + +### `ctx.sessionFileReferences` — `SessionFileReferences` + +Host Remote adapter over the composed file-reference provider. + +```ts cordis-catalog +/** + * List file and directory candidates for one Agent's working directory. + * @param agent - target Agent resolved from the Session identity on the wire. + * @param query - path text following `@` or `@"`. + * @param signal - caller cancellation. + * @returns deterministic path-only candidates from the composed provider. + */ +@Remote list( agent: Agent, query: string, signal: AbortSignal, ): Promise +``` + +Types: [Agent](core.md) + +Source: [`packages/api/session-controller/src/file-references.ts`](../../packages/api/session-controller/src/file-references.ts) + ### `ctx.sessionReferenceResolver` — `SessionReferenceResolver` diff --git a/docs/subsystems/session-reference.zh.md b/docs/subsystems/session-reference.zh.md index b505498e56..75b5018a6a 100644 --- a/docs/subsystems/session-reference.zh.md +++ b/docs/subsystems/session-reference.zh.md @@ -119,22 +119,33 @@ Host capability for cancellable file-reference discovery. * @returns deterministic path-only candidates. */ abstract list( agent: Agent, query: string, signal: AbortSignal, ): Promise - -/** - * Remote face of {@link list}; the decorator cannot mark the abstract - * member, so this concrete adapter carries the identical contract. - * @param agent - target agent whose session cwd bounds discovery. - * @param query - path text following `@` or `@"`. - * @param signal - caller cancellation. - * @returns deterministic path-only candidates. - */ -@Remote('list') remoteExportList( agent: Agent, query: string, signal: AbortSignal, ): Promise ``` Types: [Agent](core.zh.md) Source: [`packages/context/file-reference/src/index.ts`](../../packages/context/file-reference/src/index.ts) + + +### `ctx.sessionFileReferences` — `SessionFileReferences` + +Host Remote adapter over the composed file-reference provider. + +```ts cordis-catalog +/** + * List file and directory candidates for one Agent's working directory. + * @param agent - target Agent resolved from the Session identity on the wire. + * @param query - path text following `@` or `@"`. + * @param signal - caller cancellation. + * @returns deterministic path-only candidates from the composed provider. + */ +@Remote list( agent: Agent, query: string, signal: AbortSignal, ): Promise +``` + +Types: [Agent](core.zh.md) + +Source: [`packages/api/session-controller/src/file-references.ts`](../../packages/api/session-controller/src/file-references.ts) + ### `ctx.sessionReferenceResolver` — `SessionReferenceResolver` diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 80666a3c88..423b9a6668 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.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/session.md -session.md: 87e8b33dec8eda6f7a716d71f5c4afa69a420d35 -session.zh.md: d30b7d24412a04521300c657c1b07daa1620ca5f +session.md: fe3f71de6aeb6b5d9924fa88c94688f7eac8ff0a +session.zh.md: 844fd7a057c853fcbda6add875b997069ab024de diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index 87e8b33dec..fe3f71de6a 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -577,6 +577,12 @@ What a persistence backend relies on: the durable log persists every event lossl The backends that consume this contract are on [persistence.md](persistence.md). +## Remote catalog and workspace opening + +`ModelCatalog` is the Host-generation model directory returned by `session/modelCatalog`: it carries the deployment default, routable provider ids, successful provider groups, and isolated provider failures. It is not derived from one Session and remains separate from Session projections. + +`SessionOpenWorkspacePathRequest` carries a `sessionId` and an absolute or Session-workspace-relative `path`. `SessionOpenWorkspacePathValue` confirms that the Host accepted the native handoff. The controller inspects the Session without activating its Agent, resolves a relative path against the recorded cwd, and reports missing Sessions, cancellation, and opener failures through the Session Remote error vocabulary. + @@ -637,6 +643,21 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH */ @Remote('selectModel') selectModel(request: SessionSelectModelRequest): Promise +/** + * Describe every currently routable model for Host-generation selectors. + * @returns provider-grouped models, the deployment default, and isolated provider failures. + */ +@Remote('modelCatalog') modelCatalog(): Promise + +/** + * Open a path resolved against one Session's workspace on the Host desktop. + * @param request - Session identity and absolute or workspace-relative path. + * @param signal - caller lifetime; abort terminates inspection or the native command. + * @returns confirmation after the native opener accepts the path. + * @throws TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails. + */ +@Remote('openWorkspacePath') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise + /** * Rename one Session after explicitly resuming it. * @param request - Session identity and proposed title. diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index d30b7d2441..844fd7a057 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -581,6 +581,12 @@ interface TurnEndReasonMap { 消费此约定的后端见 [persistence.md](persistence.zh.md)。 +## Remote 目录与 workspace 打开 + +`ModelCatalog` 是 `session/modelCatalog` 返回的 Host generation 模型目录:它携带部署默认值、可路由 provider id、成功的 provider 分组与相互隔离的 provider 失败。它不由某个 Session 派生,因此与 Session projection 分开保存。 + +`SessionOpenWorkspacePathRequest` 携带 `sessionId` 与绝对路径或相对于 Session workspace 的 `path`。`SessionOpenWorkspacePathValue` 确认 Host 已接受原生交接。controller 在不激活 Agent 的前提下检查 Session,基于记录的 cwd 解析相对路径,并通过 Session Remote 错误词汇表报告 Session 缺失、取消与打开器失败。 + @@ -641,6 +647,21 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH */ @Remote('selectModel') selectModel(request: SessionSelectModelRequest): Promise +/** + * Describe every currently routable model for Host-generation selectors. + * @returns provider-grouped models, the deployment default, and isolated provider failures. + */ +@Remote('modelCatalog') modelCatalog(): Promise + +/** + * Open a path resolved against one Session's workspace on the Host desktop. + * @param request - Session identity and absolute or workspace-relative path. + * @param signal - caller lifetime; abort terminates inspection or the native command. + * @returns confirmation after the native opener accepts the path. + * @throws TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails. + */ +@Remote('openWorkspacePath') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise + /** * Rename one Session after explicitly resuming it. * @param request - Session identity and proposed title. diff --git a/docs/subsystems/settings.i18n.yaml b/docs/subsystems/settings.i18n.yaml index 0c345cf749..5b83c19297 100644 --- a/docs/subsystems/settings.i18n.yaml +++ b/docs/subsystems/settings.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/settings.md -settings.md: 59bd06a6e7a8659097c726b85db13caa46dd461f -settings.zh.md: a6bbdbce633a8b51167615f7838e55b698d48829 +settings.md: 467d0fc5fb4c97eeb6a18f36d879733e063bd36b +settings.zh.md: 42ed78f321021c5b7b55c51db6ef55b506550057 diff --git a/docs/subsystems/settings.md b/docs/subsystems/settings.md index 59bd06a6e7..467d0fc5fb 100644 --- a/docs/subsystems/settings.md +++ b/docs/subsystems/settings.md @@ -161,6 +161,10 @@ Every committed change — an in-process write or an externally observed provide type SettingsUpdateSource = 'update' | 'provider' ``` +## Native document operations + +`SettingsDocumentOpenValue` confirms that `settings/openSettingsDocument` prepared the provider-owned document and handed it to the native text editor. `AgentPresetDirectoryOpenValue` reports either a completed native handoff or the resolved user-preset directory when desktop opening is unavailable. Neither operation accepts a browser-selected Host path. + @@ -300,6 +304,23 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write. */ @Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, ): Promise + +/** + * Materialize the provider-owned settings document and open it in a native text editor. + * @param signal - caller lifetime; abort terminates preparation or the native command. + * @returns confirmation after the native opener accepts the document. + * @throws TypertRemoteFailure when no document exists, preparation fails, or opening fails. + */ +@Remote async openSettingsDocument(signal: AbortSignal): Promise + +/** + * Open one user-authored Agent preset directory or return its path when no native opener exists. + * @param agentPreset - preset id resolved against Host-owned roots. + * @param signal - caller lifetime; abort terminates the native command. + * @returns an opened confirmation or the resolved directory for text display. + * @throws TypertRemoteFailure when the preset is missing, read-only, invalid, or cannot be opened. + */ +@Remote async openAgentPresetDirectory( agentPreset: string, signal: AbortSignal, ): Promise ``` Source: [`packages/api/settings-controller/src/index.ts`](../../packages/api/settings-controller/src/index.ts) diff --git a/docs/subsystems/settings.zh.md b/docs/subsystems/settings.zh.md index a6bbdbce63..42ed78f321 100644 --- a/docs/subsystems/settings.zh.md +++ b/docs/subsystems/settings.zh.md @@ -161,6 +161,10 @@ interface SettingsDescribeOptions { type SettingsUpdateSource = 'update' | 'provider' ``` +## 原生文档操作 + +`SettingsDocumentOpenValue` 确认 `settings/openSettingsDocument` 已准备好 provider 持有的文档,并将其交给原生文本编辑器。`AgentPresetDirectoryOpenValue` 报告已完成的原生交接,或在桌面打开不可用时返回解析后的用户 preset 目录。两项操作都不接受由浏览器选择的 Host 路径。 + @@ -300,6 +304,23 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote * @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write. */ @Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, ): Promise + +/** + * Materialize the provider-owned settings document and open it in a native text editor. + * @param signal - caller lifetime; abort terminates preparation or the native command. + * @returns confirmation after the native opener accepts the document. + * @throws TypertRemoteFailure when no document exists, preparation fails, or opening fails. + */ +@Remote async openSettingsDocument(signal: AbortSignal): Promise + +/** + * Open one user-authored Agent preset directory or return its path when no native opener exists. + * @param agentPreset - preset id resolved against Host-owned roots. + * @param signal - caller lifetime; abort terminates the native command. + * @returns an opened confirmation or the resolved directory for text display. + * @throws TypertRemoteFailure when the preset is missing, read-only, invalid, or cannot be opened. + */ +@Remote async openAgentPresetDirectory( agentPreset: string, signal: AbortSignal, ): Promise ``` Source: [`packages/api/settings-controller/src/index.ts`](../../packages/api/settings-controller/src/index.ts) diff --git a/docs/subsystems/skills.i18n.yaml b/docs/subsystems/skills.i18n.yaml index 93efc1068d..7bf2ead73e 100644 --- a/docs/subsystems/skills.i18n.yaml +++ b/docs/subsystems/skills.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/skills.md -skills.md: 5abfa84356dc947f8b3359ad5f0fc5c42a99553a -skills.zh.md: 87759b5ab5bdfc5e9ab10cf9a86147c16893efd3 +skills.md: 18eb4d3289cec5c7807feab4229096b5799d4168 +skills.zh.md: aa167373cb9736cf24d7bf20dc965e68568826be diff --git a/docs/subsystems/skills.md b/docs/subsystems/skills.md index 5abfa84356..18eb4d3289 100644 --- a/docs/subsystems/skills.md +++ b/docs/subsystems/skills.md @@ -234,6 +234,10 @@ Before each later model step, the consumer applies exact tool visibility and dig The model-facing `skill({ name })` tool validates the kebab-case name, finds the summary in the invocation-neutral catalog, rejects it before loading unless `isModelInvocable` permits access, then rereads the complete definition for the calling agent cwd and rechecks the policy before returning content. It reports an unresolved skill as unknown or no longer available and returns a tool result containing ``, ``, and ``. `resourceBase` resolves explicitly referenced scripts, references, and assets only as needed; the loaded result does not enumerate a skill directory. Body-only edits therefore change later tool calls without producing catalog messages or rewriting earlier tool results. +## Browser Session catalog + +`SkillListRequest` addresses one Session by `sessionId`; `SkillListValue` returns the user-invocable entries with name, description, optional usage guidance, and model-invocation availability. `SessionSkillCatalog` reads the Session cwd and recorded preset without activating an Agent. A live Agent may supply its scoped registry, while a cold Session uses the preset's standing scope. + @@ -242,6 +246,25 @@ The model-facing `skill({ name })` tool validates the kebab-case name, finds the Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + +### `ctx.sessionSkillCatalog` — `SessionSkillCatalog` + +Host service backing `ctx.remote.skills` without activating a cold Agent. + +```ts cordis-catalog +/** + * List the user-invocable skills visible to one Session composition. + * @param request - Session identity whose cwd and preset select the catalog view. + * @param signal - caller lifetime carried by the Remote transport; admitted catalog reads retain their existing completion semantics. + * @returns user-invocable skill metadata without loading skill bodies. + * @throws TypertRemoteFailure when the Session cannot be inspected or no registry can serve it. + */ +@Remote async list(request: SkillListRequest, signal: AbortSignal): Promise +``` + +Source: [`packages/api/session-controller/src/skill-catalog.ts`](../../packages/api/session-controller/src/skill-catalog.ts) + ### `ctx.skills` — `SkillRegistry` diff --git a/docs/subsystems/skills.zh.md b/docs/subsystems/skills.zh.md index 87759b5ab5..aa167373cb 100644 --- a/docs/subsystems/skills.zh.md +++ b/docs/subsystems/skills.zh.md @@ -234,6 +234,10 @@ interface Config { 面向模型的 `skill({ name })` 工具校验 kebab-case 名称,在与调用策略无关的目录中查找摘要,并在加载前通过 `isModelInvocable` 拒绝无权访问的 skill;随后它根据调用方 agent 的 cwd 重新读取完整定义,并在返回内容前再次检查策略。该工具将无法解析的 skill 报告为未知或已不可用,并返回包含 ``、`` 和 `` 的工具结果。`resourceBase` 仅按需解析显式引用的脚本、参考资料和资产;加载结果不枚举 skill 目录。因此,仅修改正文会改变后续工具调用,而不会生成目录消息或改写先前工具结果。 +## 浏览器 Session 目录 + +`SkillListRequest` 通过 `sessionId` 指定一个 Session;`SkillListValue` 返回允许用户调用的条目,其中包含名称、描述、可选使用提示与模型调用可用性。`SessionSkillCatalog` 在不激活 Agent 的前提下读取 Session cwd 与记录的 preset。live Agent 可以提供其作用域 registry,冷 Session 则使用 preset 的 standing scope。 + @@ -242,6 +246,25 @@ interface Config { Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + +### `ctx.sessionSkillCatalog` — `SessionSkillCatalog` + +Host service backing `ctx.remote.skills` without activating a cold Agent. + +```ts cordis-catalog +/** + * List the user-invocable skills visible to one Session composition. + * @param request - Session identity whose cwd and preset select the catalog view. + * @param signal - caller lifetime carried by the Remote transport; admitted catalog reads retain their existing completion semantics. + * @returns user-invocable skill metadata without loading skill bodies. + * @throws TypertRemoteFailure when the Session cannot be inspected or no registry can serve it. + */ +@Remote async list(request: SkillListRequest, signal: AbortSignal): Promise +``` + +Source: [`packages/api/session-controller/src/skill-catalog.ts`](../../packages/api/session-controller/src/skill-catalog.ts) + ### `ctx.skills` — `SkillRegistry` diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 4f66278d0d..41a4c618d3 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-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/session-controller/README.md -README.md: dc04e595c9c4bf029ed427d5a1214802041c11c0 -README.zh.md: b34c8ad018e4e2c0d95ac0b8aa1954742f9ecfa4 +README.md: c25a7b53908f140ce866c984938401116f8e0c54 +README.zh.md: 4cc8d550e1685d8f15c71279135ede5826011f5f diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index dc04e595c9..c25a7b5390 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -8,7 +8,7 @@ English | [中文](README.zh.md) ## Summary -`@deepseek-ai/dsh-api-session-controller` owns the Host `ctx.sessionController` service and the generated Client `ctx.remote.session` namespace. It serves Session list, search, creation, model selection, rename, fork, prompt, attachment, queue, cancellation, message-aligned history, live log following, and Host-wide control state. Use it through API Gateway when a Client needs these Session operations. +`@deepseek-ai/dsh-api-session-controller` owns the Host `ctx.sessionController` service and the generated Client `session`, `skills`, and `fileReferences` Remote namespaces. It serves Session lifecycle and history, the Host-generation model catalog, workspace-path opening, user-invocable skill discovery, and the adapter for Agent-scoped file references. Use it through API Gateway when a Client needs operations addressed by a Session. ## Table of Contents @@ -25,7 +25,7 @@ English | [中文](README.zh.md) History pages and follow opening snapshots carry a discriminated `SessionHistoryRecord`. Both variants use `{ type, event }`: `type: 'event'` carries one raw `SessionWireEvent`, while `type: 'chunks'` carries one lossless `ChunkRowEvent` for consecutive same-block `assistant/chunk` deltas. Both inner values expose `type`, `seq`, `time`, and `data`, so the Client retains each accepted record as one `SessionEventLikeEntry` without record-by-record conversion. A packed event's `seq` and `time` identify its first member, and `data` retains the fragment and timestamp-gap arrays. Live follow frames remain individual `event` records. Tool arguments, result content, failures, and `tool/result.data.meta` pass through unchanged; the controller does not resolve a Tool definition, run a presenter, or attach UI data. -Each endpoint states its activation policy. List, search, attachment, history pages, and log following can inspect persistence without activating an Agent; queue mutation and cancellation require the corresponding live state; model, rename, and prompt commands may explicitly resume an ordinary Session. Create and fork are the only operations that create a new Agent. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. +Each endpoint states its activation policy. List, search, attachment, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; queue mutation and cancellation require live state; model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Create and fork are the only operations that create a new Agent directly. The skill catalog instead uses a live Agent when present or the recorded preset's standing scope when cold, so listing never starts an Agent. The Client adapter exposes `SessionEventStream`, a Gateway `RemoteJournalStream` bound to one ordinary or direct-subagent address. It opens follow before the initial page, publishes only contiguous `replace`, `prepend`, and `append` changes, and repairs reconnect or sequence gaps through a tail page. Ordinary records cover `[event.seq, event.seq]`; packed rows cover `[event.seq, event.seq + memberCount - 1]`. A business, persistence, or unresolved continuity failure terminates the stream, while only physical carrier loss selects automatic resumption. `SessionControlStream` is a Gateway `RemoteSnapshotStream`; every generation opens with a complete process-local baseline, so reconnect replaces queue, jobs, and projection state instead of treating transient values as durable events. @@ -59,6 +59,7 @@ No direct effect; model requests remain owned by the Agent and LLM packages. - Control baselines represent process-local state and therefore cannot reconstruct jobs after a Host restart. - A failed follow resumption remains visible to the caller instead of retrying indefinitely. +- File-reference completion uses the shared Agent lookup and can resume a cold Session; the `skills/list` catalog is the non-activating alternative for skill metadata. diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index b34c8ad018..4cc8d550e1 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -8,7 +8,7 @@ kind: "package-reference" ## 概述 -`@deepseek-ai/dsh-api-session-controller` 拥有 Host 的 `ctx.sessionController` 服务和生成的 Client `ctx.remote.session` namespace。它提供 Session 列表、搜索、创建、模型选择、重命名、fork、prompt、附件、queue、取消、按消息对齐的历史、live 日志跟随和 Host 范围 control 状态。当 Client 需要这些 Session 操作时,请通过 API Gateway 使用它。 +`@deepseek-ai/dsh-api-session-controller` 拥有 Host 的 `ctx.sessionController` 服务,以及生成的 Client `session`、`skills` 和 `fileReferences` Remote namespace。它提供 Session 生命周期与历史、Host generation 模型目录、工作区路径打开、用户可调用 skill 发现,以及面向 Agent 的文件引用 adapter。当 Client 需要按 Session 寻址的操作时,请通过 API Gateway 使用它。 ## 目录 @@ -25,7 +25,7 @@ kind: "package-reference" 历史页与 follow opening snapshot 携带带判别字段的 `SessionHistoryRecord`。两个分支都使用 `{ type, event }`:`type: 'event'` 携带一个原始 `SessionWireEvent`,`type: 'chunks'` 则携带一个由连续且属于同一 block 的 `assistant/chunk` delta 组成的无损 `ChunkRowEvent`。两种内部值都公开 `type`、`seq`、`time` 与 `data`,因此 Client 无需逐 record 转换,就能把每条已接受 record 保留为一个 `SessionEventLikeEntry`。packed event 的 `seq` 与 `time` 表示首成员,`data` 保留 fragment 与 timestamp-gap 数组。实时 follow frame 继续携带单个 `event` record。工具参数、结果内容、失败信息和 `tool/result.data.meta` 原样通过;controller 不解析 Tool definition、不运行 presenter,也不附加 UI 数据。 -每个 endpoint 都声明自己的激活策略。列表、搜索、附件、历史页和日志跟随可以在不激活 Agent 的情况下检查 persistence;queue 变更和取消要求对应 live 状态仍然存在;模型、重命名和 prompt 命令可以显式恢复普通 Session。只有 create 和 fork 会创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。 +每个 endpoint 都声明自己的激活策略。列表、搜索、附件、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;queue 变更与取消要求 live 状态;模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。只有 create 与 fork 会直接创建新 Agent。skill 目录则优先使用已有 live Agent,否则使用所记录 preset 的常驻 scope,因此列表查询绝不会启动 Agent。 Client adapter 提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend` 和 `append` 变更,并通过 tail page 修复重连或 seq 缺口。普通 record 覆盖 `[event.seq, event.seq]`,packed row 覆盖 `[event.seq, event.seq + memberCount - 1]`。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 queue、jobs 和 projection 状态,而不会把瞬态值当作 durable event。 @@ -59,6 +59,7 @@ Session 对象还承载本地提交回显:`session.beginSubmission` 在调用 - Control baseline 表示进程本地状态,因此 Host 重启后无法重建 jobs。 - follow 恢复失败会对调用方可见,而不会无限重试。 +- 文件引用补全使用共享 Agent lookup,因此可能恢复冷 Session;`skills/list` 目录是不激活 Agent 的 skill 元数据读取路径。 diff --git a/packages/api/session-controller/tests/fake-api.client.ts b/packages/api/session-controller/tests/fake-api.client.ts index a0eae81529..f1369e4c8c 100644 --- a/packages/api/session-controller/tests/fake-api.client.ts +++ b/packages/api/session-controller/tests/fake-api.client.ts @@ -3,7 +3,7 @@ // deferred-controlled timing). Session streams are hand pumps: pushFollow/pushControl. import type { IApiClient, MessageId, - RpcError, RpcResponse, SessionId, SessionSearchItem, SkillEntry, + RpcError, RpcResponse, SessionId, SessionSearchItem, SubagentCatalog, SubagentInterruptReceipt, SubagentPromptReceipt, WorkspaceId, WorkspaceView, } from '@deepseek-ai/dsh-api-remotes/client' @@ -156,6 +156,8 @@ export class FakeApiClient implements IApiClient { () => Promise.resolve(ok({ attachment: { attachmentId: 'a' as never, mediaType: 'image/png', bytes: 1, width: 1, height: 1 }, data: 'AA==' })) onUpdateQueue: (payload: unknown) => Promise> = () => Promise.resolve(ok({ accepted: true as const })) onCancel: (payload: unknown) => Promise> = () => Promise.resolve(ok({ accepted: true as const })) + onOpenWorkspacePath: (payload: unknown) => Promise> = + () => Promise.resolve(remoteOk({ opened: true as const })) onDescribe: (payload: unknown) => Promise Promise.resolve(ok({ version: '0-fake', cwd: '/f', attachedSessions: 0, home: '/h', canOpenPath: true, })) - onOpenPath: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ opened: true as const })) - private readonly followConns = new Map[]>() private readonly controlConns: ValueStreamConn[] = [] private readonly workspaceConns: ValueStreamConn[] = [] @@ -196,7 +195,6 @@ export class FakeApiClient implements IApiClient { readonly host: IApiClient['host'] = { describe: (payload: unknown) => this.record('host.describe', payload, this.onDescribe(payload)), - openPath: (payload: unknown) => this.record('host.openPath', payload, this.onOpenPath(payload)), } onWorkspaceCreate: (payload: unknown) => Promise> = @@ -217,37 +215,6 @@ export class FakeApiClient implements IApiClient { onWorkspaceArchiveSession: (payload: unknown) => Promise> = payload => Promise.resolve(remoteOk({ archivedSessionIds: [(payload as { sessionId: SessionId }).sessionId] })) - // Payloads stay `unknown` (lint-lane note above); response rows are the real - // wire shapes so cases can program requires-bearing catalogs and dual-address - // skill lists without casts. - onSkillList: (payload: unknown) => Promise> - = () => Promise.resolve(ok({ skills: [] })) - - - readonly agentPresets: IApiClient['agentPresets'] = { - openDocument: (payload: { agentPreset: string }) => - this.record('agentPreset.openDocument', payload, Promise.resolve(ok({ opened: true as const }))), - } - - readonly skills: IApiClient['skills'] = { - list: (payload: unknown) => this.record('skill.list', payload, this.onSkillList(payload)), - } - - readonly settings: IApiClient['settings'] = { - openDocument: payload => this.record('settings.openDocument', payload, Promise.resolve(ok({ opened: true as const }))), - } - - readonly llm: IApiClient['llm'] = { - providers: payload => this.record('llm.providers', payload, Promise.resolve(ok({ providers: [] }))), - models: payload => this.record('llm.models', payload, Promise.resolve(ok({ - default: { provider: 'fixture', model: 'fixture' }, - routableProviders: [], - groups: [], - failures: [], - }))), - discoverModels: payload => this.record('llm.discoverModels', payload, Promise.resolve(ok({ models: [] }))), - } - /** Remote namespaces bound to this fake's programmable unary slots and stream pumps. */ sessionRemotes(): RuntimeRemotes { return { @@ -259,6 +226,15 @@ export class FakeApiClient implements IApiClient { }, session: { list: payload => this.remoteResult('session.list', payload, this.onList(payload)), + modelCatalog: () => Promise.resolve({ + ok: true, + value: { + default: { provider: 'fixture', model: 'fixture' }, + routableProviders: [], + groups: [], + failures: [], + }, + }), search: (payload, signal) => { this.lastSearchSignal = signal return this.remoteResult('session.search', payload, this.onSearch(payload)) @@ -275,6 +251,11 @@ export class FakeApiClient implements IApiClient { attachment: payload => this.remoteResult('session.attachment', payload, this.onAttachment(payload)), updateQueue: payload => this.remoteResult('session.updateQueue', payload, this.onUpdateQueue(payload)), cancel: payload => this.remoteResult('session.cancel', payload, this.onCancel(payload)), + openWorkspacePath: payload => this.record( + 'session.openWorkspacePath', + payload, + this.onOpenWorkspacePath(payload), + ), page: request => this.page(request), follow: (request, signal) => this.openFollow(request, signal), control: signal => this.openControl(signal), diff --git a/packages/api/session-controller/tests/file-references.host.spec.ts b/packages/api/session-controller/tests/file-references.host.spec.ts new file mode 100644 index 0000000000..89e6e9f667 --- /dev/null +++ b/packages/api/session-controller/tests/file-references.host.spec.ts @@ -0,0 +1,20 @@ +import { Context } from '@deepseek-ai/cordis' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types' +import { describe, expect, it, vi } from 'vitest' +import { SessionFileReferences } from '../src/file-references.ts' + +describe('SessionFileReferences', () => { + it('delegates the resolved Agent, query, and cancellation signal unchanged', async () => { + const ctx = new Context() + const candidates: FileReferenceCandidate[] = [{ path: 'src', kind: 'directory' }] + const list = vi.fn(() => Promise.resolve(candidates)) + ctx.provide('fileReferences', { list } as never) + const adapter = new SessionFileReferences(ctx) + const agent = { id: 'target' } as unknown as Agent + const signal = new AbortController().signal + + await expect(adapter.list(agent, 'sr', signal)).resolves.toBe(candidates) + expect(list).toHaveBeenCalledWith(agent, 'sr', signal) + }) +}) diff --git a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts new file mode 100644 index 0000000000..f8c89efd5e --- /dev/null +++ b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts @@ -0,0 +1,95 @@ +import { Context } from '@deepseek-ai/cordis' +import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' +import { describe, expect, it, vi } from 'vitest' +import { createSessionTestRemote, testSessionPersistence } from './test-remote.ts' + +async function context(): Promise { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(AgentRegistry) + return ctx +} + +describe('session/openWorkspacePath', () => { + it('resolves a relative path against the attached Session cwd', async () => { + const ctx = await context() + const sessionId = SessionId('open-relative') + ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } }) + const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) + const remote = createSessionTestRemote(ctx, { + defaultModelSelection: () => ({ provider: 'p', model: 'm' }), + cwd: '/default', + openPath, + }) + const signal = new AbortController().signal + + await expect(remote.openWorkspacePath({ sessionId, path: 'src/a.ts' }, signal)) + .resolves.toEqual({ ok: true, value: { opened: true } }) + expect(openPath).toHaveBeenCalledWith('/workspace/project/src/a.ts', signal) + expect(ctx.agents.list()).toEqual([]) + }) + + it('preserves absolute paths and cwd-less Session paths', async () => { + const ctx = await context() + const withCwd = SessionId('open-absolute') + const withoutCwd = SessionId('open-without-cwd') + ctx.sessions.create(withCwd, { meta: { cwd: '/workspace/project' } }) + ctx.sessions.create(withoutCwd) + const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) + const remote = createSessionTestRemote(ctx, { + defaultModelSelection: () => ({ provider: 'p', model: 'm' }), + cwd: '/default', + openPath, + }) + + await remote.openWorkspacePath({ sessionId: withCwd, path: '/tmp/result.html' }) + await remote.openWorkspacePath({ sessionId: withoutCwd, path: 'result.html' }) + expect(openPath.mock.calls.map(call => call[0])).toEqual(['/tmp/result.html', 'result.html']) + }) + + it('rejects empty paths and missing Sessions before opening anything', async () => { + const ctx = await context() + const sessionId = SessionId('open-validation') + ctx.provide('sessionPersistence', testSessionPersistence(ctx, { + list: () => Promise.resolve([]), + inspect: () => Promise.resolve(undefined), + }) as never) + const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) + const remote = createSessionTestRemote(ctx, { + defaultModelSelection: () => ({ provider: 'p', model: 'm' }), + cwd: '/default', + openPath, + }) + + await expect(remote.openWorkspacePath({ sessionId, path: '' })) + .resolves.toMatchObject({ ok: false, error: { code: 'bad-request' } }) + await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' })) + .resolves.toMatchObject({ ok: false, error: { code: 'session-not-found' } }) + expect(openPath).not.toHaveBeenCalled() + }) + + it('preserves native opener failure and cancellation results', async () => { + const ctx = await context() + const sessionId = SessionId('open-failure') + ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } }) + const openPath = vi.fn((_path: string, _signal: AbortSignal) => + Promise.reject(new Error('desktop unavailable'))) + const remote = createSessionTestRemote(ctx, { + defaultModelSelection: () => ({ provider: 'p', model: 'm' }), + cwd: '/default', + openPath, + }) + + await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' })) + .resolves.toMatchObject({ + ok: false, + error: { code: 'internal', message: 'path open failed: desktop unavailable' }, + }) + + const aborted = new AbortController() + aborted.abort(new Error('cancelled')) + await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }, aborted.signal)) + .resolves.toMatchObject({ ok: false, error: { code: 'cancelled' } }) + }) +}) diff --git a/packages/api/session-controller/tests/session-skills.host.spec.ts b/packages/api/session-controller/tests/session-skills.host.spec.ts new file mode 100644 index 0000000000..e0640ddfe6 --- /dev/null +++ b/packages/api/session-controller/tests/session-skills.host.spec.ts @@ -0,0 +1,191 @@ +import { Context } from '@deepseek-ai/cordis' +import AgentRegistry from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' +import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' +import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query' +import type {} from '@deepseek-ai/dsh-skill' +import { describe, expect, it, vi } from 'vitest' +import { SessionSkillCatalog } from '../src/skill-catalog.ts' + +function observation( + sessionId: SessionId, + options: { readonly cwd?: string; readonly agentPreset?: string } = {}, +): SessionObservation { + const events = Object.freeze([]) + const lease = (): SessionObservation => ({ + source: 'live', + header: { + version: 0, + id: sessionId, + createdAt: 1, + ...options.cwd === undefined ? {} : { cwd: options.cwd }, + }, + events, + cursor: -1, + projections: { + asOfSeq: -1, + values: { + ...options.agentPreset === undefined ? {} : { agentPreset: options.agentPreset }, + }, + }, + retain: lease, + [Symbol.dispose]: () => {}, + }) + return lease() +} + +async function context(): Promise { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(AgentRegistry) + return ctx +} + +describe('SessionSkillCatalog', () => { + it('reads a cold Session catalog without resuming an Agent', async () => { + const ctx = await context() + const sessionId = SessionId('cold-skills') + const observed = observation(sessionId, { cwd: '/cold/project' }) + const dispose = vi.spyOn(observed, Symbol.dispose) + const observeSession = vi.fn(() => Promise.resolve(observed)) + ctx.provide('sessionQuery', { observeSession } as never) + const resume = vi.spyOn(ctx.agents, 'resume') + const list = vi.fn(() => Promise.resolve([ + { + name: 'review', + description: 'Review the current change.', + whenToUse: 'Before publishing.', + invocation: { modelInvocable: true, userInvocable: true }, + }, + { + name: 'model-only', + description: 'Not shown to the user.', + invocation: { modelInvocable: true, userInvocable: false }, + }, + ])) + ctx.provide('skills', { list } as never) + const catalog = new SessionSkillCatalog(ctx) + + await expect(catalog.list({ sessionId }, new AbortController().signal)).resolves.toEqual({ + skills: [{ + name: 'review', + description: 'Review the current change.', + whenToUse: 'Before publishing.', + modelInvocable: true, + }], + }) + expect(observeSession).toHaveBeenCalledWith(sessionId) + expect(dispose).toHaveBeenCalledOnce() + expect(resume).not.toHaveBeenCalled() + expect(ctx.agents.list()).toEqual([]) + expect(list).toHaveBeenCalledWith({ cwd: '/cold/project', scope: undefined }) + }) + + it('uses a live Agent to address a preset-owned registry', async () => { + const ctx = await context() + const sessionId = SessionId('live-skills') + const session = ctx.sessions.create(sessionId, { meta: { cwd: '/live/project' } }) + const agent = { id: sessionId, session, status: 'idle', ctx } as Agent + ctx.agents.register(agent) + ctx.provide('sessionQuery', { + observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/live/project' })), + } as never) + const scopedList = vi.fn(() => Promise.resolve([{ + name: 'preset-owned', + description: 'Composed for this Agent.', + invocation: { modelInvocable: false, userInvocable: true }, + }])) + const standingKeyFor = vi.fn() + ctx.provide('agentPresets', { + serviceFor: () => ({ list: scopedList }), + standingKeyFor, + } as never) + const catalog = new SessionSkillCatalog(ctx) + + await expect(catalog.list({ sessionId }, new AbortController().signal)).resolves.toEqual({ + skills: [{ + name: 'preset-owned', + description: 'Composed for this Agent.', + modelInvocable: false, + }], + }) + expect(scopedList).toHaveBeenCalledWith({ cwd: '/live/project', scope: agent }) + expect(standingKeyFor).not.toHaveBeenCalled() + }) + + it('uses the recorded preset standing scope for a cold Session', async () => { + const ctx = await context() + const sessionId = SessionId('standing-skills') + const scope = { agentPreset: 'minimal' } + ctx.provide('sessionQuery', { + observeSession: () => Promise.resolve(observation(sessionId, { + cwd: '/cold/project', + agentPreset: 'minimal', + })), + } as never) + const standingKeyFor = vi.fn(() => Promise.resolve(scope)) + ctx.provide('agentPresets', { standingKeyFor } as never) + const list = vi.fn(() => Promise.resolve([])) + ctx.provide('skills', { list } as never) + const catalog = new SessionSkillCatalog(ctx) + + await expect(catalog.list({ sessionId }, new AbortController().signal)).resolves.toEqual({ skills: [] }) + expect(standingKeyFor).toHaveBeenCalledWith('minimal') + expect(list).toHaveBeenCalledWith({ cwd: '/cold/project', scope }) + expect(ctx.agents.list()).toEqual([]) + }) + + it('falls back to the global registry when the recorded preset is unavailable', async () => { + const ctx = await context() + const sessionId = SessionId('gone-preset') + ctx.provide('sessionQuery', { + observeSession: () => Promise.resolve(observation(sessionId, { + cwd: '/cold/project', + agentPreset: 'gone', + })), + } as never) + ctx.provide('agentPresets', { + standingKeyFor: () => Promise.reject(new Error('unknown preset')), + } as never) + const list = vi.fn(() => Promise.resolve([])) + ctx.provide('skills', { list } as never) + const catalog = new SessionSkillCatalog(ctx) + + await expect(catalog.list({ sessionId }, new AbortController().signal)).resolves.toEqual({ skills: [] }) + expect(list).toHaveBeenCalledWith({ cwd: '/cold/project', scope: undefined }) + }) + + it.each([ + { + error: new SessionQueryError( + 'session "missing-skills" not found', + 'SESSION_QUERY_SESSION_NOT_FOUND', + ), + code: 'session-not-found', + }, + { error: new Error('storage offline'), code: 'internal' }, + ] as const)('classifies failed Session inspection as $code', async ({ error, code }) => { + const ctx = await context() + ctx.provide('sessionQuery', { observeSession: () => Promise.reject(error) } as never) + const catalog = new SessionSkillCatalog(ctx) + + await expect(catalog.list( + { sessionId: SessionId('missing-skills') }, + new AbortController().signal, + )).rejects.toMatchObject({ failure: { code } }) + }) + + it('reports an absent skill registry instead of an empty catalog', async () => { + const ctx = await context() + const sessionId = SessionId('no-skills') + ctx.provide('sessionQuery', { + observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/project' })), + } as never) + const catalog = new SessionSkillCatalog(ctx) + + await expect(catalog.list({ sessionId }, new AbortController().signal)) + .rejects.toMatchObject({ + failure: { code: 'internal', message: expect.stringContaining('skill registry is absent') }, + }) + }) +}) diff --git a/packages/api/session-controller/tests/test-remote.ts b/packages/api/session-controller/tests/test-remote.ts index ac131ab628..f5766382cb 100644 --- a/packages/api/session-controller/tests/test-remote.ts +++ b/packages/api/session-controller/tests/test-remote.ts @@ -32,6 +32,8 @@ import type { SessionFollowRequest, SessionListRequest, SessionListValue, + SessionOpenWorkspacePathRequest, + SessionOpenWorkspacePathValue, SessionPage, SessionPageRequest, SessionPromptRequest, @@ -58,6 +60,10 @@ export interface TestSessionRemote { attachment(request: SessionAttachmentRequest): Promise> updateQueue(request: SessionUpdateQueueRequest): Promise> cancel(request: SessionCancelRequest): Promise> + openWorkspacePath( + request: SessionOpenWorkspacePathRequest, + signal?: AbortSignal, + ): Promise> page(request: SessionPageRequest, signal?: AbortSignal): Promise> follow(request: SessionFollowRequest, signal?: AbortSignal): AsyncIterable control(signal?: AbortSignal): AsyncIterable @@ -69,6 +75,7 @@ export interface TestSessionRemoteDefaults { readonly cwd: string readonly coldBlankProbeMaxBytes?: number readonly saveDefaultModelSelection?: (selection: AgentModelSelection) => void | Promise + readonly openPath?: (path: string, signal: AbortSignal) => Promise } const installed = new WeakMap() @@ -174,9 +181,13 @@ function installControllers( const cwd = vi.spyOn(process, 'cwd').mockReturnValue(defaults.cwd) let controller: SessionController try { - controller = new SessionController(ctx, defaults.coldBlankProbeMaxBytes === undefined - ? {} - : { coldBlankProbeMaxBytes: defaults.coldBlankProbeMaxBytes }) + controller = new SessionController( + ctx, + defaults.coldBlankProbeMaxBytes === undefined + ? {} + : { coldBlankProbeMaxBytes: defaults.coldBlankProbeMaxBytes }, + defaults.openPath === undefined ? {} : { openPath: defaults.openPath }, + ) } finally { cwd.mockRestore() } @@ -239,6 +250,10 @@ export function createSessionTestRemote( attachment: request => remoteResult(() => direct.attachment(request)), updateQueue: request => remoteResult(() => direct.updateQueue(request)), cancel: request => remoteResult(() => direct.cancel(request)), + openWorkspacePath: (request, signal = new AbortController().signal) => remoteResult( + () => direct.openWorkspacePath(request, signal), + signal, + ), page: (request, signal = new AbortController().signal) => remoteResult( () => direct.page(request, signal), signal, diff --git a/packages/api/settings-controller/README.i18n.yaml b/packages/api/settings-controller/README.i18n.yaml index f393e41d2e..3b05b15761 100644 --- a/packages/api/settings-controller/README.i18n.yaml +++ b/packages/api/settings-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/settings-controller/README.md -README.md: f57bab1cf68ff05d807102a831f4ad9ce65dba76 -README.zh.md: 41062db004b8c98f544f70c42137a71aee997ec8 +README.md: b545b27ff1716e54be1a25a9bbd4131f86a12b68 +README.zh.md: 46977c6e0e3fbd43eb0a688b37e7db6eb7007f85 diff --git a/packages/api/settings-controller/README.md b/packages/api/settings-controller/README.md index f57bab1cf6..b545b27ff1 100644 --- a/packages/api/settings-controller/README.md +++ b/packages/api/settings-controller/README.md @@ -1,5 +1,5 @@ --- -description: "Host Remote owner for settings and credential configuration surfaces, including redacted reads, path-addressed settings writes, and credential reference management." +description: "Host Remote owner for settings and credential configuration surfaces, including redacted reads, writes, credential references, and native document opening." kind: "package-reference" --- # Settings Controller @@ -8,11 +8,12 @@ English | [中文](README.zh.md) ## Summary -`@deepseek-ai/dsh-api-settings-controller` exposes generated `ctx.remote.settings` and `ctx.remote.credentials` namespaces for browser configuration surfaces. It returns redacted settings and credential metadata, supports merge, replacement, and path-addressed settings writes, and stores or removes credential references without returning secret values. When either provider is absent, its namespace remains registered and returns an actionable configuration error. +`@deepseek-ai/dsh-api-settings-controller` exposes generated `ctx.remote.settings` and `ctx.remote.credentials` namespaces for browser configuration surfaces. It returns redacted settings and credential metadata, supports settings and credential writes without returning secret values, and opens provider-owned settings or Agent preset locations on the Host desktop. When a provider is absent, the namespace remains registered and returns an actionable configuration error. ## Table of Contents - [Use this package](#use-this-package) +- [Configuration](#configuration) - [Model Experience](#model-experience) - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) - [Dev Note](#dev-note) @@ -28,6 +29,19 @@ Mount this package as a Loader entry in a profile that serves browser configurat `settings.describe()` returns deployment facts and every namespace under `redactSecrets: true`. `settings.update`, `settings.replace`, and `settings.mutate` expose the settings service's three write operations and return the namespace's new redacted view; stale writes use `settings-conflict` and other provider refusals use `settings-rejected`. +`settings.openSettingsDocument()` prepares the provider-owned document and opens it with the native text-editor intent. `settings.openAgentPresetDirectory(id)` resolves only a user-authored preset and either opens its directory or returns the path when native opening is unavailable; neither method accepts a browser-supplied filesystem target. + +----- + + +## Configuration + +| Field | Default | Meaning | +|---|---|---| +| `nativeOpen` | platform-detected | Whether Agent preset directories can be handed to a native desktop opener | + +The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-api-settings-controller) is the exhaustive source for accepted fields and their JSDoc. + ----- @@ -43,7 +57,6 @@ No direct effect; reading or writing these configuration values does not alter m -- Settings document opening uses API Proxy rather than this Remote namespace. - The batch bound is fixed at 64 references and is not a deployment-configurable field. diff --git a/packages/api/settings-controller/README.zh.md b/packages/api/settings-controller/README.zh.md index 41062db004..46977c6e0e 100644 --- a/packages/api/settings-controller/README.zh.md +++ b/packages/api/settings-controller/README.zh.md @@ -1,5 +1,5 @@ --- -description: "settings 与凭据配置界面的 Host Remote owner,涵盖脱敏读取、按路径写入 settings 和管理凭据引用。" +description: "settings 与凭据配置界面的 Host Remote owner,涵盖脱敏读取、写入、凭据引用与原生文档打开。" kind: "package-reference" --- # Settings Controller @@ -8,11 +8,12 @@ kind: "package-reference" ## 概述 -`@deepseek-ai/dsh-api-settings-controller` 为浏览器配置界面提供生成的 `ctx.remote.settings` 与 `ctx.remote.credentials` namespace。它返回脱敏的 settings 与凭据元数据,支持合并、替换和按路径表达的 settings 写入,并在不返回密钥值的前提下写入或移除凭据引用。任一 provider 缺失时,对应 namespace 仍会注册,并返回可操作的配置错误。 +`@deepseek-ai/dsh-api-settings-controller` 为浏览器配置界面提供生成的 `ctx.remote.settings` 与 `ctx.remote.credentials` namespace。它返回脱敏的 settings 与凭据元数据,支持 settings 与凭据写入而不返回密钥值,并在 Host 桌面打开由 provider 持有的 settings 或 Agent preset 位置。provider 缺失时,namespace 仍会注册,并返回可操作的配置错误。 ## 目录 - [使用本包](#use-this-package) +- [配置](#configuration) - [模型体验](#model-experience) - [已知限制与延期工作](#known-limitations-and-deferred-work) - [开发备注](#dev-note) @@ -28,6 +29,19 @@ kind: "package-reference" `settings.describe()` 返回部署信息,以及在 `redactSecrets: true` 下读取的所有 namespace。`settings.update`、`settings.replace` 与 `settings.mutate` 暴露 settings service 的三种写入操作,并返回该 namespace 的新脱敏视图;过期写入使用 `settings-conflict`,其他 provider 拒绝使用 `settings-rejected`。 +`settings.openSettingsDocument()` 准备 provider 持有的文档,并用原生文本编辑器意图将其打开。`settings.openAgentPresetDirectory(id)` 只解析用户创作的 preset,并在原生打开不可用时返回目录路径;两种方法都不接受浏览器提供的文件系统目标。 + +----- + + +## 配置 + +| 字段 | 默认值 | 含义 | +|---|---|---| +| `nativeOpen` | 平台探测 | Agent preset 目录能否交给原生桌面打开器 | + +生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-api-settings-controller)是所有受支持字段及其 JSDoc 的完整来源。 + ----- @@ -43,7 +57,6 @@ kind: "package-reference" -- settings 文档打开使用 API Proxy,而不经过本 Remote namespace。 - 批量上限固定为 64 个引用,不是可按部署配置的字段。 diff --git a/packages/api/settings-controller/tests/settings-controller.host.spec.ts b/packages/api/settings-controller/tests/settings-controller.host.spec.ts index 894fbddd53..4b3b973eae 100644 --- a/packages/api/settings-controller/tests/settings-controller.host.spec.ts +++ b/packages/api/settings-controller/tests/settings-controller.host.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { settingsNamespace } from '@deepseek-ai/dsh-settings' @@ -80,6 +80,8 @@ describe('the settings Remote namespace a configuration page calls', () => { { method: 'update', invocation: { kind: 'direct' } }, { method: 'replace', invocation: { kind: 'direct' } }, { method: 'mutate', invocation: { kind: 'direct' } }, + { method: 'openSettingsDocument', invocation: { kind: 'direct' } }, + { method: 'openAgentPresetDirectory', invocation: { kind: 'direct' } }, ]) }) @@ -91,6 +93,7 @@ describe('the settings Remote namespace a configuration page calls', () => { () => ctx.settingsController.update('ui-test', {}, undefined), () => ctx.settingsController.replace('ui-test', {}, undefined), () => ctx.settingsController.mutate('ui-test', [], undefined), + () => ctx.settingsController.openSettingsDocument(new AbortController().signal), ] for (const call of calls) { const failure = await Promise.resolve().then(call).catch((error: unknown) => error) @@ -239,4 +242,108 @@ describe('the settings Remote namespace a configuration page calls', () => { expect(code).toBe('internal') expect(message).toContain('was disposed after the mutate') }) + + it('prepares and opens the provider-owned settings document', async () => { + const ctx = new Context() + await ctx.plugin(DocumentSettings) + const prepare = vi.spyOn(ctx.settings, 'prepareDocument').mockResolvedValue('/tmp/settings.yaml') + const openTextFile = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) + const controller = new SettingsController(ctx, {}, { openTextFile }) + const signal = new AbortController().signal + + await expect(controller.openSettingsDocument(signal)).resolves.toEqual({ opened: true }) + expect(prepare).toHaveBeenCalledOnce() + expect(openTextFile).toHaveBeenCalledWith('/tmp/settings.yaml', signal) + }) + + it('preserves settings-document absence, failure, and cancellation', async () => { + const absent = await boot() + await expect(absent.controller.openSettingsDocument(new AbortController().signal)) + .rejects.toMatchObject({ failure: { code: 'internal', message: expect.stringContaining('no local document') } }) + + const failed = await boot(DocumentSettings) + vi.spyOn(failed.ctx.settings, 'prepareDocument').mockRejectedValue(new Error('read failed')) + await expect(failed.controller.openSettingsDocument(new AbortController().signal)) + .rejects.toMatchObject({ failure: { code: 'internal', message: expect.stringContaining('read failed') } }) + + const cancelled = new AbortController() + cancelled.abort(new Error('cancelled')) + const prepare = vi.spyOn(failed.ctx.settings, 'prepareDocument') + prepare.mockClear() + await expect(failed.controller.openSettingsDocument(cancelled.signal)) + .rejects.toMatchObject({ failure: { code: 'cancelled' } }) + expect(prepare).not.toHaveBeenCalled() + }) + + it('does not open a settings document cancelled during preparation', async () => { + const ctx = new Context() + await ctx.plugin(DocumentSettings) + const prepared = Promise.withResolvers() + vi.spyOn(ctx.settings, 'prepareDocument').mockReturnValue(prepared.promise) + const openTextFile = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) + const controller = new SettingsController(ctx, {}, { openTextFile }) + const abort = new AbortController() + + const opening = controller.openSettingsDocument(abort.signal) + abort.abort(new Error('cancelled')) + prepared.resolve('/tmp/settings.yaml') + + await expect(opening).rejects.toMatchObject({ failure: { code: 'cancelled' } }) + expect(openTextFile).not.toHaveBeenCalled() + }) + + it('maps native settings-document opener failures', async () => { + const ctx = new Context() + await ctx.plugin(DocumentSettings) + vi.spyOn(ctx.settings, 'prepareDocument').mockResolvedValue('/tmp/settings.yaml') + const controller = new SettingsController(ctx, {}, { + openTextFile: () => Promise.reject(new Error('no default editor')), + }) + + await expect(controller.openSettingsDocument(new AbortController().signal)) + .rejects.toMatchObject({ + failure: { code: 'internal', message: 'path open failed: no default editor' }, + }) + }) + + it('opens a user Agent preset directory or returns its path without a native opener', async () => { + const ctx = new Context() + ctx.provide('agentPresets', { + resolve: (id: string) => Promise.resolve({ + id, trust: 'user', path: `/presets/${id}/agent.cordis.yml`, + }), + } as never) + const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) + const openable = new SettingsController(ctx, { nativeOpen: true }, { openPath }) + const signal = new AbortController().signal + await expect(openable.openAgentPresetDirectory('mine', signal)) + .resolves.toEqual({ opened: true }) + expect(openPath).toHaveBeenCalledWith('/presets/mine', signal) + + const headless = new Context() + headless.provide('agentPresets', { + resolve: (id: string) => Promise.resolve({ + id, trust: 'user', path: `/presets/${id}/agent.cordis.yml`, + }), + } as never) + const reveal = new SettingsController(headless, { nativeOpen: false }) + await expect(reveal.openAgentPresetDirectory('mine', new AbortController().signal)) + .resolves.toEqual({ opened: false, path: '/presets/mine' }) + }) + + it('refuses a shipped Agent preset and a missing preset provider', async () => { + const ctx = new Context() + ctx.provide('agentPresets', { + resolve: (id: string) => Promise.resolve({ + id, trust: 'system', path: `/presets/${id}/agent.cordis.yml`, + }), + } as never) + const controller = new SettingsController(ctx) + await expect(controller.openAgentPresetDirectory('standard', new AbortController().signal)) + .rejects.toMatchObject({ failure: { code: 'agent-preset-read-only' } }) + + const missing = new SettingsController(new Context()) + await expect(missing.openAgentPresetDirectory('mine', new AbortController().signal)) + .rejects.toMatchObject({ failure: { code: 'agent-preset-not-found' } }) + }) }) diff --git a/packages/client/connection/tests/fake-api.client.ts b/packages/client/connection/tests/fake-api.client.ts index a2cb5f78b8..bc546ad185 100644 --- a/packages/client/connection/tests/fake-api.client.ts +++ b/packages/client/connection/tests/fake-api.client.ts @@ -1,7 +1,7 @@ // Test-local programmable IApiClient fake (NOT the fixture: fixture is a demo // data source on a real clock; behavior tests need per-case responses and // deferred-controlled timing). The generation source is a hand pump. -import type { IApiClient, RpcResponse, SkillEntry } from '../src/client/api.ts' +import type { IApiClient, RpcResponse } from '../src/client/api.ts' import type { ConnectionGenerationSource } from '../src/client/connection.ts' import { RpcId } from '../src/client/api.ts' @@ -50,44 +50,11 @@ export class FakeApiClient implements IApiClient { () => Promise.resolve(ok({ version: '0-fake', cwd: '/f', attachedSessions: 0, home: '/h', canOpenPath: true, })) - onOpenPath: (payload: unknown) => Promise> = - () => Promise.resolve(ok({ opened: true as const })) private readonly generationConns: StreamConn[] = [] readonly host: IApiClient['host'] = { describe: payload => this.record('host.describe', payload, this.onDescribe(payload)), - openPath: payload => this.record('host.openPath', payload, this.onOpenPath(payload)), - } - - // Payloads stay `unknown` (lint-lane note above); response rows are the real - // wire shapes so cases can program catalogs and skill lists without casts. - onSkillList: (payload: unknown) => Promise> - = () => Promise.resolve(ok({ skills: [] })) - - - readonly agentPresets: IApiClient['agentPresets'] = { - openDocument: (payload: { agentPreset: string }) => - this.record('agentPreset.openDocument', payload, Promise.resolve(ok({ opened: true as const }))), - } - - readonly skills: IApiClient['skills'] = { - list: (payload: unknown) => this.record('skill.list', payload, this.onSkillList(payload)), - } - - readonly settings: IApiClient['settings'] = { - openDocument: payload => this.record('settings.openDocument', payload, Promise.resolve(ok({ opened: true as const }))), - } - - readonly llm: IApiClient['llm'] = { - providers: payload => this.record('llm.providers', payload, Promise.resolve(ok({ providers: [] }))), - models: payload => this.record('llm.models', payload, Promise.resolve(ok({ - default: { provider: 'fixture', model: 'fixture' }, - routableProviders: [], - groups: [], - failures: [], - }))), - discoverModels: payload => this.record('llm.discoverModels', payload, Promise.resolve(ok({ models: [] }))), } /** When true, the source never reports ready. */ diff --git a/packages/client/connection/tests/fixture-commands.client.spec.ts b/packages/client/connection/tests/fixture-commands.client.spec.ts index 163fb414a6..818e34d92a 100644 --- a/packages/client/connection/tests/fixture-commands.client.spec.ts +++ b/packages/client/connection/tests/fixture-commands.client.spec.ts @@ -1,14 +1,10 @@ /** * Fixture commands/skills domains: session-addressed catalogs, execute - * parse/dispatch and its logged lifecycle pair, skill.list session resolution, - * and the FixtureApiClient dispatch rows. Commands answer on the Remote face - * and skills on the legacy API face, so both are driven here. + * parse/dispatch and its logged lifecycle pair, and skills/list Session resolution. */ import { describe, expect, it } from 'vitest' import type { SessionId } from '../src/client/api.ts' -import { RpcId } from '../src/client/api.ts' -import type { RpcRequest } from '../src/client/api.ts' -import { FixtureApiClient, createFixtureApi, createFixtureFaces } from '../src/client/fixture.ts' +import { createFixtureFaces } from '../src/client/fixture.ts' /** Drive one commands Remote endpoint against the fixture state graph. */ async function callRemote( @@ -22,8 +18,6 @@ async function callRemote( } const sid = (id: string): SessionId => id as SessionId -let reqCount = 0 -const req =

(payload: P): RpcRequest

=> ({ rpcId: RpcId(`t-${reqCount++}`), payload }) describe('createFixtureApi commands/skills', () => { it('serves the addressed session catalog', async () => { @@ -162,26 +156,30 @@ describe('createFixtureApi commands/skills', () => { }) it('serves the skill catalog for the addressed session and rejects unknown sessions', async () => { - const api = createFixtureApi() - const response = await api.skills.list(req({ sessionId: sid('fx-alpha') })) - if (!response.result.ok) throw new Error('skill list failed') - expect(response.result.value.skills[0]?.name).toBe('fixture-demo') + const { rpc } = createFixtureFaces() + const skills = await callRemote<{ skills: Array<{ name: string }> }>( + rpc, 'skills/list', { request: { sessionId: sid('fx-alpha') } }, + ) + expect(skills.skills[0]?.name).toBe('fixture-demo') - const missingSession = await api.skills.list(req({ sessionId: sid('fx-nope') })) - expect(missingSession.result).toMatchObject({ ok: false, error: { code: 'session-not-found' } }) + const missingSession = await rpc.call('/api', 'skills/list', { + args: { request: { sessionId: sid('fx-nope') } }, + }) + expect(missingSession).toMatchObject({ ok: false, error: { code: 'session-not-found' } }) }) }) describe('FixtureApiClient command/skill dispatch', () => { - it('routes the Remote commands face and the legacy skill row through one state graph', async () => { - const client = new FixtureApiClient() - const commands = await callRemote<{ name: string }[]>(client.rpc, 'commands/list', { agentId: sid('fx-alpha') }) + it('routes the Remote command and skill rows through one state graph', async () => { + const { rpc } = createFixtureFaces() + const commands = await callRemote<{ name: string }[]>(rpc, 'commands/list', { agentId: sid('fx-alpha') }) expect(commands.length).toBeGreaterThan(0) const executed = await callRemote<{ commandId: string } | undefined>( - client.rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/compact' }) + rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/compact' }) expect(executed?.commandId).toBeTruthy() - const skills = await client.skills.list({ sessionId: sid('fx-alpha') }) - if (!skills.result.ok) throw new Error('skill.list failed') - expect(skills.result.value.skills.length).toBeGreaterThan(0) + const skills = await callRemote<{ skills: unknown[] }>( + rpc, 'skills/list', { request: { sessionId: sid('fx-alpha') } }, + ) + expect(skills.skills.length).toBeGreaterThan(0) }) }) diff --git a/packages/client/connection/tests/fixture.client.spec.ts b/packages/client/connection/tests/fixture.client.spec.ts index de8cbf42d9..8e41867de0 100644 --- a/packages/client/connection/tests/fixture.client.spec.ts +++ b/packages/client/connection/tests/fixture.client.spec.ts @@ -20,6 +20,7 @@ import type { ClientConnectionRpc, ConnectionRpcResult, } from '../src/rpc.ts' import type { DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types' +import type { ModelCatalog } from '@deepseek-ai/dsh-api-session-controller/types' const sid = (id: string): SessionId => id as SessionId type WorkspaceId = string & { readonly __fixtureWorkspaceId: 'WorkspaceId' } @@ -175,6 +176,7 @@ type FixtureSessionClient = { } interface FixtureSessionRemote { + modelCatalog(): Promise> follow(sessionId: SessionId, signal: AbortSignal): AsyncIterable control(signal: AbortSignal): AsyncIterable } @@ -446,6 +448,8 @@ function createSessionRemote(rpc: ClientConnectionRpc): FixtureSessionRemote { return stream as AsyncIterable } return { + modelCatalog: () => rpc.call('/api', 'session/modelCatalog', { args: {} }) as + Promise>, follow: (sessionId, signal) => open('session/follow', { request: { address: { kind: 'session', sessionId } }, }, signal), @@ -732,10 +736,10 @@ describe('createFixtureApi', () => { it('serves grouped models and keeps a selection for later history and fixture requests', async () => { const api = createFixtureApi() const sessionId = sid('fx-alpha') - const catalog = await api.llm.models(req({})) - if (!catalog.result.ok) throw new Error('models failed') - expect(catalog.result.value.groups.map(group => group.name)).toEqual(['DeepSeek', 'OpenAI']) - expect(catalog.result.value.groups[0]?.models.map(model => model.id)) + const catalog = await api.sessionRemote.modelCatalog() + if (!catalog.ok) throw new Error('models failed') + expect(catalog.value.groups.map(group => group.name)).toEqual(['DeepSeek', 'OpenAI']) + expect(catalog.value.groups[0]?.models.map(model => model.id)) .toEqual(['deepseek-v4-flash', 'deepseek-v4-pro']) const selected = await api.sessions.selectModel(req({ diff --git a/packages/client/connection/tests/node-half.host.spec.ts b/packages/client/connection/tests/node-half.host.spec.ts index 4a5121b97b..4f4fc17e39 100644 --- a/packages/client/connection/tests/node-half.host.spec.ts +++ b/packages/client/connection/tests/node-half.host.spec.ts @@ -174,8 +174,8 @@ 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.openPath', - 'llm.discoverModels', 'llm.models', 'agentPreset.openDocument', + 'session/openWorkspacePath', + 'llm/discoverModels', 'skills/list', 'settings/openAgentPresetDirectory', ] for (const method of methods) { const denied = fakeResponse() @@ -500,17 +500,17 @@ describe('connection node half over a real HTTP server', () => { const { port, close } = await serve(routes) try { const methods = [ - 'settings.openDocument', - 'host.openPath', - 'llm.discoverModels', - 'agentPreset.openDocument', - 'llm.providers', 'llm.models', + 'settings/openSettingsDocument', + 'session/openWorkspacePath', + 'llm/discoverModels', 'skills/list', + 'settings/openAgentPresetDirectory', + 'llm/listProviders', 'session/modelCatalog', ] for (const method of methods) { expect([method, await call(port, method, 'localhost')]).toEqual([method, 401]) expect([method, await call(port, method, 'harness.example')]).toEqual([method, 401]) } - expect(await call(port, 'settings.openDocument', 'other.example')).toBe(403) + expect(await call(port, 'settings/openSettingsDocument', 'other.example')).toBe(403) const declaredCookie = browserCookie(connection, 'harness.example') for (const method of methods) { @@ -519,7 +519,7 @@ describe('connection node half over a real HTTP server', () => { const loopbackAuthority = `127.0.0.1:${String(port)}` expect(await call( port, - 'settings.openDocument', + 'settings/openSettingsDocument', loopbackAuthority, browserCookie(connection, loopbackAuthority), )).toBe(404) diff --git a/packages/client/ui-agent-preset/README.i18n.yaml b/packages/client/ui-agent-preset/README.i18n.yaml index 3ff87b2d8f..f3461bcbf7 100644 --- a/packages/client/ui-agent-preset/README.i18n.yaml +++ b/packages/client/ui-agent-preset/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-agent-preset/README.md -README.md: 1f07b034839dafcb0794d1a2e41b5015f5a55df0 -README.zh.md: 2aa5420f2092d974e293ddce97d34517c63e37b8 +README.md: 495d38631257bef3f9dbf74b7762c357b90779e4 +README.zh.md: fdf0a994b00c8a28e2bd0dc4dfa7238d25c4774b diff --git a/packages/client/ui-agent-preset/README.md b/packages/client/ui-agent-preset/README.md index 1f07b03483..495d386312 100644 --- a/packages/client/ui-agent-preset/README.md +++ b/packages/client/ui-agent-preset/README.md @@ -43,7 +43,7 @@ When the roster carries the self-referential `cordis` preset, a dashed add-card

Implementation internals — click to expand -Options and the current default both come from one `agentPreset.list` call — the roster already reports which id a session with no explicit choice gets, so the row needs no settings-schema introspection — and the write targets the `agent-presets` settings namespace's `default` field, which is what the host resolves at creation. The new-session chip and the header label share one controller, because the staged choice belongs to the flow rather than to any one session; the stage is applied when a session arrives (covering both the session a workspace connect created and the blank one it reused) and dropped on refusal. A refusal announces itself as a transient banner over the composer column, because the chip's label has already reverted and a preset the host refuses to mount is one discovery reported healthy — its roster card carries no reason to go back and read. Only a pick a person just made is announced; the applier that runs when a session becomes current is not. [`dsh-client-connection`](../connection/README.md) authenticates `agentPreset.read`, `copy`, `openDocument`, `remove`, `list`, and every other Host API method with the same browser session. A composition still names the plugins a session runs, so reading one is reconnaissance, while copy, remove, and openDocument manage the roster and drive the host desktop. The section re-reads on its own actions, `settings/changed`, and `connection/reset`, because composition files are edited outside the browser and nothing on the wire announces a file change. +Options and the current default both come from one `agentPresets/list` call — the roster already reports which id a session with no explicit choice gets, so the row needs no settings-schema introspection — and the write targets the `agent-presets` settings namespace's `default` field, which is what the host resolves at creation. The new-session chip and the header label share one controller, because the staged choice belongs to the flow rather than to any one session; the stage is applied when a session arrives (covering both the session a workspace connect created and the blank one it reused) and dropped on refusal. A refusal announces itself as a transient banner over the composer column, because the chip's label has already reverted and a preset the host refuses to mount is one discovery reported healthy — its roster card carries no reason to go back and read. Only a pick a person just made is announced; the applier that runs when a session becomes current is not. [`dsh-client-connection`](../connection/README.md) authenticates `agentPresets/read`, `agentPresets/copy`, `settings/openAgentPresetDirectory`, `agentPresets/deletePreset`, `agentPresets/list`, and every other Host API method with the same browser session. A composition still names the plugins a session runs, so reading one is reconnaissance, while copy, delete, and the settings-owned directory opener manage the roster and drive the host desktop. The section re-reads on its own actions, `settings/document-updated`, and `connection/reset`, because composition files are edited outside the browser and nothing on the wire announces a file change.
diff --git a/packages/client/ui-agent-preset/README.zh.md b/packages/client/ui-agent-preset/README.zh.md index 2aa5420f20..fdf0a994b0 100644 --- a/packages/client/ui-agent-preset/README.zh.md +++ b/packages/client/ui-agent-preset/README.zh.md @@ -43,7 +43,7 @@ kind: "package-reference"
实现细节——点击展开 -选项与当前默认值都来自同一次 `agentPreset.list` 调用——名单本身已报告未显式选择的会话会得到哪个 id,因此该行无需对 settings schema 做内省——写入目标是 `agent-presets` settings 命名空间的 `default` 字段,也正是宿主在创建时解析的字段。新建会话 chip 与标题标签共用一个控制器,因为暂存选择属于流程而非任何单个会话;暂存值在会话到达时应用(既覆盖工作区连接新建的会话,也覆盖它复用的空白会话),被拒绝时丢弃。被拒绝会以一条瞬时横幅在 composer 列上方自报,因为 chip 的标签此时已经弹回,而被宿主拒绝挂载的 preset 正是发现过程报告为健康的那一种——它的名单卡片上没有任何原因可供回头查看。只有人刚做出的选择会被自报;会话成为当前会话时触发的应用器不会。[`dsh-client-connection`](../connection/README.zh.md) 使用同一浏览器会话认证 `agentPreset.read`、`copy`、`openDocument`、`remove`、`list` 及其他所有 Host API 方法。组装仍会指明一个会话所运行的插件,因此读取属于侦察,而 copy、remove 与 openDocument 管理名单并驱动宿主桌面。分区在自身操作、`settings/changed` 与 `connection/reset` 时重读,因为组装文件在浏览器之外编辑,线上没有任何机制宣布文件变动。 +选项与当前默认值都来自同一次 `agentPresets/list` 调用——名单本身已报告未显式选择的会话会得到哪个 id,因此该行无需对 settings schema 做内省——写入目标是 `agent-presets` settings 命名空间的 `default` 字段,也正是宿主在创建时解析的字段。新建会话 chip 与标题标签共用一个控制器,因为暂存选择属于流程而非任何单个会话;暂存值在会话到达时应用(既覆盖工作区连接新建的会话,也覆盖它复用的空白会话),被拒绝时丢弃。被拒绝会以一条瞬时横幅在 composer 列上方自报,因为 chip 的标签此时已经弹回,而被宿主拒绝挂载的 preset 正是发现过程报告为健康的那一种——它的名单卡片上没有任何原因可供回头查看。只有人刚做出的选择会被自报;会话成为当前会话时触发的应用器不会。[`dsh-client-connection`](../connection/README.zh.md) 使用同一浏览器会话认证 `agentPresets/read`、`agentPresets/copy`、`settings/openAgentPresetDirectory`、`agentPresets/deletePreset`、`agentPresets/list` 及其他所有 Host API 方法。组装仍会指明一个会话所运行的插件,因此读取属于侦察,而 copy、delete 与 settings 所有的目录打开操作负责管理名单并驱动宿主桌面。分区在自身操作、`settings/document-updated` 与 `connection/reset` 时重读,因为组装文件在浏览器之外编辑,线上没有任何机制宣布文件变动。
diff --git a/packages/client/ui-agent-preset/tests/apply.client.spec.ts b/packages/client/ui-agent-preset/tests/apply.client.spec.ts index 2cde10fc4e..b7e73785e4 100644 --- a/packages/client/ui-agent-preset/tests/apply.client.spec.ts +++ b/packages/client/ui-agent-preset/tests/apply.client.spec.ts @@ -82,6 +82,10 @@ async function bench() { calls.push(`settings:${JSON.stringify(patch)}`) return Promise.resolve({ ok: true as const, value: {} }) }, + openAgentPresetDirectory: (agentPreset: string) => { + calls.push(`openAgentPresetDirectory:${agentPreset}`) + return Promise.resolve({ ok: true as const, value: { opened: true as const } }) + }, } const remote = new TestRemote(ctx, { settings }) // The roster and the switch are the AgentPresets Remote namespace; the @@ -118,12 +122,6 @@ async function bench() { result: { ok: true as const, value: { canOpenPath: true } }, }), }, - agentPresets: { - openDocument: (payload: { agentPreset: string }) => { - calls.push(`openDocument:${payload.agentPreset}`) - return Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: { opened: true as const } } }) - }, - }, }, } as never) await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await() @@ -257,7 +255,7 @@ describe('ui-agent-preset apply', () => { // one the roster re-read reflects, and the delete the section confirmed // is the one its remove() sees. expect(calls).toContain('copy:mine') - expect(calls.filter(call => call === 'openDocument:mine').length).toBeGreaterThan(0) + expect(calls.filter(call => call === 'openAgentPresetDirectory:mine').length).toBeGreaterThan(0) expect(section.hooks.agentPresetSection.getSnapshot().rows).toHaveLength(2) }) diff --git a/packages/client/ui-agent-preset/tests/section-store.client.spec.ts b/packages/client/ui-agent-preset/tests/section-store.client.spec.ts index 8d925f4978..df1ad1e201 100644 --- a/packages/client/ui-agent-preset/tests/section-store.client.spec.ts +++ b/packages/client/ui-agent-preset/tests/section-store.client.spec.ts @@ -8,7 +8,6 @@ import { describe, expect, it } from 'vitest' import type { ClientRemote, IApiClient } from '@deepseek-ai/dsh-api-remotes/client' -import type { SettingsWireFace } from '@deepseek-ai/dsh-client-ui-settings/client' import { AgentPresetSectionController, draftBlocker } from '../src/client/section-store.ts' import type { CopyDraft, PresetRow } from '../src/client/section-store.ts' @@ -49,9 +48,6 @@ interface FakeOptions { } const ok = (value: unknown) => Promise.resolve({ rpcId: 'r', result: { ok: true as const, value } }) -const fail = (message: string) => - Promise.resolve({ rpcId: 'r', result: { ok: false as const, error: { code: 'internal', message, details: {} } } }) - const remoteOk = (value: unknown) => Promise.resolve({ ok: true as const, value }) const remoteFail = (message: string) => Promise.resolve({ ok: false as const, error: { code: 'internal', message, details: {} } }) @@ -64,36 +60,15 @@ const remoteFail = (message: string) => * @returns the fake client. */ function fakeApi( - defaultId: { id: string }, options: FakeOptions = {}, -): SettingsWireFace & Pick { - const record = (method: string, payload: unknown): void => { options.calls?.push({ method, payload }) } +): Pick { return { host: { describe: () => (options.throwDescribe === true ? Promise.reject(new Error('socket closed')) : ok({ canOpenPath: options.hasDocument ?? true })), }, - agentPresets: { - openDocument: (payload: { agentPreset: string }) => { - record('openDocument', payload) - if (options.throwOpen === true) return Promise.reject(new Error('socket closed')) - if (options.failOpen !== undefined) return fail(options.failOpen) - return (options.hasDocument ?? true) - ? ok({ opened: true }) - : ok({ opened: false, path: `/presets/${payload.agentPreset}` }) - }, - }, - settings: { - update: (ns: string, patch: { default?: string }) => { - record('settings.update', { ns, patch }) - if (options.failSettings !== undefined) return remoteFail(options.failSettings) - /* v8 ignore next -- the controller only ever sets `default` */ - defaultId.id = patch.default ?? defaultId.id - return remoteOk({}) - }, - }, - } as unknown as SettingsWireFace & Pick + } as Pick } /** @@ -108,7 +83,7 @@ function fakeRemote( presets: Map, defaultId: { id: string }, options: FakeOptions = {}, -): Pick { +): Pick { const record = (method: string, payload: unknown): void => { options.calls?.push({ method, payload }) } return { agentPresets: { @@ -167,7 +142,24 @@ function fakeRemote( return await remoteOk(undefined) }, }, - } as unknown as Pick + settings: { + update: (ns: string, patch: { default?: string }) => { + record('settings.update', { ns, patch }) + if (options.failSettings !== undefined) return remoteFail(options.failSettings) + /* v8 ignore next -- the controller only ever sets `default` */ + defaultId.id = patch.default ?? defaultId.id + return remoteOk({}) + }, + openAgentPresetDirectory: (agentPreset: string) => { + record('openAgentPresetDirectory', { agentPreset }) + if (options.throwOpen === true) return Promise.reject(new Error('socket closed')) + if (options.failOpen !== undefined) return remoteFail(options.failOpen) + return (options.hasDocument ?? true) + ? remoteOk({ opened: true }) + : remoteOk({ opened: false, path: `/presets/${agentPreset}` }) + }, + }, + } as unknown as Pick } function seed(): Map { @@ -184,7 +176,7 @@ function harness(options: FakeOptions = {}) { let rosterChanges = 0 const wired = { ...options, calls: options.calls ?? calls } const controller = new AgentPresetSectionController( - fakeApi(defaultId, wired), + fakeApi(wired), fakeRemote(presets, defaultId, wired), () => { rosterChanges += 1 }, ) @@ -406,7 +398,7 @@ describe('submitting a copy', () => { .toEqual({ from: 'standard', id: 'my-copy', name: '我的模式' }) // A preset is its files from here on, so landing in them completes the // copy rather than following it. - expect(calls.find(call => call.method === 'openDocument')?.payload) + expect(calls.find(call => call.method === 'openAgentPresetDirectory')?.payload) .toEqual({ agentPreset: 'my-copy' }) }) @@ -476,7 +468,7 @@ describe('the location action', () => { await controller.openLocation('mine') - expect(calls.find(call => call.method === 'openDocument')?.payload).toEqual({ agentPreset: 'mine' }) + expect(calls.find(call => call.method === 'openAgentPresetDirectory')?.payload).toEqual({ agentPreset: 'mine' }) expect(controller.store.getSnapshot().revealedPaths).toEqual({}) }) @@ -580,13 +572,14 @@ describe('deleting', () => { await controller.load() presets.clear() const broken = new AgentPresetSectionController( - { agentPresets: {}, settings: {}, host: {} } as unknown as SettingsWireFace & Pick, + { host: {} } as unknown as Pick, { agentPresets: { list: () => Promise.reject(new Error('gone')), deletePreset: () => Promise.reject(new Error('socket closed')), }, - } as unknown as Pick, + settings: {}, + } as unknown as Pick, ) broken.confirmDelete('mine') @@ -603,7 +596,7 @@ describe('a controller with no roster listener', () => { const presets = seed() const defaultId = { id: 'standard' } const alone = new AgentPresetSectionController( - fakeApi(defaultId), fakeRemote(presets, defaultId)) + fakeApi(), fakeRemote(presets, defaultId)) await alone.load() alone.confirmDelete('mine') diff --git a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx index 328868a2dc..2a0d9b045c 100644 --- a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx @@ -5,9 +5,10 @@ import { AttachmentId } from '@deepseek-ai/dsh-attachment' import type { ISession } from '@deepseek-ai/dsh-api-session-controller/client' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import { - SlotTestRuntime, stubSettingsScope, usePinnedBrowserLanguages, + SlotTestRuntime, TestRemote, stubSettingsScope, usePinnedBrowserLanguages, } from '@deepseek-ai/dsh-client-test-runtime' import type { SessionBehaviorOverrides } from '@deepseek-ai/dsh-client-test-runtime' +import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client' import { apply as applyConversation, inject as injectConversation, } from '@deepseek-ai/dsh-client-ui-conversation/client' @@ -48,10 +49,12 @@ async function bench() { runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const layout = { openDetails: vi.fn(), closeDetails: vi.fn() } runtime.ctx.provide('layout', layout as never) - const openPath = vi.fn<(path: string) => Promise>(async () => {}) + const openWorkspacePath = vi.fn( + () => Promise.resolve({ ok: true, value: { opened: true } }), + ) + new TestRemote(runtime.ctx, { session: { openWorkspacePath } }) runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => ROOT), - openPath, } as never) const session = sessionFakeFor() await runtime.sessions.add({ @@ -79,7 +82,7 @@ async function bench() { ) => ChatViewInjected)(id, instance.actions) return { instance, injected } } - return { runtime, layout, openPath, session, chatViewApi } + return { runtime, layout, openWorkspacePath, session, chatViewApi } } describe('Chat inject API', () => { @@ -120,10 +123,13 @@ describe('Chat inject API', () => { const b = await bench() const { injected } = b.chatViewApi(ROOT) await injected.openFile('src/a.ts') - expect(b.openPath).toHaveBeenCalledWith('/proj/src/a.ts') + expect(b.openWorkspacePath).toHaveBeenCalledWith({ sessionId: ROOT, path: 'src/a.ts' }) - b.openPath.mockRejectedValueOnce(new Error('xdg-open is not available')) - await expect(injected.openFile('src/b.ts')).rejects.toThrow('xdg-open is not available') + b.openWorkspacePath.mockResolvedValueOnce({ + ok: false, + error: { code: 'internal', message: 'xdg-open is not available', details: {} }, + }) + await expect(injected.openFile('src/b.ts')).rejects.toThrow('path open failed: xdg-open is not available') await b.runtime.dispose() }) diff --git a/packages/client/ui-chat/tests/chat-apply.client.spec.tsx b/packages/client/ui-chat/tests/chat-apply.client.spec.tsx index 537bea3e56..d9c1538190 100644 --- a/packages/client/ui-chat/tests/chat-apply.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-apply.client.spec.tsx @@ -1,7 +1,7 @@ // @vitest-environment jsdom import { describe, expect, it, vi } from 'vitest' import { - chatSnapshot, SlotTestRuntime, stubSettingsScope, usePinnedBrowserLanguages, + chatSnapshot, SlotTestRuntime, TestRemote, stubSettingsScope, usePinnedBrowserLanguages, } from '@deepseek-ai/dsh-client-test-runtime' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' @@ -40,8 +40,10 @@ async function bench() { runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() } as never) runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID), - openPath: vi.fn(async () => {}), } as never) + new TestRemote(runtime.ctx, { + session: { openWorkspacePath: vi.fn(async () => ({ ok: true, value: { opened: true } })) }, + }) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) runtime.slots.installLocale(locale) diff --git a/packages/client/ui-model-selection/tests/browser-plugin.client.spec.ts b/packages/client/ui-model-selection/tests/browser-plugin.client.spec.ts index 459c972208..1bf11733b5 100644 --- a/packages/client/ui-model-selection/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-model-selection/tests/browser-plugin.client.spec.ts @@ -65,6 +65,18 @@ async function bench() { // block follows this, never catalog membership. let routable = true const sessionRemote = { + modelCatalog: () => { + calls.models += 1 + return Promise.resolve({ + ok: true as const, + value: { + default: defaultSelection, + routableProviders: routable ? ['deepseek-official'] : [], + groups: GROUPS, + failures: [], + }, + }) + }, selectModel: (payload: { sessionId: SessionId; provider: string; model: string; reasoningEffort?: string }) => { calls.select += 1 selected = { @@ -80,28 +92,6 @@ async function bench() { } const remote = Object.assign(new TestRemote(ctx), { session: sessionRemote }) ctx.reflect.provide('remote.session', sessionRemote) - ctx.provide('connection', { - api: { - llm: { - models: () => { - calls.models += 1 - return Promise.resolve({ - rpcId: 'model-catalog', - result: { - ok: true as const, - value: { - default: defaultSelection, - routableProviders: routable ? ['deepseek-official'] : [], - groups: GROUPS, - failures: [], - }, - }, - }) - }, - }, - }, - isLoopback: false, - } as never) const blocks = new Map() ctx.provide('conversation', { blocks: { diff --git a/packages/client/ui-model-selection/tests/catalog.client.spec.ts b/packages/client/ui-model-selection/tests/catalog.client.spec.ts index 95f3d440b4..a9333113e8 100644 --- a/packages/client/ui-model-selection/tests/catalog.client.spec.ts +++ b/packages/client/ui-model-selection/tests/catalog.client.spec.ts @@ -1,4 +1,4 @@ -import type { IApiClient, ModelCatalog } from '@deepseek-ai/dsh-client-connection/client' +import type { ClientRemote, ModelCatalog } from '@deepseek-ai/dsh-api-remotes/client' import { describe, expect, it, vi } from 'vitest' import { ModelCatalogDirectory } from '../src/client/catalog.ts' @@ -10,16 +10,16 @@ const catalog = (model: string): ModelCatalog => ({ }) function directory(models: () => Promise): ModelCatalogDirectory { - return new ModelCatalogDirectory({ llm: { models } } as unknown as IApiClient) + return new ModelCatalogDirectory({ modelCatalog: models } as unknown as ClientRemote['session']) } describe('ModelCatalogDirectory', () => { it('shares one failing request, exposes the RPC error, and permits a retry', async () => { const models = vi.fn() .mockResolvedValueOnce({ - result: { ok: false, error: { code: 'unavailable', message: 'catalog offline', details: {} } }, + ok: false, error: { code: 'unavailable', message: 'catalog offline', details: {} }, }) - .mockResolvedValueOnce({ result: { ok: true, value: catalog('recovered') } }) + .mockResolvedValueOnce({ ok: true, value: catalog('recovered') }) const subject = directory(models) const first = subject.load() @@ -40,10 +40,10 @@ describe('ModelCatalogDirectory', () => { const stale = subject.load() subject.resetGeneration() - first.resolve({ result: { ok: true, value: catalog('stale') } }) + first.resolve({ ok: true, value: catalog('stale') }) await expect(stale).resolves.toEqual(catalog('stale')) expect(subject.store.getSnapshot()).toMatchObject({ value: null, status: 'loading' }) - second.resolve({ result: { ok: true, value: catalog('fresh') } }) + second.resolve({ ok: true, value: catalog('fresh') }) await vi.waitFor(() => { expect(subject.store.getSnapshot()).toMatchObject({ value: catalog('fresh'), status: 'ready' }) }) @@ -62,7 +62,7 @@ describe('ModelCatalogDirectory', () => { first.reject(new Error('stale failure')) await expect(stale).rejects.toThrow('stale failure') expect(subject.store.getSnapshot()).toMatchObject({ value: null, status: 'loading', error: null }) - second.resolve({ result: { ok: true, value: catalog('fresh') } }) + second.resolve({ ok: true, value: catalog('fresh') }) await vi.waitFor(() => { expect(subject.store.getSnapshot()).toMatchObject({ value: catalog('fresh'), status: 'ready' }) }) @@ -70,7 +70,7 @@ describe('ModelCatalogDirectory', () => { it('contains refresh failures while retaining old data and clears it on a failed Host reset', async () => { const models = vi.fn() - .mockResolvedValueOnce({ result: { ok: true, value: catalog('old') } }) + .mockResolvedValueOnce({ ok: true, value: catalog('old') }) .mockRejectedValueOnce('refresh failed') .mockRejectedValueOnce(new Error('reset failed')) const subject = directory(models) diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml index 7041d38cad..93ac13b82e 100644 --- a/packages/client/ui-settings-general/README.i18n.yaml +++ b/packages/client/ui-settings-general/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-settings-general/README.md -README.md: 0c42e7c8c63ee720e6766471de8e43d97624418b -README.zh.md: 09cacf4bcdd1c47b66b34840ccda2c15ce3dc2f2 +README.md: f0c4ab7c50798919d42ade0eac6a9a93c8a4df1c +README.zh.md: 912ea0fb16c0d1d512ff846c8eda1e724664f1ea diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md index 0c42e7c8c6..f0c4ab7c50 100644 --- a/packages/client/ui-settings-general/README.md +++ b/packages/client/ui-settings-general/README.md @@ -55,7 +55,7 @@ The navigation is a projection of the `settings.section` ledger; nav labels may ### Document availability -On a loopback page, the Client loads the provider's `hasDocument` capability through `settings.describe` and renders **Open configuration file** only when the Host confirms that a provider-owned local document can be prepared. The action sends the pathless, browser-authenticated `settings.openDocument` request; the Host resolves the provider path again, materializes an absent document, and hands it to a native text editor (`open -t` on macOS, bypassing a browser file association; the desktop file association on Linux and Windows; Windows association after `wslpath -w` translation on WSL). Open failures keep the action available and render a localized error. Reopening the dialog or reconnecting refreshes availability after a transient read failure or Host topology change. Non-loopback pages retain the Client policy that withholds this native action and its settings read. +On a loopback page, the Client loads the provider's `hasDocument` capability through `settings/describe` and renders **Open configuration file** only when the Host confirms that a provider-owned local document can be prepared. The action calls the pathless, browser-authenticated `settings/openSettingsDocument` Remote; the Host resolves the provider path again, materializes an absent document, and hands it to a native text editor (`open -t` on macOS, bypassing a browser file association; the desktop file association on Linux and Windows; Windows association after `wslpath -w` translation on WSL). Open failures keep the action available and render a localized error. Reopening the dialog or reconnecting refreshes availability after a transient read failure or Host topology change. Non-loopback pages retain the Client policy that withholds this native action and its settings read. ### Host half diff --git a/packages/client/ui-settings-general/README.zh.md b/packages/client/ui-settings-general/README.zh.md index 09cacf4bcd..912ea0fb16 100644 --- a/packages/client/ui-settings-general/README.zh.md +++ b/packages/client/ui-settings-general/README.zh.md @@ -55,7 +55,7 @@ kind: "package-reference" ### 文档可用性 -在 loopback 页面上,Client 通过 `settings.describe` 加载提供方的 `hasDocument` 能力,且只有在 Host 确认可准备好一份由提供方持有的本地文档时才渲染配置文件操作。该操作发送无路径参数且经浏览器认证的 `settings.openDocument` 请求;Host 会再次解析提供方路径、在文档缺失时将其创建出来,并交给原生文本编辑器(macOS 上使用 `open -t`,绕过浏览器文件关联;Linux 和 Windows 上使用桌面文件关联;WSL 上经 `wslpath -w` 转换后使用 Windows 文件关联)。打开失败时该操作仍可使用,并渲染本地化错误。临时读取失败或 Host 拓扑变化后,重新打开对话框或重新连接会刷新可用性。非 loopback 页面保留 Client 策略,不提供该原生操作及其 settings 读取。 +在 loopback 页面上,Client 通过 `settings/describe` 加载提供方的 `hasDocument` 能力,且只有在 Host 确认可准备好一份由提供方持有的本地文档时才渲染配置文件操作。该操作调用无路径参数且经浏览器认证的 `settings/openSettingsDocument` Remote;Host 会再次解析提供方路径、在文档缺失时将其创建出来,并交给原生文本编辑器(macOS 上使用 `open -t`,绕过浏览器文件关联;Linux 和 Windows 上使用桌面文件关联;WSL 上经 `wslpath -w` 转换后使用 Windows 文件关联)。打开失败时该操作仍可使用,并渲染本地化错误。临时读取失败或 Host 拓扑变化后,重新打开对话框或重新连接会刷新可用性。非 loopback 页面保留 Client 策略,不提供该原生操作及其 settings 读取。 ### 宿主端 diff --git a/packages/client/ui-settings-general/tests/apply.client.spec.ts b/packages/client/ui-settings-general/tests/apply.client.spec.ts index bf2174d571..86ec40db91 100644 --- a/packages/client/ui-settings-general/tests/apply.client.spec.ts +++ b/packages/client/ui-settings-general/tests/apply.client.spec.ts @@ -40,14 +40,14 @@ async function bench(isLoopback = true) { }, })) const settingsOpenDocument = vi.fn(() => Promise.resolve({ - rpcId: 'settings-open' as never, - result: { ok: true as const, value: { opened: true as const } }, + ok: true as const, value: { opened: true as const }, })) ctx.provide('connection', { - api: { settings: { openDocument: settingsOpenDocument } }, isLoopback, } as never) - new TestRemote(ctx, { settings: { describe: settingsDescribe } }) + new TestRemote(ctx, { + settings: { describe: settingsDescribe, openSettingsDocument: settingsOpenDocument }, + }) await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await() return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, settingsDescribe, settingsOpenDocument } } @@ -76,7 +76,7 @@ function generalEntry(slots: SlotRegistry) { describe('ui-settings-general apply', () => { it('declares the services it uses', () => { - expect(inject).toEqual(['slots', 'locale', 'connection', 'settingsScope']) + expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'remote.settings', 'settingsScope']) }) it('fills all five seats for declarations before or after apply', async () => { diff --git a/packages/client/ui-settings-general/tests/components.client.spec.tsx b/packages/client/ui-settings-general/tests/components.client.spec.tsx index 9c2271db4b..19464e6778 100644 --- a/packages/client/ui-settings-general/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-general/tests/components.client.spec.tsx @@ -71,8 +71,7 @@ describe('GeneralSection', () => { describe('SettingsDocumentAction', () => { it('appears only for a file-backed provider and requests its Host-owned document', async () => { const openDocument = vi.fn(() => Promise.resolve({ - rpcId: 'document-open' as never, - result: { ok: true as const, value: { opened: true as const } }, + ok: true as const, value: { opened: true as const }, })) const controller = derivedDocumentStore({ settings: { @@ -80,7 +79,7 @@ describe('SettingsDocumentAction', () => { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [] }, })), - openDocument, + openSettingsDocument: openDocument, }, }) render( { />) const action = await screen.findByRole('button', { name: 'Open configuration file' }) fireEvent.click(action) - await waitFor(() => { expect(openDocument).toHaveBeenCalledWith({}) }) + await waitFor(() => { expect(openDocument).toHaveBeenCalledWith() }) }) it('stays absent without a document and follows a mirror refresh to available', async () => { const describe = vi.fn() .mockResolvedValueOnce({ ok: true as const, value: { writable: true, hasDocument: false, namespaces: [] } }) .mockResolvedValueOnce({ ok: true as const, value: { writable: true, hasDocument: true, namespaces: [] } }) - const wire = { settings: { describe, openDocument: vi.fn() } } as never + const wire = { settings: { describe, openSettingsDocument: vi.fn() } } as never const mirror = new SettingsDescribeMirror(wire) const controller = new SettingsDocumentStore(wire, mirror) const first = render( { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [] }, })), - openDocument: vi.fn(() => Promise.resolve({ - rpcId: 'document-open-failed' as never, - result: { ok: false as const, error: { code: 'internal' as const, message: 'xdg-open missing', details: {} } }, + openSettingsDocument: vi.fn(() => Promise.resolve({ + ok: false as const, + error: { code: 'internal' as const, message: 'xdg-open missing', details: {} }, })), }, }) diff --git a/packages/client/ui-settings-general/tests/settings-document-store.client.spec.ts b/packages/client/ui-settings-general/tests/settings-document-store.client.spec.ts index 85c55c6ad0..b166e8b452 100644 --- a/packages/client/ui-settings-general/tests/settings-document-store.client.spec.ts +++ b/packages/client/ui-settings-general/tests/settings-document-store.client.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { RpcResponse } from '@deepseek-ai/dsh-api-remotes/client' +import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol' import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/src/client/settings-mirror.ts' import { SettingsDocumentStore } from '../src/client/settings-document-store.ts' @@ -13,11 +13,8 @@ function response(hasDocument = false) { return { ok: true, value: { writable: true, hasDocument, namespaces: [] } } } -function opened(): RpcResponse<{ opened: true }> { - return { - rpcId: 'settings-open' as never, - result: { ok: true, value: { opened: true } }, - } +function opened(): RemoteResult<{ opened: true }> { + return { ok: true, value: { opened: true } } } function describeFailed(message: string) { @@ -28,19 +25,19 @@ describe('SettingsDocumentStore', () => { it('loads provider metadata and asks the settings domain to open its document', async () => { const describe = vi.fn(() => Promise.resolve(response(true))) const openDocument = vi.fn(() => Promise.resolve(opened())) - const controller = derivedDocumentStore({ settings: { describe, openDocument } }) + const controller = derivedDocumentStore({ settings: { describe, openSettingsDocument: openDocument } }) await controller.load() expect(controller.store.getSnapshot()).toEqual({ status: 'ready', opening: false, error: null, }) await controller.open() - expect(openDocument).toHaveBeenCalledWith({}) + expect(openDocument).toHaveBeenCalledWith() }) it('marks absent or failed metadata unavailable without opening anything', async () => { const openDocument = vi.fn(() => Promise.resolve(opened())) const absent = derivedDocumentStore({ - settings: { describe: () => Promise.resolve(response()), openDocument }, + settings: { describe: () => Promise.resolve(response()), openSettingsDocument: openDocument }, }) await absent.load() await absent.open() @@ -48,13 +45,13 @@ describe('SettingsDocumentStore', () => { expect(openDocument).not.toHaveBeenCalled() const failed = derivedDocumentStore({ - settings: { describe: () => Promise.reject(new Error('offline')), openDocument }, + settings: { describe: () => Promise.reject(new Error('offline')), openSettingsDocument: openDocument }, }) await failed.load() expect(failed.store.getSnapshot()).toMatchObject({ status: 'unavailable', error: 'offline' }) const rejected = derivedDocumentStore({ - settings: { describe: () => Promise.resolve(describeFailed('provider failed')), openDocument }, + settings: { describe: () => Promise.resolve(describeFailed('provider failed')), openSettingsDocument: openDocument }, }) await rejected.load() expect(rejected.store.getSnapshot()).toMatchObject({ @@ -63,19 +60,16 @@ describe('SettingsDocumentStore', () => { }) it('collapses concurrent open gestures and recovers after a failure', async () => { - let resolveOpen!: (response: RpcResponse<{ opened: true }>) => void - const openDocument = vi.fn(() => new Promise>((resolve) => { resolveOpen = resolve })) + let resolveOpen!: (response: RemoteResult<{ opened: true }>) => void + const openDocument = vi.fn(() => new Promise>((resolve) => { resolveOpen = resolve })) const controller = derivedDocumentStore({ - settings: { describe: () => Promise.resolve(response(true)), openDocument }, + settings: { describe: () => Promise.resolve(response(true)), openSettingsDocument: openDocument }, }) await controller.load() const first = controller.open() const second = controller.open() expect(openDocument).toHaveBeenCalledOnce() - resolveOpen({ - rpcId: 'settings-open-failed' as never, - result: { ok: false, error: { code: 'internal', message: 'no default editor', details: {} } }, - }) + resolveOpen({ ok: false, error: { code: 'internal', message: 'no default editor', details: {} } }) await Promise.all([first, second]) expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', opening: false, error: 'no default editor', @@ -87,7 +81,7 @@ describe('SettingsDocumentStore', () => { const controller = derivedDocumentStore({ settings: { describe: vi.fn(() => Promise.resolve(response(true))), - openDocument: () => new Promise((_, reject) => { rejectOpen = reject }), + openSettingsDocument: () => new Promise((_, reject) => { rejectOpen = reject }), }, }) await controller.load() @@ -106,7 +100,7 @@ describe('SettingsDocumentStore', () => { describe: vi.fn() .mockRejectedValueOnce(new Error('offline')) .mockResolvedValueOnce(response(true)), - openDocument: vi.fn(), + openSettingsDocument: vi.fn(), }, } as never const mirror = new SettingsDescribeMirror(wire) diff --git a/packages/client/ui-settings-general/tests/shell.client.spec.ts b/packages/client/ui-settings-general/tests/shell.client.spec.ts index 89da5433ef..4bd9263dec 100644 --- a/packages/client/ui-settings-general/tests/shell.client.spec.ts +++ b/packages/client/ui-settings-general/tests/shell.client.spec.ts @@ -54,7 +54,9 @@ const CHILD_SPECS = { describe('ui-settings apply', () => { it('declares only the slot registry (a pure composition face, no locale)', () => { - expect(inject).toEqual(['slots', 'locale', 'connection', 'settingsScope']) + expect(inject).toEqual([ + 'slots', 'locale', 'connection', 'remote', 'remote.settings', 'settingsScope', + ]) }) it('registers the shell and declares every child slot, before or after the declaration', async () => { diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index 9772bd7a42..8b69e152a0 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/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-settings-models/README.md -README.md: 658b357992a1926f1f21e109c19b7c0975da58ea -README.zh.md: dcc19f80a15e6e7e81c3dea128a999b83e8311ba +README.md: 75efb18daf3e4a96fea637f36633f216046ebccd +README.zh.md: f3c7099e242e379be5143352d4045208a8209dc6 diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index 658b357992..75efb18daf 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -37,7 +37,7 @@ The collapsed 自定义设置 fold carries the curated extras: `baseURL` for bot ### Adding and deleting providers -The add flow is a card carrying the dormant-directory provider select — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists. **Add a custom provider** declares a route pi-ai does not ship; the create card asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. **Fetch available models** asks `llm.discoverModels` about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a picker rather than being written, and nothing is written until **Add selected**. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider. +The add flow is a card carrying the dormant-directory provider select — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists. **Add a custom provider** declares a route pi-ai does not ship; the create card asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a picker rather than being written, and nothing is written until **Add selected**. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider. ### First-run dialogs diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index dcc19f80a1..f3c7099e24 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -37,7 +37,7 @@ kind: "package-reference" ### 新增与删除提供方 -「新增」流程是一张承载休眠目录提供方选择框的卡片——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog。**添加自定义提供方**声明一条 pi-ai 不提供的路由;创建卡片会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。**获取可用模型**就表单显示的端点询问 `llm.discoverModels`,因此新增提供方一次即可完成,而非先保存再返回;回复打开的是选择器而非直接写入,只有点击**添加所选**才会写入。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供方。 +「新增」流程是一张承载休眠目录提供方选择框的卡片——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog。**添加自定义提供方**声明一条 pi-ai 不提供的路由;创建卡片会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供方一次即可完成,而非先保存再返回;回复打开的是选择器而非直接写入,只有点击**添加所选**才会写入。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供方。 ### 首次运行弹窗 diff --git a/packages/client/ui-settings-models/tests/apply.client.spec.ts b/packages/client/ui-settings-models/tests/apply.client.spec.ts index 7045e4831d..4513bde5a2 100644 --- a/packages/client/ui-settings-models/tests/apply.client.spec.ts +++ b/packages/client/ui-settings-models/tests/apply.client.spec.ts @@ -30,6 +30,12 @@ async function bench(isLoopback = true, settings?: object, services: object = {} set: vi.fn(), unset: vi.fn(), }, + llm: { + listProviders: vi.fn(() => Promise.resolve({ ok: true, value: [] })), + listConfigurableProviders: vi.fn(() => Promise.resolve({ ok: true, value: [] })), + discoverModels: vi.fn(() => Promise.resolve({ ok: true, value: [] })), + ...services, + }, // Without a settings face the mirror's reads fail and stay contained; the // Models join itself never fetches until a section actually loads. The real // ui-settings apply also provides the settingsSchema service. @@ -56,7 +62,7 @@ function declare(slots: SlotRegistry): () => void { describe('ui-settings-models apply', () => { it('declares the services it uses', () => { expect(inject).toEqual([ - 'slots', 'locale', 'connection', 'remote', 'remote.credentials', 'remote.settings', + 'slots', 'locale', 'remote', 'remote.credentials', 'remote.llm', 'remote.settings', 'settingsScope', 'settingsSchema', ]) }) @@ -302,11 +308,8 @@ describe('pushed invalidations', () => { }], }, })) - const providers = vi.fn(() => Promise.resolve({ - rpcId: 'apply-models-providers' as never, - result: { ok: true as const, value: { providers: [] } }, - })) - const b = await bench(true, { describe }, { llm: { providers } }) + const listProviders = vi.fn(() => Promise.resolve({ ok: true as const, value: [] })) + const b = await bench(true, { describe }, { listProviders }) declare(b.slots) await b.ctx.plugin({ inject: [...inject], apply }).await() const entry = b.slots.entries('settings.section') diff --git a/packages/client/ui-settings-models/tests/components.client.spec.tsx b/packages/client/ui-settings-models/tests/components.client.spec.tsx index da23418375..a22f787a1b 100644 --- a/packages/client/ui-settings-models/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/components.client.spec.tsx @@ -4,7 +4,7 @@ import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testi import { afterEach, describe, expect, it, vi } from 'vitest' import Schema from '@deepseek-ai/schemastery' import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime' -import type { JsonValue, RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client' +import type { JsonValue, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client' import { ModelsSection, needsSetup, providerCopy, providerTargetLabel, removeProviderProfile, } from '../src/client/ModelsSection.tsx' @@ -133,16 +133,6 @@ function wireNamespaces(): SettingsNamespaceView[] { ] } -let nextRpc = 0 -function ok(value: T): RpcResponse { - return { rpcId: `r-${nextRpc++}` as never, result: { ok: true, value } } -} -function fail(message: string, code = 'settings-rejected'): RpcResponse { - return { - rpcId: `r-${nextRpc++}` as never, - result: { ok: false, error: { code, message, details: { ns: 'x' } } as never }, - } -} /** Credentials answers over the Remote carrier, which has no envelope. */ function remoteOk(value: T) { return { ok: true as const, value } @@ -164,17 +154,19 @@ function scriptedFace(overrides: { const unset = overrides.unset ?? vi.fn(() => Promise.resolve(remoteOk(undefined))) const face = { llm: { - providers: vi.fn(() => Promise.resolve(ok({ - 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 }, - { provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], active: false }, - { provider: 'zombie', displayName: 'zombie', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'zombie'], active: false }, - { provider: 'broken', displayName: 'broken', settingsNs: 'llm-pi-ai', settingsPath: ['nope', 'x'], active: false }, - { provider: 'plain', displayName: 'plain', settingsNs: 'llm-plain', settingsPath: ['profiles', 'plain'], active: false }, - ], - }))), - models: vi.fn(() => Promise.resolve(ok({ groups: [], failures: [] }))), + listProviders: vi.fn(() => Promise.resolve(remoteOk([ + { id: 'deepseek-official', name: 'DeepSeek' }, + { id: 'openai', name: 'openai' }, + ]))), + listConfigurableProviders: vi.fn(() => Promise.resolve(remoteOk([ + { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true }, + { provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: true }, + { provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], active: false }, + { provider: 'zombie', displayName: 'zombie', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'zombie'], active: false }, + { provider: 'broken', displayName: 'broken', settingsNs: 'llm-pi-ai', settingsPath: ['nope', 'x'], active: false }, + { provider: 'plain', displayName: 'plain', settingsNs: 'llm-plain', settingsPath: ['profiles', 'plain'], active: false }, + ].map(({ active: _active, ...entry }) => entry)))), + discoverModels: vi.fn(() => Promise.resolve(remoteOk([]))), }, settings: { describe: vi.fn(() => Promise.resolve(remoteOk({ writable: true, hasDocument: false, namespaces: wireNamespaces() }))), @@ -315,12 +307,11 @@ describe('ModelsSection', () => { it('skips the draft seat when a refresh drops the dormant row', async () => { const { renderSlot, face, controller } = await mountSection() fireEvent.click(screen.getByRole('button', { name: en.add })) - face.llm.providers.mockImplementation(() => Promise.resolve(ok({ - 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 }, - ], - }))) + const directory = [ + { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true }, + { provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: true }, + ].map(({ active: _active, ...entry }) => entry) + face.llm.listConfigurableProviders.mockImplementation(() => Promise.resolve(remoteOk(directory))) renderSlot.mockClear() await act(async () => { await controller.load() }) // The draft card is still open while its row is gone from the directory. @@ -448,7 +439,7 @@ describe('ModelsSection', () => { expect(mutate).not.toHaveBeenCalled() // The saved key re-loads the join; the settings answer rides the shared // mirror, so the reload shows as a directory read rather than a describe. - await waitFor(() => { expect(face.llm.providers.mock.calls.length).toBeGreaterThan(1) }) + await waitFor(() => { expect(face.llm.listProviders.mock.calls.length).toBeGreaterThan(1) }) expect((await screen.findByRole('status')).textContent).toBe( providerCopy(en.savedProvider, { provider: 'deepseek-official', displayName: 'DeepSeek' }), ) @@ -1254,7 +1245,7 @@ describe('ModelsSection', () => { it('renders the load failure with a retry control', async () => { const face = scriptedFace() - face.face.llm.providers = vi.fn(() => Promise.resolve(fail('directory down', 'internal'))) as never + face.face.llm.listProviders = vi.fn(() => Promise.resolve(remoteFail('directory down', 'internal'))) as never const controller = new ModelsSettingsStore( face.face as unknown as WireFace, settingsSchema, new SettingsDescribeMirror(face.face as never)) await controller.load() diff --git a/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx b/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx index d45736cd6d..54b7bdd815 100644 --- a/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx @@ -3,7 +3,7 @@ import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import Schema from '@deepseek-ai/schemastery' -import type { JsonValue, RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client' +import type { JsonValue, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client' import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime' import { DeepSeekOnboardingDialog } from '../src/client/DeepSeekOnboardingDialog.tsx' import type { DeepSeekOnboardingDialogProps } from '../src/client/DeepSeekOnboardingDialog.tsx' @@ -17,10 +17,6 @@ afterEach(() => { document.getElementById('root')?.remove() }) -let nextRpc = 0 -function ok(value: T): RpcResponse { - return { rpcId: `onboarding-${nextRpc++}` as never, result: { ok: true, value } } -} /** Credentials answers over the Remote carrier, which has no envelope. */ function remoteOk(value: T) { return { ok: true as const, value } @@ -91,20 +87,25 @@ function harness(options: { }) const face = { llm: { - providers: () => { + listProviders: () => { if (options.providersReject === true) return Promise.reject(new Error('provider transport unavailable')) - return Promise.resolve(ok({ - providers: options.provider === false + return Promise.resolve(remoteOk( + options.provider === false || options.providerActive === false ? [] - : [{ - provider: 'deepseek-official', - displayName: 'DeepSeek', - settingsNs: options.providerSettingsNs ?? 'llm-deepseek', - settingsPath: [], - active: options.providerActive ?? true, - }], - })) + : [{ id: 'deepseek-official', name: 'DeepSeek' }], + )) }, + listConfigurableProviders: () => Promise.resolve(remoteOk( + options.provider === false + ? [] + : [{ + provider: 'deepseek-official', + displayName: 'DeepSeek', + settingsNs: options.providerSettingsNs ?? 'llm-deepseek', + settingsPath: [], + }], + )), + discoverModels: () => Promise.resolve(remoteOk([])), }, settings: { describe: () => Promise.resolve(remoteOk({ diff --git a/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx b/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx index e6168398af..0bf6b59861 100644 --- a/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx @@ -4,7 +4,7 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re import { afterEach, describe, expect, it, vi } from 'vitest' import Schema from '@deepseek-ai/schemastery' import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime' -import type { JsonValue, RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client' +import type { JsonValue, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client' import { ModelsSection, providerCopy } from '../src/client/ModelsSection.tsx' import type { ModelsSectionInjected, ModelsSectionProps } from '../src/client/ModelsSection.tsx' import { CustomProviderCard } from '../src/client/CustomProviderCard.tsx' @@ -38,12 +38,11 @@ const PiAiConfig = Schema.object({ })), }) -let nextRpc = 0 -function ok(value: T): RpcResponse { - return { rpcId: `r-${nextRpc++}` as never, result: { ok: true, value } } +function ok(value: T) { + return { ok: true as const, value } } -function fail(message: string, code: string): RpcResponse { - return { rpcId: `r-${nextRpc++}` as never, result: { ok: false, error: { code, message, details: {} } as never } } +function fail(message: string, code: string) { + return { ok: false as const, error: { code, message, details: {} } } } /** Credentials answers over the Remote carrier, which has no envelope. */ function remoteOk(value: T) { @@ -88,22 +87,23 @@ function scriptedFace(options: { openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy.example/v1' }, } const namespace = piAiNamespace(providers, options.userProviders ?? providers, options.baseProviders ?? {}) - const discover = options.discover ?? vi.fn(() => Promise.resolve(ok({ models: [] }))) + const discover = options.discover ?? vi.fn(() => Promise.resolve(ok([]))) const mutate = options.mutate ?? vi.fn(() => Promise.resolve(remoteOk(namespace))) const set = options.set ?? vi.fn(() => Promise.resolve(remoteOk(undefined))) const face = { llm: { - providers: vi.fn(() => Promise.resolve(ok({ - providers: Object.keys(providers).map(provider => ({ + listProviders: vi.fn(() => Promise.resolve(ok( + Object.keys(providers).map(provider => ({ id: provider, name: provider })), + ))), + listConfigurableProviders: vi.fn(() => Promise.resolve(ok( + Object.keys(providers).map(provider => ({ provider, displayName: provider, settingsNs: 'llm-pi-ai', settingsPath: ['providers', provider], - active: true, declared: options.declaredRoutes?.includes(provider) ?? false, })), - }))), - models: vi.fn(() => Promise.resolve(ok({ groups: [], failures: [] }))), + ))), discoverModels: discover, }, settings: { @@ -132,9 +132,9 @@ interface MutateCall { /** The first interrogation payload; fails the case when nothing was asked. */ function firstProbe(discover: ReturnType): unknown { - const call = (discover.mock.calls as unknown as [unknown][])[0]?.[0] + const call = (discover.mock.calls as unknown as [string, Record][])[0] if (call === undefined) throw new Error('no interrogation was recorded') - return call + return { settingsNs: call[0], ...call[1] } } /** @@ -440,7 +440,7 @@ describe('capacity spellings', () => { describe('endpoint interrogation', () => { it('asks the endpoint the form shows, with a key that is not yet stored', async () => { - const discover = vi.fn(() => Promise.resolve(ok({ models: [{ id: 'acme-large', contextWindow: 65_536 }] }))) + const discover = vi.fn(() => Promise.resolve(ok([{ id: 'acme-large', contextWindow: 65_536 }]))) await mountSection({ discover }) openEditor('openai') @@ -460,7 +460,7 @@ describe('endpoint interrogation', () => { }) it('carries the protocol the profile already names', async () => { - const discover = vi.fn(() => Promise.resolve(ok({ models: [] }))) + const discover = vi.fn(() => Promise.resolve(ok([]))) await mountSection({ discover, providers: { openai: { baseURL: 'https://proxy.example/v1', api: 'openai-responses' } }, @@ -479,9 +479,9 @@ describe('endpoint interrogation', () => { }) it('adopts only the picked candidates, keeping a row the user already tuned', async () => { - const discover = vi.fn(() => Promise.resolve(ok({ - models: [{ id: 'kept', contextWindow: 999 }, { id: 'fresh', contextWindow: 4096, name: 'Fresh' }], - }))) + const discover = vi.fn(() => Promise.resolve(ok([ + { id: 'kept', contextWindow: 999 }, { id: 'fresh', contextWindow: 4096, name: 'Fresh' }, + ]))) const { mutate } = await mountSection({ discover, providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'kept', contextWindow: 111 }] } }, @@ -518,7 +518,7 @@ describe('endpoint interrogation', () => { }) it('reports an empty listing and a rejected transport', async () => { - const empty = vi.fn(() => Promise.resolve(ok({ models: [] }))) + const empty = vi.fn(() => Promise.resolve(ok([]))) await mountSection({ discover: empty }) openEditor('openai') fireEvent.click(screen.getByText(en.fetchModels)) @@ -533,7 +533,7 @@ describe('endpoint interrogation', () => { }) it('can be asked for a configured route even with no endpoint', async () => { - const discover = vi.fn(() => Promise.resolve(ok({ models: [{ id: 'from-registry' }] }))) + const discover = vi.fn(() => Promise.resolve(ok([{ id: 'from-registry' }]))) await mountSection({ discover, providers: { openai: {} } }) openEditor('openai') @@ -585,7 +585,7 @@ describe('endpoint interrogation', () => { }) it('closes the picker without adopting anything on cancel', async () => { - const discover = vi.fn(() => Promise.resolve(ok({ models: [{ id: 'fresh' }] }))) + const discover = vi.fn(() => Promise.resolve(ok([{ id: 'fresh' }]))) const { mutate } = await mountSection({ discover }) openEditor('openai') @@ -599,9 +599,9 @@ describe('endpoint interrogation', () => { }) it('toggles a candidate off and back on before adopting', async () => { - const discover = vi.fn(() => Promise.resolve(ok({ - models: [{ id: 'a' }, { id: 'b', maxTokens: 2048 }], - }))) + const discover = vi.fn(() => Promise.resolve(ok([ + { id: 'a' }, { id: 'b', maxTokens: 2048 }, + ]))) const { mutate } = await mountSection({ discover }) openEditor('openai') @@ -620,9 +620,9 @@ describe('endpoint interrogation', () => { }) it('selects and clears every discovered candidate in one action', async () => { - const discover = vi.fn(() => Promise.resolve(ok({ - models: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], - }))) + const discover = vi.fn(() => Promise.resolve(ok([ + { id: 'a' }, { id: 'b' }, { id: 'c' }, + ]))) await mountSection({ discover }) openEditor('openai') @@ -664,15 +664,12 @@ describe('provider rows', () => { it('shows no tag when the adapter draws no catalog distinction', async () => { const scripted = scriptedFace({ providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY' } } }) - scripted.face.llm.providers = vi.fn(() => Promise.resolve(ok({ - providers: [{ - provider: 'openai', - displayName: 'openai', - settingsNs: 'llm-pi-ai', - settingsPath: ['providers', 'openai'], - active: true, - }], - }))) as never + scripted.face.llm.listConfigurableProviders = vi.fn(() => Promise.resolve(ok([{ + provider: 'openai', + displayName: 'openai', + settingsNs: 'llm-pi-ai', + settingsPath: ['providers', 'openai'], + }]))) as never const controller = new ModelsSettingsStore( scripted.face as unknown as WireFace, settingsSchema, new SettingsDescribeMirror(scripted.face as never)) await controller.load() @@ -828,16 +825,13 @@ describe('hand-declared providers', () => { }) // The reload after the write answers with the renamed route, exactly as // the adapter re-registers it. - face.llm.providers = vi.fn(() => Promise.resolve(ok({ - providers: [{ - provider: 'acme-gateway', - displayName: 'Acme 网关', - settingsNs: 'llm-pi-ai', - settingsPath: ['providers', 'acme-gateway'], - active: true, - declared: true, - }], - }))) + face.llm.listConfigurableProviders = vi.fn(() => Promise.resolve(ok([{ + provider: 'acme-gateway', + displayName: 'Acme 网关', + settingsNs: 'llm-pi-ai', + settingsPath: ['providers', 'acme-gateway'], + declared: true, + }]))) openEditor('acme-gateway') fireEvent.change(screen.getByLabelText(en.customDisplayName), { target: { value: 'Acme 网关' } }) diff --git a/packages/client/ui-settings-models/tests/store.client.spec.ts b/packages/client/ui-settings-models/tests/store.client.spec.ts index 677ca14418..8ce1724b32 100644 --- a/packages/client/ui-settings-models/tests/store.client.spec.ts +++ b/packages/client/ui-settings-models/tests/store.client.spec.ts @@ -58,10 +58,33 @@ function api(overrides: { describeCredentials?: (refs: readonly string[]) => Promise>> } = {}) { const seenRefs: string[][] = [] + const providers = overrides.providers ?? (() => Promise.resolve(ok({ providers: DIRECTORY }))) + let providerBatch: Promise> | undefined + let providerBatchReads = 0 + const readProviderBatch = (): Promise> => { + providerBatch ??= providers() + const current = providerBatch + providerBatchReads += 1 + if (providerBatchReads % 2 === 0) providerBatch = undefined + return current + } + const mapProviderBatch = async ( + project: (rows: typeof DIRECTORY) => T, + ): Promise> => { + const response = await readProviderBatch() + return response.result.ok + ? remoteOk(project(response.result.value.providers)) + : remoteFail(response.result.error.message) + } const face = { llm: { - providers: overrides.providers ?? (() => Promise.resolve(ok({ providers: DIRECTORY }))), - models: () => Promise.resolve(ok({ groups: [], failures: [] })), + listProviders: () => mapProviderBatch(rows => rows + .filter(row => row.active) + .map(row => ({ id: row.provider, name: row.displayName }))), + listConfigurableProviders: () => mapProviderBatch(rows => rows + .filter(row => row.settingsNs !== '') + .map(({ active: _active, ...row }) => row)), + discoverModels: () => Promise.resolve(remoteOk([])), }, settings: { describe: overrides.describeSettings diff --git a/packages/client/ui-skill/README.i18n.yaml b/packages/client/ui-skill/README.i18n.yaml index 0e5bc155d8..acedab1603 100644 --- a/packages/client/ui-skill/README.i18n.yaml +++ b/packages/client/ui-skill/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-skill/README.md -README.md: 7d43f0a28010b68cf46b43376c36895e3d74f66b -README.zh.md: 846f1703ecf3460a370954655cee66a9c67acf21 +README.md: de564bd533f6cec83bb0a55e6ebfa687fa716ee9 +README.zh.md: 2408a29b83befd68785f7e7feccc588b6372a6fb diff --git a/packages/client/ui-skill/README.md b/packages/client/ui-skill/README.md index 7d43f0a280..de564bd533 100644 --- a/packages/client/ui-skill/README.md +++ b/packages/client/ui-skill/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-client-ui-skill` lets users invoke skills by typing `/name` in the composer: the suggestion menu offers user-invocable skills from the `skill.list` RPC, and a pick lands the literal `/name ` text that the host then loads as the skill's instructions. Loading is deterministic: the host's pre-step boundary (`dsh-tool-skill`) recognizes the whitespace-bounded `/name` token in the sent message and injects the rendered `` for every entry point, so a menu pick, a hand-typed token, and a TUI/ACP prompt all load the skill the same way. Settled skill calls render in the conversation as an expandable `Instructions` card, derived only from the frozen call/result slice. +`dsh-client-ui-skill` lets users invoke skills by typing `/name` in the composer: the suggestion menu offers user-invocable skills from the `skills/list` Remote, and a pick lands the literal `/name ` text that the host then loads as the skill's instructions. Loading is deterministic: the host's pre-step boundary (`dsh-tool-skill`) recognizes the whitespace-bounded `/name` token in the sent message and injects the rendered `` for every entry point, so a menu pick, a hand-typed token, and a TUI/ACP prompt all load the skill the same way. Settled skill calls render in the conversation as an expandable `Instructions` card, derived only from the frozen call/result slice. ## Table of Contents @@ -29,7 +29,7 @@ Type `/` in the composer and pick a skill from the suggestions, or type `/name` ### What the source offers -Ordinary-session candidates come from the `skill.list` RPC; the host serves every user-invocable skill, and a `modelInvocable: false` entry (a `disable-model-invocation` skill, whose only entry point is this path) wears the user-only marker as a description prefix in the active language. Results filter by `startsWith(query)`. A failed `skill.list` is logged and folded into a silent menu-group drop — the menu shows only pending/ready states. +Ordinary-session candidates come from the `skills/list` Remote; the host serves every user-invocable skill, and a `modelInvocable: false` entry (a `disable-model-invocation` skill, whose only entry point is this path) wears the user-only marker as a description prefix in the active language. Results filter by `startsWith(query)`. A failed `skills/list` call is logged and folded into a silent menu-group drop — the menu shows only pending/ready states. ### The skill tool row diff --git a/packages/client/ui-skill/README.zh.md b/packages/client/ui-skill/README.zh.md index 846f1703ec..2408a29b83 100644 --- a/packages/client/ui-skill/README.zh.md +++ b/packages/client/ui-skill/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -`dsh-client-ui-skill` 让用户通过在编辑器中键入 `/name` 来调用 skill:建议菜单从 `skill.list` RPC 提供用户可调用的 skill 候选,选择一项会落下字面文本 `/name `,宿主随后将其加载为 skill 的指令。加载是确定性的:宿主的 pre-step 边界(`dsh-tool-skill`)识别发出消息中以空白为界的 `/name` token,并为每个入口注入渲染后的 ``,因此菜单 pick、手动键入的 token 与 TUI/ACP 提示词都以同一种方式加载 skill。已结算的 skill 调用在对话中渲染为可展开的 `Instructions` 卡片,只从冻结的调用/结果切片派生。 +`dsh-client-ui-skill` 让用户通过在编辑器中键入 `/name` 来调用 skill:建议菜单从 `skills/list` Remote 提供用户可调用的 skill 候选,选择一项会落下字面文本 `/name `,宿主随后将其加载为 skill 的指令。加载是确定性的:宿主的 pre-step 边界(`dsh-tool-skill`)识别发出消息中以空白为界的 `/name` token,并为每个入口注入渲染后的 ``,因此菜单 pick、手动键入的 token 与 TUI/ACP 提示词都以同一种方式加载 skill。已结算的 skill 调用在对话中渲染为可展开的 `Instructions` 卡片,只从冻结的调用/结果切片派生。 ## 目录 @@ -29,7 +29,7 @@ kind: "package-reference" ### source 提供什么 -普通会话的候选来自 `skill.list` RPC;宿主提供每一个用户可调用的 skill,`modelInvocable: false` 的条目(即 `disable-model-invocation` skill,此路径是其唯一入口)会以当前语言把仅限用户标记作为描述前缀带上。结果按 `startsWith(query)` 过滤。`skill.list` 失败时会被记录并静默丢弃该菜单组——菜单只显示 pending/ready 状态。 +普通会话的候选来自 `skills/list` Remote;宿主提供每一个用户可调用的 skill,`modelInvocable: false` 的条目(即 `disable-model-invocation` skill,此路径是其唯一入口)会以当前语言把仅限用户标记作为描述前缀带上。结果按 `startsWith(query)` 过滤。`skills/list` 调用失败时会被记录并静默丢弃该菜单组——菜单只显示 pending/ready 状态。 ### skill 工具行 diff --git a/packages/client/ui-skill/tests/browser-plugin.client.spec.ts b/packages/client/ui-skill/tests/browser-plugin.client.spec.ts index c4f2e3ea63..208baac3bc 100644 --- a/packages/client/ui-skill/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-skill/tests/browser-plugin.client.spec.ts @@ -27,11 +27,7 @@ type SkillRow = { name: string; description: string; whenToUse?: string; modelIn type ListResult = | { ok: true; value: { skills: SkillRow[] } } | { ok: false; error: { code: string; message: string; details: object } } -type ListFn = (payload: object, signal?: AbortSignal) => Promise<{ result: ListResult }> -type InvokeResult = - | { ok: true; value: { accepted: true } } - | { ok: false; error: { code: string; message: string; details: object } } -type InvokeFn = (payload: object) => Promise<{ result: InvokeResult }> +type ListFn = (payload: object, signal?: AbortSignal) => Promise interface PresentationCapture { slots: SlotRegistry @@ -63,18 +59,17 @@ function providePresentation(ctx: Context): PresentationCapture { } /** Boot the plugin over fake slash/connection faces; returns the captured source and its ctx. */ -async function bench(list: ListFn, addressed?: SessionId, invoke?: InvokeFn) { +async function bench(list: ListFn, addressed?: SessionId) { const ctx = new Context() let captured: InputTriggerSource | undefined ctx.provide('inputTriggers', { registerSource: (src: InputTriggerSource) => { captured = src; return () => {} } }) - const defaultInvoke: InvokeFn = () => Promise.resolve({ result: { ok: true as const, value: { accepted: true as const } } }) - ctx.provide('connection', { api: { skills: { list, invoke: invoke ?? defaultInvoke } } }) + ctx.provide('connection', {}) ctx.provide('sessions', { subagentAddress: (id: SessionId) => id === addressed ? { parentSessionId: sid('parent'), childSessionId: id, mode: 'continuable' as const } : undefined, }) - const remote = new TestRemote(ctx) + const remote = new TestRemote(ctx, { skills: { list } }) providePresentation(ctx) await ctx.plugin({ inject: [...inject], apply }).await() return { ctx, source: captured!, remote } @@ -86,7 +81,7 @@ const CATALOG: SkillRow[] = [ { name: 'deploy', description: 'deploy flow', modelInvocable: true }, ] -const listOk = (skills: SkillRow[]): ListFn => () => Promise.resolve({ result: { ok: true as const, value: { skills } } }) +const listOk = (skills: SkillRow[]): ListFn => () => Promise.resolve({ ok: true as const, value: { skills } }) /** Counting fake: records payloads, resolves the shared catalog. */ function countingList(skills: SkillRow[] = CATALOG) { @@ -113,9 +108,9 @@ describe('apply', () => { it('registers the dedicated skill row and its locale dictionaries', async () => { const ctx = new Context() ctx.provide('inputTriggers', { registerSource: () => () => {} }) - ctx.provide('connection', { api: { skills: { list: listOk(CATALOG) } } }) + ctx.provide('connection', {}) ctx.provide('sessions', { subagentAddress: () => undefined }) - new TestRemote(ctx) + new TestRemote(ctx, { skills: { list: listOk(CATALOG) } }) const presentation = providePresentation(ctx) await ctx.plugin({ inject: [...inject], apply }).await() const entry = presentation.slots.entries('tool.call.toolview')[0] @@ -151,8 +146,8 @@ describe('apply', () => { // InputTriggerService itself injects 'sessions'; the stub unblocks its fiber. ctx.provide('sessions', {}) await ctx.plugin(InputTriggerService).await() - ctx.provide('connection', { api: { skills: { list: listOk(CATALOG) } } }) - new TestRemote(ctx) + ctx.provide('connection', {}) + new TestRemote(ctx, { skills: { list: listOk(CATALOG) } }) const presentation = providePresentation(ctx) const fiber = ctx.plugin({ inject: [...inject], apply }) await fiber.await() @@ -188,10 +183,10 @@ describe('candidates: sessionId addressing', () => { it('rejects on a failed result (the slash shell owns the menu-side fold)', async () => { const { source } = await bench(() => Promise.resolve({ - result: { ok: false, error: { code: 'internal', message: 'boom', details: {} } }, + ok: false, error: { code: 'internal', message: 'boom', details: {} }, })) await expect(source.candidates(proj('s1'), req('co'))) - .rejects.toThrow('skill.list failed: internal: boom') + .rejects.toThrow('skills/list failed: internal: boom') }) it('does not fetch Agent-bound skills for an addressed child', async () => { @@ -248,7 +243,7 @@ describe('catalog cache', () => { const { source } = await bench((payload) => { payloads.push(payload) return fail - ? Promise.resolve({ result: { ok: false as const, error: { code: 'internal', message: 'boom', details: {} } } }) + ? Promise.resolve({ ok: false as const, error: { code: 'internal', message: 'boom', details: {} } }) : listOk(CATALOG)(payload) }) await expect(source.candidates(proj('s1'), req(''))).rejects.toThrow('boom') diff --git a/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts b/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts index dc37fd7c14..3cb8c2adbd 100644 --- a/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts +++ b/packages/client/ui-workspace/tests/workspaces-service.client.spec.ts @@ -6,12 +6,6 @@ import type { import type { IWorkspaces, WorkspaceId, WorkspaceSnapshot, WorkspaceView, } from '@deepseek-ai/dsh-api-workspace-controller/client' -import { - RpcId, - 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' @@ -153,16 +147,6 @@ class FakeWorkspaces implements IWorkspaces { } } -let nextRpcId = 0 - -function ok(value: T): RpcResponse { - return { rpcId: RpcId(`workspace-test-${nextRpcId++}`), result: { ok: true, value } } -} - -function failed(error: RpcError): RpcResponse { - return { rpcId: RpcId(`workspace-test-${nextRpcId++}`), result: { ok: false, error } } -} - const listing: DirectoryListing = { path: '/home/u', home: '/home/u', @@ -171,38 +155,6 @@ const listing: DirectoryListing = { truncated: false, } -class FakeApiClient implements IApiClient { - readonly calls: Array<{ readonly method: string; readonly payload: unknown }> = [] - - onDescribe: IApiClient['host']['describe'] = () => Promise.resolve(ok({ - version: 'test', - cwd: '/home/u', - attachedSessions: 0, - home: '/home/u', - canOpenPath: true, - })) - onOpenPath: IApiClient['host']['openPath'] = () => Promise.resolve(ok({ opened: true })) - - declare readonly skills: IApiClient['skills'] - declare readonly agentPresets: IApiClient['agentPresets'] - declare readonly settings: IApiClient['settings'] - declare readonly llm: IApiClient['llm'] - - readonly host: IApiClient['host'] = { - describe: (payload, signal) => this.record('host.describe', payload, this.onDescribe(payload, signal)), - openPath: (payload, signal) => this.record('host.openPath', payload, this.onOpenPath(payload, signal)), - } - - callsOf(method: string): unknown[] { - return this.calls.filter(call => call.method === method).map(call => call.payload) - } - - private record(method: string, payload: unknown, response: Promise): Promise { - this.calls.push({ method, payload }) - return response - } -} - /** The directory-picking Remote namespace, recorded and scripted per case. */ class FakeDirectoryPicker { readonly calls: { method: string; payload: unknown }[] = [] @@ -236,18 +188,16 @@ 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, directoryPicker, sessions, uiWorkspace, workspaces } + return { ctx, directoryPicker, sessions, uiWorkspace, workspaces } } async function flush(): Promise { @@ -484,9 +434,6 @@ describe('UiWorkspaceService', () => { 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.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.directoryPicker.onPick = () => Promise.resolve({ ok: false, error: { code: 'internal', message: 'no chooser', details: {} }, }) @@ -503,7 +450,5 @@ describe('UiWorkspaceService', () => { await expect(b.uiWorkspace.createDirectory('/home/u', 'new')).rejects.toMatchObject({ rpcError: { code: 'directory-exists' }, }) - b.api.onOpenPath = () => Promise.resolve(failed({ code: 'internal', message: 'boom', details: {} })) - await expect(b.uiWorkspace.openPath('/missing')).rejects.toThrow('path open failed: boom') }) }) diff --git a/packages/context/file-reference/README.i18n.yaml b/packages/context/file-reference/README.i18n.yaml index 611627675c..5c7142368a 100644 --- a/packages/context/file-reference/README.i18n.yaml +++ b/packages/context/file-reference/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/context/file-reference/README.md -README.md: 1f4e04a8c3f78feefd86e7e90096d2c7f27e86cb -README.zh.md: 5b30a9950dd6920bd3e951224d743e4c68698e0e +README.md: 7571aecbc42cdcda39a2c7ad5416205e7f4c108c +README.zh.md: ad13336f47b3069fd35e75eb2aed994221bb9e3c diff --git a/packages/context/file-reference/README.md b/packages/context/file-reference/README.md index 1f4e04a8c3..7571aecbc4 100644 --- a/packages/context/file-reference/README.md +++ b/packages/context/file-reference/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Host-backed user interfaces use `dsh-file-reference` to offer `@file` completion: a UI asks for path candidates for the addressed agent, the model types `@path` or `@"path with spaces"`, and picking a candidate inserts the matching mention as ordinary prompt text. The seam itself owns no filesystem access — a concrete provider such as `@deepseek-ai/dsh-file-reference-local` supplies candidates, ranking, caching, and invalidation. Selecting a candidate never reads or attaches file contents; the model must call a filesystem tool to inspect a file. The same discovery is callable from browser consumers through the remote `fileReferences/list` method without an API Proxy route. +Host-backed user interfaces use `dsh-file-reference` to offer `@file` completion: a UI asks for path candidates for the addressed agent, the model types `@path` or `@"path with spaces"`, and picking a candidate inserts the matching mention as ordinary prompt text. The seam itself owns no filesystem access — a concrete provider such as `@deepseek-ai/dsh-file-reference-local` supplies candidates, ranking, caching, and invalidation. Selecting a candidate never reads or attaches file contents; the model must call a filesystem tool to inspect a file. Session Controller exposes the same discovery to browser consumers through the `fileReferences/list` Remote. ## Table of Contents @@ -33,7 +33,7 @@ An `@path` token at the start of input or after whitespace triggers completion; ### Getting candidates -`ctx.fileReferences.list(agent, query, signal)` returns path-only file and directory candidates for one agent's working directory, deterministically ranked by the provider. Directory mentions render with a trailing `/` so completion can descend another level. Browser consumers call the same discovery as `ctx.remote.fileReferences.list`; the reserved trailing signal cancels a slow autocomplete. +`ctx.fileReferences.list(agent, query, signal)` returns path-only file and directory candidates for one agent's working directory, deterministically ranked by the provider. Directory mentions render with a trailing `/` so completion can descend another level. Browser consumers call the Session Controller adapter as `ctx.remote.fileReferences.list`; the trailing signal cancels a slow autocomplete. ### Pairing with a provider @@ -51,13 +51,13 @@ This section explains the design of the seam; the observable behavior is covered ### Design concept -The package is one separation: an abstract discovery service plus a shared, browser-safe mention grammar, with providers owning namespace access, ranking, caching, and invalidation. The service is a `TypertRemoteService` whose `list` contract is remotely callable as the unary `fileReferences/list` method, so the same seam serves in-process and browser consumers. +The package separates an abstract discovery service from a shared, browser-safe mention grammar, with providers owning namespace access, ranking, caching, and invalidation. The service remains wire-neutral; `dsh-api-session-controller` owns the `fileReferences/list` Remote adapter and delegates to the active provider after resolving its Agent. ### Source map | File | Role | |---|---| -| [`src/index.ts`](src/index.ts) | Abstract `FileReferenceService`, `FILE_REFERENCE_PROMPT`, remote `list` face | +| [`src/index.ts`](src/index.ts) | Abstract `FileReferenceService` and `FILE_REFERENCE_PROMPT` | | [`src/grammar.ts`](src/grammar.ts) | `activeAtToken` recognition and `formatFileMention` rendering | | [`src/types.ts`](src/types.ts) | `FileReferenceCandidate` path-only result type | | [`src/invariant.ts`](src/invariant.ts) | Invariant companion for the discovery contract | diff --git a/packages/context/file-reference/README.zh.md b/packages/context/file-reference/README.zh.md index 5b30a9950d..ad13336f47 100644 --- a/packages/context/file-reference/README.zh.md +++ b/packages/context/file-reference/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -宿主驱动 UI 使用 `dsh-file-reference` 提供 `@file` 补全:UI 为指定 agent 请求路径候选,模型输入 `@path` 或 `@"path with spaces"`,选中候选后,匹配的 mention 作为普通提示词文本插入。seam 本身不拥有文件系统访问——具体提供方(如 `@deepseek-ai/dsh-file-reference-local`)负责提供候选、排序、缓存与失效。选中候选绝不读取或附带文件内容;模型必须调用文件系统工具才能查看文件。浏览器消费方无需 API Proxy 路由,即可通过远程 `fileReferences/list` 方法调用同一发现能力。 +宿主驱动 UI 使用 `dsh-file-reference` 提供 `@file` 补全:UI 为指定 agent 请求路径候选,模型输入 `@path` 或 `@"path with spaces"`,选中候选后,匹配的 mention 作为普通提示词文本插入。seam 本身不拥有文件系统访问——具体提供方(如 `@deepseek-ai/dsh-file-reference-local`)负责提供候选、排序、缓存与失效。选中候选绝不读取或附带文件内容;模型必须调用文件系统工具才能查看文件。Session Controller 通过 `fileReferences/list` Remote 向浏览器消费方暴露同一发现能力。 ## 目录 @@ -33,7 +33,7 @@ kind: "package-reference" ### 获取候选 -`ctx.fileReferences.list(agent, query, signal)` 返回指定 agent 工作目录中仅含路径的文件与目录候选,由提供方确定性地排序。目录 mention 呈现时带尾随 `/`,使补全可以继续深入下一层。浏览器消费方通过 `ctx.remote.fileReferences.list` 调用同一发现能力;保留的末位 signal 参数可取消慢速自动补全。 +`ctx.fileReferences.list(agent, query, signal)` 返回指定 agent 工作目录中仅含路径的文件与目录候选,由提供方确定性地排序。目录 mention 呈现时带尾随 `/`,使补全可以继续深入下一层。浏览器消费方通过 Session Controller adapter 的 `ctx.remote.fileReferences.list` 调用同一发现能力;末位 signal 参数可取消慢速自动补全。 ### 搭配提供方 @@ -51,13 +51,13 @@ kind: "package-reference" ### 设计理念 -本包建立在一个分离上:抽象发现服务加共享、浏览器安全的 mention 语法,由提供方负责命名空间访问、排序、缓存与失效。该服务是 `TypertRemoteService`,其 `list` 约定可通过一元 `fileReferences/list` 方法远程调用,因此同一 seam 同时服务进程内与浏览器消费方。 +本包把抽象发现服务与共享、浏览器安全的 mention 语法分开,由提供方负责命名空间访问、排序、缓存与失效。该服务保持 wire 中立;`dsh-api-session-controller` 持有 `fileReferences/list` Remote adapter,并在解析 Agent 后委派给当前 provider。 ### 源码地图 | 文件 | 职责 | |---|---| -| [`src/index.ts`](src/index.ts) | 抽象 `FileReferenceService`、`FILE_REFERENCE_PROMPT`、远程 `list` 面 | +| [`src/index.ts`](src/index.ts) | 抽象 `FileReferenceService` 与 `FILE_REFERENCE_PROMPT` | | [`src/grammar.ts`](src/grammar.ts) | `activeAtToken` 识别与 `formatFileMention` 渲染 | | [`src/types.ts`](src/types.ts) | 仅含路径的结果类型 `FileReferenceCandidate` | | [`src/invariant.ts`](src/invariant.ts) | 发现约定的不变式伴生插件 | diff --git a/packages/context/file-reference/tests/service.spec.ts b/packages/context/file-reference/tests/service.spec.ts index 1f41ad6f79..388af70458 100644 --- a/packages/context/file-reference/tests/service.spec.ts +++ b/packages/context/file-reference/tests/service.spec.ts @@ -1,4 +1,4 @@ -/** The Remote face delegates to the provider's discovery contract unchanged. */ +/** The abstract service preserves the provider's discovery contract. */ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -6,7 +6,7 @@ import { FileReferenceService } from '../src/index.ts' import type { FileReferenceCandidate } from '../src/types.ts' describe('FileReferenceService', () => { - it('serves the Remote face through the abstract discovery member', async () => { + it('registers a provider implementation without wrapping its discovery member', async () => { const candidates: FileReferenceCandidate[] = [{ path: 'src', kind: 'directory' }] const list = vi.fn((_agent: Agent, _query: string, _signal: AbortSignal) => Promise.resolve(candidates)) class StubProvider extends FileReferenceService { @@ -15,7 +15,7 @@ describe('FileReferenceService', () => { const provider = new StubProvider(new Context()) const agent = { id: 'target' } as unknown as Agent const signal = new AbortController().signal - await expect(provider.remoteExportList(agent, 'sr', signal)).resolves.toBe(candidates) + await expect(provider.list(agent, 'sr', signal)).resolves.toBe(candidates) expect(list).toHaveBeenCalledWith(agent, 'sr', signal) }) }) diff --git a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts index 633c553557..a4f182a645 100644 --- a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts @@ -348,11 +348,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [{ name: 'path', description: 'existing parent directory.' }, { name: 'name', description: 'child directory name.' }], returns: 'created absolute path.', }, - { - signature: 'openPath(path: string): Promise', - description: 'Open a path with the Host operating system.', - parameters: [{ name: 'path', description: 'absolute or Host-resolvable path.' }], - }, ], }, { diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 964f83ad24..4891cd5537 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -1625,10 +1625,10 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Owner share of one provider-card extension occurrence. */\nexport interface ProviderCardExtrasOwnerProps {\n /** The card\'s directory row (route id, display name, settings address, live state). */\n provider: ConfigurableProviderView\n /** Whether any layer configures this provider (its profile resolves); `false` while the add-provider draft edits a dormant row. */\n configured: boolean\n /** Whether the row\'s referenced api-key credential is confirmed configured (the page\'s credential join). */\n keyConfigured: boolean\n}', + '/** Owner share of one provider-card extension occurrence. */\nexport interface ProviderCardExtrasOwnerProps {\n /** The card\'s directory row (route id, display name, settings address, live state). */\n provider: ProviderDirectoryEntry\n /** Whether any layer configures this provider (its profile resolves); `false` while the add-provider draft edits a dormant row. */\n configured: boolean\n /** Whether the row\'s referenced api-key credential is confirmed configured (the page\'s credential join). */\n keyConfigured: boolean\n}', ], ownerPropsReferences: [ - 'ConfigurableProviderView', + 'ProviderDirectoryEntry', ], standardProps: [ 'useWorkspaces: SnapshotSelectorHook', diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 00f2992f94..eb1992f66d 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -854,12 +854,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [{ name: 'agent', description: 'target agent whose session cwd bounds discovery.' }, { name: 'query', description: 'path text following `@` or `@"`.' }, { name: 'signal', description: 'caller cancellation.' }], returns: 'deterministic path-only candidates.', }, - { - signature: '@Remote(\'list\') remoteExportList( agent: Agent, query: string, signal: AbortSignal, ): Promise', - description: 'Remote face of list; the decorator cannot mark the abstract member, so this concrete adapter carries the identical contract.', - parameters: [{ name: 'agent', description: 'target agent whose session cwd bounds discovery.' }, { name: 'query', description: 'path text following `@` or `@"`.' }, { name: 'signal', description: 'caller cancellation.' }], - returns: 'deterministic path-only candidates.', - }, ], }, { @@ -1118,7 +1112,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'the disposer, carrying {@link AdapterRegistrationHandle.replace}.', }, { - signature: 'listProviders(): LlmProviderInfo[]', + signature: '@Remote listProviders(): LlmProviderInfo[]', description: 'Describe provider routes with a registered adapter.', parameters: [], returns: 'detached provider metadata in registration order.', @@ -1130,23 +1124,30 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'a handle that withdraws all of them, and can atomically replace them.', }, { - signature: 'listConfigurableProviders(): LlmConfigurableProvider[]', + signature: '@Remote listConfigurableProviders(): LlmConfigurableProvider[]', description: 'List every declared configurable provider, registered or dormant.', parameters: [], returns: 'detached directory entries in declaration order.', }, { - signature: 'registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise, ): () => void', + signature: 'registerModelDiscovery( settingsNs: string, discover: ( request: LlmModelDiscoveryRequest, signal?: AbortSignal, ) => Promise, ): () => void', description: 'Offer to interrogate provider endpoints on behalf of the settings namespace this plugin owns. The namespace is the key because that is what a configuration surface already holds from the configurable-provider directory, and because a provider being *added* has no route to name yet. Disposed with the fiber.', - parameters: [{ name: 'settingsNs', description: 'the namespace whose profiles this discovery serves.' }, { name: 'discover', description: 'interrogates one endpoint; must honor `request.signal`.' }], + parameters: [{ name: 'settingsNs', description: 'the namespace whose profiles this discovery serves.' }, { name: 'discover', description: 'interrogates one endpoint and must honor the supplied signal.' }], returns: 'the disposer that withdraws the offer.', }, { - signature: 'async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, ): Promise', + signature: 'async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, signal?: AbortSignal, ): Promise', description: 'Interrogate one provider endpoint for the models it advertises. The request describes a draft, not a stored route, so nothing here reads or writes settings or credentials — the caller owns both, and the reply is candidate metadata a surface may offer for adoption.', - parameters: [{ name: 'settingsNs', description: 'namespace whose registered discovery serves this draft.' }, { name: 'request', description: 'the endpoint, protocol, and one-shot credential to use.' }], + parameters: [{ name: 'settingsNs', description: 'namespace whose registered discovery serves this draft.' }, { name: 'request', description: 'the endpoint, protocol, and one-shot credential to use.' }, { name: 'signal', description: 'caller cancellation.' }], returns: 'the advertised models, deduplicated in endpoint order.', }, + { + signature: '@Remote(\'discoverModels\') async remoteDiscoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, signal: AbortSignal, ): Promise', + description: 'Remote adapter for one draft provider interrogation.', + parameters: [{ name: 'settingsNs', description: 'namespace whose registered discovery serves this draft.' }, { name: 'request', description: 'endpoint, protocol, and one-shot credential to use.' }, { name: 'signal', description: 'caller cancellation supplied by the Remote carrier.' }], + returns: 'advertised models in endpoint order.', + throws: ['TypertRemoteFailure with `model-discovery-failed` when discovery refuses or fails.'], + }, { signature: 'providerRetryPolicy(provider: string): ResolvedRetryPolicy', description: 'Resolve the retry policy captured when one provider route was registered.', @@ -1375,6 +1376,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [{ name: 'request', description: 'Session identity and requested model selection.' }], returns: 'the normalized selection installed for the Session.', }, + { + signature: '@Remote(\'modelCatalog\') modelCatalog(): Promise', + description: 'Describe every currently routable model for Host-generation selectors.', + parameters: [], + returns: 'provider-grouped models, the deployment default, and isolated provider failures.', + }, + { + signature: '@Remote(\'openWorkspacePath\') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise', + description: 'Open a path resolved against one Session\'s workspace on the Host desktop.', + parameters: [{ name: 'request', description: 'Session identity and absolute or workspace-relative path.' }, { name: 'signal', description: 'caller lifetime; abort terminates inspection or the native command.' }], + returns: 'confirmation after the native opener accepts the path.', + throws: ['TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails.'], + }, { signature: '@Remote(\'rename\') rename(request: SessionRenameRequest): Promise', description: 'Rename one Session after explicitly resuming it.', @@ -1431,6 +1445,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, ], }, + { + key: 'sessionFileReferences', + summary: 'Host Remote adapter over the composed file-reference provider.', + description: 'Host Remote adapter over the composed file-reference provider.', + methods: [ + { + signature: '@Remote list( agent: Agent, query: string, signal: AbortSignal, ): Promise', + description: 'List file and directory candidates for one Agent\'s working directory.', + parameters: [{ name: 'agent', description: 'target Agent resolved from the Session identity on the wire.' }, { name: 'query', description: 'path text following `@` or `@"`.' }, { name: 'signal', description: 'caller cancellation.' }], + returns: 'deterministic path-only candidates from the composed provider.', + }, + ], + }, { key: 'sessionPersistence', summary: 'Durable append-only session storage.', @@ -1802,6 +1829,20 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, ], }, + { + key: 'sessionSkillCatalog', + summary: 'Host service backing `ctx.remote.skills` without activating a cold Agent.', + description: 'Host service backing `ctx.remote.skills` without activating a cold Agent.', + methods: [ + { + signature: '@Remote async list(request: SkillListRequest, signal: AbortSignal): Promise', + description: 'List the user-invocable skills visible to one Session composition.', + parameters: [{ name: 'request', description: 'Session identity whose cwd and preset select the catalog view.' }, { name: 'signal', description: 'caller lifetime carried by the Remote transport; admitted catalog reads retain their existing completion semantics.' }], + returns: 'user-invocable skill metadata without loading skill bodies.', + throws: ['TypertRemoteFailure when the Session cannot be inspected or no registry can serve it.'], + }, + ], + }, { key: 'sessionTelemetry', summary: 'Loadable form of the backend contract: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis\' standard behavior.', @@ -1946,6 +1987,20 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'the namespace\'s redacted view after the write.', throws: ['TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.'], }, + { + signature: '@Remote async openSettingsDocument(signal: AbortSignal): Promise', + description: 'Materialize the provider-owned settings document and open it in a native text editor.', + parameters: [{ name: 'signal', description: 'caller lifetime; abort terminates preparation or the native command.' }], + returns: 'confirmation after the native opener accepts the document.', + throws: ['TypertRemoteFailure when no document exists, preparation fails, or opening fails.'], + }, + { + signature: '@Remote async openAgentPresetDirectory( agentPreset: string, signal: AbortSignal, ): Promise', + description: 'Open one user-authored Agent preset directory or return its path when no native opener exists.', + parameters: [{ name: 'agentPreset', description: 'preset id resolved against Host-owned roots.' }, { name: 'signal', description: 'caller lifetime; abort terminates the native command.' }], + returns: 'an opened confirmation or the resolved directory for text display.', + throws: ['TypertRemoteFailure when the preset is missing, read-only, invalid, or cannot be opened.'], + }, ], }, { @@ -3355,6 +3410,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'AgentPreset', declaration: 'export interface AgentPreset {\n readonly id: string;\n readonly trust: PresetTrust;\n readonly path: string;\n readonly name?: string;\n readonly description?: string;\n readonly order?: number;\n readonly broken?: string;\n}', }, + { + name: 'AgentPresetDirectoryOpenValue', + declaration: 'export type AgentPresetDirectoryOpenValue = {\n readonly opened: true;\n} | {\n readonly opened: false;\n readonly path: string;\n};', + }, { name: 'AgentPresetDocument', declaration: 'export interface AgentPresetDocument {\n readonly agentPreset: string;\n readonly trust: PresetTrust;\n readonly content: string;\n readonly name?: string;\n readonly description?: string;\n}', @@ -4221,7 +4280,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'LlmModelDiscoveryRequest', - declaration: 'export interface LlmModelDiscoveryRequest {\n provider?: string;\n baseURL?: string;\n api?: string;\n apiKey?: string;\n signal?: AbortSignal;\n}', + declaration: 'export interface LlmModelDiscoveryRequest {\n provider?: string;\n baseURL?: string;\n api?: string;\n apiKey?: string;\n}', }, { name: 'LlmModelInfo', @@ -4245,7 +4304,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'LlmRuntime', - declaration: 'export class LlmRuntime extends Service {\n constructor(ctx: Context);\n registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle;\n listProviders(): LlmProviderInfo[];\n registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle;\n listConfigurableProviders(): LlmConfigurableProvider[];\n registerModelDiscovery(settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise): () => void;\n async discoverModels(settingsNs: string, request: LlmModelDiscoveryRequest): Promise;\n providerRetryPolicy(provider: string): ResolvedRetryPolicy;\n imageRequestPricing(provider: string, model: string): LlmImageRequestPricing | undefined;\n async listModels(provider: string): Promise;\n async resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise;\n async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise;\n async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise;\n stream(options: GenerateOptions): AsyncIterable;\n}', + declaration: 'export class LlmRuntime extends TypertRemoteService {\n constructor(ctx: Context);\n registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle;\n @Remote\n listProviders(): LlmProviderInfo[];\n registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle;\n @Remote\n listConfigurableProviders(): LlmConfigurableProvider[];\n registerModelDiscovery(settingsNs: string, discover: (request: LlmModelDiscoveryRequest, signal?: AbortSignal) => Promise): () => void;\n async discoverModels(settingsNs: string, request: LlmModelDiscoveryRequest, signal?: AbortSignal): Promise;\n @Remote(\'discoverModels\')\n async remoteDiscoverModels(settingsNs: string, request: LlmModelDiscoveryRequest, signal: AbortSignal): Promise;\n providerRetryPolicy(provider: string): ResolvedRetryPolicy;\n imageRequestPricing(provider: string, model: string): LlmImageRequestPricing | undefined;\n async listModels(provider: string): Promise;\n async resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise;\n async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise;\n async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise;\n stream(options: GenerateOptions): AsyncIterable;\n}', }, { name: 'LspHover', @@ -4383,6 +4442,18 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'MessageSourceMap', declaration: 'export interface MessageSourceMap {\n user: {\n kind: \'user\';\n };\n plugin: {\n kind: \'plugin\';\n plugin: string;\n } & ContextFormed;\n model: ModelMessageSource;\n tool: ToolMessageSource;\n}', }, + { + name: 'ModelCatalog', + declaration: 'export interface ModelCatalog {\n readonly default: ModelSelection;\n readonly routableProviders: readonly string[];\n readonly groups: readonly ModelProviderGroup[];\n readonly failures: readonly ModelCatalogFailure[];\n}', + }, + { + name: 'ModelCatalogFailure', + declaration: 'export interface ModelCatalogFailure {\n readonly id: string;\n readonly name: string;\n readonly message: string;\n}', + }, + { + name: 'ModelCatalogModel', + declaration: 'export interface ModelCatalogModel {\n readonly id: string;\n readonly name: string;\n readonly description?: string;\n readonly reasoning?: ModelReasoning;\n}', + }, { name: 'ModelMessageSource', declaration: 'export interface ModelMessageSource extends AssistantProvenance {\n kind: \'model\';\n}', @@ -4395,6 +4466,18 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'ModelModalityMap', declaration: 'export interface ModelModalityMap {\n text: \'text\';\n image: \'image\';\n}', }, + { + name: 'ModelProviderGroup', + declaration: 'export interface ModelProviderGroup {\n readonly id: string;\n readonly name: string;\n readonly models: readonly ModelCatalogModel[];\n}', + }, + { + name: 'ModelReasoning', + declaration: 'export interface ModelReasoning {\n readonly efforts: readonly ModelReasoningEffort[];\n readonly defaultEffort?: string;\n}', + }, + { + name: 'ModelReasoningEffort', + declaration: 'export interface ModelReasoningEffort {\n readonly id: string;\n readonly name: string;\n readonly description?: string;\n}', + }, { name: 'ObjectJsonSchema', declaration: 'export type ObjectJsonSchema = JsonSchemaNode & {\n type: \'object\';\n};', @@ -4589,7 +4672,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 \'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 \'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 \'internal\': {};\n}', }, { name: 'RpcId', @@ -4875,6 +4958,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SessionObservationOptions', declaration: 'export interface SessionObservationOptions {\n readonly signal?: AbortSignal;\n readonly projectionMode?: \'all\' | \'none\';\n}', }, + { + name: 'SessionOpenWorkspacePathRequest', + declaration: 'export interface SessionOpenWorkspacePathRequest {\n readonly sessionId: SessionId;\n readonly path: string;\n}', + }, + { + name: 'SessionOpenWorkspacePathValue', + declaration: 'export interface SessionOpenWorkspacePathValue {\n readonly opened: true;\n}', + }, { name: 'SessionPage', declaration: 'export interface SessionPage {\n readonly records: readonly SessionHistoryRecord[];\n readonly hasMore: boolean;\n}', @@ -5115,6 +5206,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SettingsDescriptor', declaration: 'export interface SettingsDescriptor {\n ns: SettingsNamespace;\n schema: unknown;\n value: unknown;\n revision: number;\n base?: unknown;\n user?: unknown;\n applies: SettingsApplies;\n secrets?: RedactedSecret[];\n}', }, + { + name: 'SettingsDocumentOpenValue', + declaration: 'export interface SettingsDocumentOpenValue {\n readonly opened: true;\n}', + }, { name: 'SettingsNamespace', declaration: 'export type SettingsNamespace = Branded<\'SettingsNamespace\'>;', @@ -5183,10 +5278,22 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SkillDefinition', declaration: 'export interface SkillDefinition extends SkillSummary {\n readonly content: string;\n readonly path?: string;\n readonly metadata?: Readonly>;\n}', }, + { + name: 'SkillEntry', + declaration: 'export interface SkillEntry {\n readonly name: string;\n readonly description: string;\n readonly whenToUse?: string;\n readonly modelInvocable: boolean;\n}', + }, { name: 'SkillInvocationPolicy', declaration: 'export interface SkillInvocationPolicy {\n readonly modelInvocable: boolean;\n readonly userInvocable: boolean;\n}', }, + { + name: 'SkillListRequest', + declaration: 'export interface SkillListRequest {\n readonly sessionId: SessionId;\n}', + }, + { + name: 'SkillListValue', + declaration: 'export interface SkillListValue {\n readonly skills: readonly SkillEntry[];\n}', + }, { name: 'SkillLookupOptions', declaration: 'export interface SkillLookupOptions {\n readonly cwd?: string | undefined;\n readonly signal?: AbortSignal | undefined;\n}', diff --git a/packages/host/apiproxy/README.i18n.yaml b/packages/host/apiproxy/README.i18n.yaml index 600040a06f..a339fcb16a 100644 --- a/packages/host/apiproxy/README.i18n.yaml +++ b/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: 6586cee0b7edeb9ba0db63e9a0a13afc7323d9a5 -README.zh.md: c8984783b1fc1dcceec9bfb3b247a8805319726f +README.md: 131ea9c510733740664ca8b46510650110bca234 +README.zh.md: 4578e89acc73f1a77648dce087da1bca6b364f16 diff --git a/packages/host/apiproxy/README.md b/packages/host/apiproxy/README.md index 6586cee0b7..131ea9c510 100644 --- a/packages/host/apiproxy/README.md +++ b/packages/host/apiproxy/README.md @@ -1,5 +1,5 @@ --- -description: "The shared API gateway for web GUI host clients: the browser-safe API contract, the fetch carriers, and the host-side gateway service every client shape uses." +description: "Legacy HTTP transport for Host bootstrap metadata and streamed Session-log ZIP downloads while generated Typert Remotes own business operations." kind: "package-reference" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Every client of the web GUI host calls one typed API through `dsh-host-apiproxy` — sessions and history, workspaces, directory picking, model selection, agent presets, skills, goals, settings, LLM catalogs, events, and session export — moved over HTTP or in-process by fetch carriers. The contract layer has zero Node dependencies and imports from the browser, so one typed API serves the Web server, Electron, and any future client shape. The shipped Web composition assembles the gateway in [`dsh-web-app`](../../bundle/web-app/README.md). Choosing a carrier, calling the domain APIs, and configuring the gateway come first; the wire protocol internals live in a collapsible developer section below. +`dsh-host-apiproxy` carries the two Host operations that do not yet belong to a generated business Remote: the `host.describe` bootstrap snapshot and streamed Session-log ZIP downloads. Its browser-safe envelope and fetch adapters serve HTTP and in-process clients, while API Gateway carries all ordinary business operations. The shipped Web composition assembles both transports in [`dsh-web-app`](../../bundle/web-app/README.md). ## Table of Contents @@ -25,7 +25,7 @@ Every client of the web GUI host calls one typed API through `dsh-host-apiproxy` ## Use this package -Compose the gateway when a client of the GUI host needs the session, workspace, and configuration APIs: load `ApiProxyService`, wrap `ctx.apiProxy` in a carrier, and call the typed domain methods. +Compose this package when a GUI host needs bootstrap metadata and Session-log export: load `ApiProxyService`, wrap `ctx.apiProxy` in a carrier, and use generated Remotes for all other business calls. ### Choosing a carrier @@ -33,31 +33,19 @@ Compose the gateway when a client of the GUI host needs the session, workspace, ```text const client = new InProcessApiClient(toFetchHandler(ctx.apiProxy)) -const response = await client.sessions.list({}) +const response = await client.host.describe({}) ``` The HTTP carrier refuses non-JSON POST bodies with 415 before dispatch, so cross-site simple requests can never run a side-effectful method blind. The browser carrier applies the same Host/Origin checks and signed-cookie authentication to every Host API method ([`dsh-client-connection`](../../client/connection/README.md)); individual Client features may still withhold native or persistent operations on non-loopback pages. ### 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, openPath), `skills`, `agentPresets`, `goals`, `settings`, `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 - -`session.history` pages a session's appended message stream (`maxMessages` counts append-origin `user/message` and `assistant/message` events, so model-only replacement copies consume no quota) and keeps each page a contiguous raw event range, which keeps a compaction's log-only summary on the same page as the replacement that cites it. The tail page optionally carries a `projections` block — the watermark snapshot of every registered projection unit — while the gateway pushes live `session/projection` frames for units whose state changed. `session.search` is a bounded content-search projection over the sessions visible through `session.list`: at most 20 hits, snippets of at most 240 code points, and every hit revalidated against the visible set. - -### Workspaces and the session list - -`session.list` and `workspace.list` are separate reconnect baselines. Blank sessions stay hidden until the first turn, archiving hides a session from grouping surfaces without touching its log, and registration deletion preserves the directory and session logs. Cold summaries verify blankness by probing a small eligible artifact; a projection-cache miss or stale hint falls back to `createdAt`, so a recently worked large session may sort lower until the next checkpoint. +The unary map contains only `host.describe`; the direct download route is `GET` or `HEAD /api/session.export`. Session, workspace, settings, credentials, LLM, skill, file-reference, command, and interaction operations are generated Remotes owned by their business packages and assembled by [`dsh-api-remotes`](../../api/remotes/README.md). ### Exporting sessions `GET /api/session.export?sessionId=…&includeDescendants=true` streams a ZIP of the session's stored artifact text verbatim, every subagent descendant under `subagents//`, and each referenced image under `media/.`. `HEAD` runs the same root preparation without a body, so browsers detect pre-stream failures before handing the GET to the download manager. The response is chunked as it is produced, and `sessionExportCompressionLevel` (0–9, default 6) trades CPU and latency against archive size. Missing persistence, session-query, or attachment services answer 500, a backend without per-session raw artifacts 501, and a missing root session 404. -### Model selection, presets, commands, and configuration - -`session.models` reports the current `ModelSelection` separately from provider-grouped advisory models, and `session.selectModel` saves an accepted switch as the deployment default through the shared `agent-default-model` settings section — a default naming an unavailable provider still reaches the selector as `current` instead of being silently replaced. Each access resolves an in-process selection first, then the session's latest `request/header`, then the deployment default. A logged reasoning effort marked as an adapter default remains absent from the restored selection, so the next resolution does not promote it into an explicit choice or record a false header change. `agentPreset.list` exposes the deployment's preset roster with each row's `trust` and a `broken` reason when a preset cannot compose a session; `agentPreset.select` swaps a blank session's composition and is refused once a turn has run. `skill.list` serves the composer menu with each skill's `modelInvocable` flag, and `command.execute` runs a slash command with pure admission semantics whose outcome rides the logged `command/run`/`command/done` pair. The remaining configuration operations are `settings.openDocument` and the `llm.*` domain; generated settings and credential methods belong to [`@deepseek-ai/dsh-api-settings-controller`](../../api/settings-controller/README.md). - ### Configuration | Field | Default | Meaning | @@ -88,19 +76,18 @@ The package is built on one separation: the API contract is channel-independent, | [`src/fetch/client.ts`](src/fetch/client.ts) | Client carrier: `AbstractApiClient` plus platform subclasses, `InProcessApiClient` | | [`src/api-proxy.ts`](src/api-proxy.ts) | Gateway implementation: `createApiProxy` over the composed host context | | [`src/session-export.ts`](src/session-export.ts) | Session-log ZIP export: raw artifact reads, media collection, fflate streaming | -| [`src/native-path-opener.ts`](src/native-path-opener.ts) | Platform opener for paths (`open`/`Invoke-Item`/`xdg-open`, WSL translation) | ### The gateway service -`ApiProxyService` provides `ctx.apiProxy` and implements the contract over the composed host context — sessions, workspace registry, directory picker, agent presets, settings, LLM, events, and downloads. The Host cwd is the default project directory. The gateway consumes `ctx.agentDefaultModel` only for the deployment metadata `host.describe` reports; `session.selectModel` (Session Controller) saves an accepted switch as the deployment default through the shared agent-default-model settings section. Product `dsh --profile headless` is a direct core entry point and does not mount this package. +`ApiProxyService` provides `ctx.apiProxy`, reports process metadata through `host.describe`, and delegates Session archive production to the persistence, query, attachment, and live Session services. The Host cwd is the default project directory. Product `dsh --profile headless` is a direct core entry point and does not mount this package. ### Request flow -A request enters a carrier, which parses the envelope and the business payload in two levels, dispatches per method, and returns a response echoing the request's `rpcId`. Server pushes — the session and workspace follow streams — ride the API Gateway's `/api/remote.mux` WebSocket and deliver `opened` then gap-free `event` frames the client decodes. Unary requests carry the carrier's abort signal, so caller/connection cancellation propagates to the underlying work. +A `host.describe` request enters the fetch carrier, which parses the envelope and payload, dispatches the method, and returns a response echoing the request's `rpcId`. Session export bypasses that envelope because its streamed ZIP body and HTTP status are the result. ### What the gateway owns -The gateway is the wire contract plus a host-side projection over services owned elsewhere: it emits no cordis events, and the session/agent event streams it projects are asserted by their owning packages' companions. The carrier holds no other domain's knowledge — each projection value already passed its unit's own schema inside the registry. +The package owns its legacy envelope, Host bootstrap snapshot, and archive download. API Gateway owns generated Remote dispatch and streams; business packages own their methods and result types. @@ -135,12 +122,7 @@ None; this package neither assembles nor sends a provider request. These limits define where the gateway is a poor fit; they are current package constraints, not a task backlog. -- **Forwarded Remote events ride the gateway stream framing** — the delivery path reuses the API Gateway's Remote stream mux instead of opening a third downlink, which reads as if this package owned the Remote event contract. It does not: the allowlist belongs to `dsh-api-remotes` and the consumer verb is `ctx.remote.$on` ([rationale](../../../.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md)). -- **Pending-interaction state is host-side** — the browser's pending-interaction snapshot folds plugin-registered pending domains (user questions and approvals); the wire defines no dedicated respond route and no `RpcReceipt` type. -- **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. -- **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). ### Dev Note diff --git a/packages/host/apiproxy/README.zh.md b/packages/host/apiproxy/README.zh.md index c8984783b1..4578e89acc 100644 --- a/packages/host/apiproxy/README.zh.md +++ b/packages/host/apiproxy/README.zh.md @@ -1,5 +1,5 @@ --- -description: "web GUI 宿主客户端共享的 API 网关:浏览器安全的 API 约定、fetch 载体,以及每种客户端形态都使用的宿主侧网关服务。" +description: "Host 启动元数据与 Session 日志 ZIP 流下载的旧版 HTTP 载体;普通业务操作由生成的 Typert Remote 持有。" kind: "package-reference" --- @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -web GUI 宿主的每个客户端都通过 `dsh-host-apiproxy` 调用同一套类型化 API——会话与历史、工作区、目录选择、模型选择、agent preset、skill、目标、设置、LLM 目录、事件与会话导出——由 fetch 载体经由 HTTP 或进程内搬运。约定层零 Node 依赖、可从浏览器导入,因此一套类型化 API 同时服务 Web 服务器、Electron 与任何未来的客户端形态。随发行版交付的 Web 组合在 [`dsh-web-app`](../../bundle/web-app/README.zh.md) 中组装网关。选择载体、调用领域 API 与配置网关在前;协议内部细节放在下方可折叠的开发者章节中。 +`dsh-host-apiproxy` 承载尚不属于生成业务 Remote 的两项 Host 操作:`host.describe` 启动快照与流式 Session 日志 ZIP 下载。它的浏览器安全 envelope 与 fetch adapter 服务 HTTP 和进程内客户端,其余普通业务操作由 API Gateway 承载。随发行版交付的 Web 组合在 [`dsh-web-app`](../../bundle/web-app/README.zh.md) 中组装两种传输。 ## 目录 @@ -25,7 +25,7 @@ web GUI 宿主的每个客户端都通过 `dsh-host-apiproxy` 调用同一套类 ## 使用本包 -当 GUI 宿主的客户端需要会话、工作区与配置 API 时组合网关:加载 `ApiProxyService`,把 `ctx.apiProxy` 包进一个载体,然后调用类型化的领域方法。 +当 GUI Host 需要启动元数据与 Session 日志导出时组合本包:加载 `ApiProxyService`,把 `ctx.apiProxy` 包进一个载体,其他业务调用使用生成的 Remote。 ### 选择载体 @@ -33,31 +33,19 @@ web GUI 宿主的每个客户端都通过 `dsh-host-apiproxy` 调用同一套类 ```text const client = new InProcessApiClient(toFetchHandler(ctx.apiProxy)) -const response = await client.sessions.list({}) +const response = await client.host.describe({}) ``` HTTP 载体在分发前以 415 拒绝非 JSON 的 POST 请求体,因此跨站「简单请求」永远无法盲目执行有副作用的方法。浏览器载体对每个 Host API 方法实施相同的 Host/Origin 检查与签名 cookie 认证([`dsh-client-connection`](../../client/connection/README.zh.md));各 Client 功能仍可以在非 loopback 页面上拒绝原生操作或持久化操作。 ### 网关暴露什么 -API 按领域分组:`sessions`(list、create、history、prompt、cancel、queue、models、selectModel、rename、fork、search、attachment)、`workspace`、`host`(describe、openPath)、`skills`、`agentPresets`、`goals`、`settings`、`llm`、`events` 与 `downloads`。sessions、workspace 与 events 契约分别归 Session Controller、Workspace Controller 与 API Remotes 包所有;其余领域契约与 `RpcMethodMap` 位于 `src/api/`。 - -### 会话与历史 - -`session.history` 对会话的追加消息流分页(`maxMessages` 统计以追加方式进入 surface 的 `user/message` 与 `assistant/message` 事件,因此仅供模型使用的替换副本不占用配额),并让每一页保持一段连续的原始事件区间,这使压缩的仅日志摘要与引用它的替换留在同一页。尾页可选携带 `projections` 块——每个已注册投影单元的水位线快照——网关会为状态发生变化的单元推送实时的 `session/projection` 帧。`session.search` 是对 `session.list` 可见会话的有界内容搜索投影:至多 20 个命中、每个摘要至多 240 个码点,且每个命中都对照可见集合重新校验。 - -### 工作区与会话列表 - -`session.list` 与 `workspace.list` 是彼此独立的重连基线。空白会话在首轮开始前保持隐藏,归档会把会话从分组表面隐藏而不触碰其日志,注销注册则保留目录与会话日志。冷摘要通过探测一个小型合格工件来验证空白状态;projection cache miss 或陈旧提示会回退到 `createdAt`,因此最近工作过的大型会话可能在下一个 checkpoint 前排得偏低。 +一元映射只包含 `host.describe`;直接下载路由是 `GET` 或 `HEAD /api/session.export`。Session、workspace、settings、credentials、LLM、skill、file-reference、command 与 interaction 操作都是由各业务包持有、并由 [`dsh-api-remotes`](../../api/remotes/README.zh.md) 组装的生成 Remote。 ### 导出会话 `GET /api/session.export?sessionId=…&includeDescendants=true` 流式输出一个 ZIP,其中每个会话的已存工件文本原样包含,每个子代理后代位于 `subagents//` 下,每张被引用的图片位于 `media/.` 下。`HEAD` 在无请求体的情况下运行同样的根准备,因此浏览器能在把 GET 交给下载管理器之前检测到流前失败。响应边生成边分块输出,`sessionExportCompressionLevel`(0–9,默认 6)在 CPU 与延迟之间权衡归档大小。缺少 persistence、session-query 或 attachment 服务时回答 500,后端没有按会话原始工件时回答 501,根会话缺失时回答 404。 -### 模型选择、preset、命令与配置 - -`session.models` 把当前 `ModelSelection` 与按提供方分组的咨询模型分开报告,`session.selectModel` 通过共享的 `agent-default-model` settings 分节把已接受的切换保存为部署默认值——指向不可用提供方的默认值仍会作为 `current` 送到选择器,而不是被静默替换。每次访问都先解析进程内选择,再读会话最新的 `request/header`,最后使用部署默认值。日志中标记为适配器默认值的推理强度不会进入恢复后的选择,因此下一次解析不会把它提升为显式选择,也不会记录虚假 header 变更。`agentPreset.list` 暴露部署的 preset 名单,每行带 `trust`,preset 无法组合会话时带 `broken` 原因;`agentPreset.select` 替换空白会话的组合,一旦跑过一轮即被拒绝。`skill.list` 为 composer 菜单提供每个 skill 的 `modelInvocable` 标志,`command.execute` 以纯准入语义运行斜杠命令,其结局由落账的 `command/run`/`command/done` 事件对承载。剩余配置操作是 `settings.openDocument` 与 `llm.*` 领域;生成的 settings 与凭据方法归 [`@deepseek-ai/dsh-api-settings-controller`](../../api/settings-controller/README.zh.md) 所有。 - ### 配置 | 字段 | 默认值 | 含义 | @@ -88,19 +76,18 @@ API 按领域分组:`sessions`(list、create、history、prompt、cancel、q | [`src/fetch/client.ts`](src/fetch/client.ts) | 客户端载体:`AbstractApiClient` 及平台子类、`InProcessApiClient` | | [`src/api-proxy.ts`](src/api-proxy.ts) | 网关实现:基于所组合宿主上下文的 `createApiProxy` | | [`src/session-export.ts`](src/session-export.ts) | 会话日志 ZIP 导出:原始工件读取、媒体收集、fflate 流式输出 | -| [`src/native-path-opener.ts`](src/native-path-opener.ts) | 平台路径打开器(`open`/`Invoke-Item`/`xdg-open`、WSL 转换) | ### 网关服务 -`ApiProxyService` 提供 `ctx.apiProxy`,并基于所组合的宿主上下文实现约定——会话、工作区注册表、目录选择器、agent preset、设置、LLM、事件与下载。Host cwd 是默认项目目录。网关只在 `host.describe` 报告的部署元数据中消费 `ctx.agentDefaultModel`;保存已接受的切换由 Session Controller 的 `session.selectModel` 通过共享的 agent-default-model settings 分节完成。产品的 `dsh --profile headless` 是直连 core 的入口,不挂载本包。 +`ApiProxyService` 提供 `ctx.apiProxy`,通过 `host.describe` 报告进程元数据,并把 Session 归档生成委派给 persistence、query、attachment 与 live Session 服务。Host cwd 是默认项目目录。产品的 `dsh --profile headless` 是直连 core 的入口,不挂载本包。 ### 请求流 -请求进入载体,载体分两层解析信封与业务载荷、按方法分发,并返回回显请求 `rpcId` 的响应。服务器推送——会话与工作区 follow 流——搭乘 API Gateway 的 `/api/remote.mux` WebSocket,投递 `opened` 及之后无间隙的 `event` 帧,由客户端解码。一元请求携带载体的中止信号,因此调用方/连接的取消会传播到底层工作。 +`host.describe` 请求进入 fetch 载体,载体解析 envelope 与 payload、分发方法,并返回回显请求 `rpcId` 的响应。Session 导出不使用该 envelope,因为其流式 ZIP body 与 HTTP 状态就是结果。 ### 网关拥有什么 -网关是协议约定外加一层对别处所拥有服务的宿主侧投影:它不发出任何 cordis 事件,它所投影的会话/agent 事件流由各自所属包的伴生插件断言。载体不持有其他领域的知识——每个投影值在注册表内部已经过其单元自己的 schema。 +本包持有旧版 envelope、Host 启动快照与归档下载。API Gateway 持有生成的 Remote 分发与流;业务包持有各自的方法和结果类型。 @@ -135,12 +122,7 @@ API 按领域分组:`sessions`(list、create、history、prompt、cancel、q 这些限制说明网关在何处不合适;它们是当前包约束,不是任务积压。 -- **转发的 Remote 事件搭乘网关流帧封装**——投递路径复用 API Gateway 的 Remote 流 mux、不必新开第三条下行通道,因此读起来像是本包拥有 Remote 事件契约。并非如此:名单归 `dsh-api-remotes`,消费端动词是 `ctx.remote.$on`([原委](../../../.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.zh.md))。 -- **待处理交互状态位于宿主侧**——浏览器的待处理交互快照由插件注册的待处理领域(用户提问与审批)折叠而成;wire 未定义专门的 respond 路由,也没有 `RpcReceipt` 类型。 -- **预留 seam 不进入 `RpcMethodMap`**——`prompt.mode: 'inject'`、`job.list` 和描述字段 `hostInstanceId` 都是已记录的预留项;模型发现使用 `llm.models`。未知方法会在信封解析时直接失败,而不会返回「尚未实现」错误码。 - **没有协议版本字段**——客户端与宿主一同发布;只有出现独立发布的客户端后,`host.describe` 才会增加版本协商字段。 -- **搜索失败会包含提供方诊断信息**——网关是单用户本地服务;将其暴露给多名用户的载体必须用可安全公开的诊断信息替代内部搜索细节。 -- **冷列表提示只向“保持可见、排序偏旧”降级**——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)的范围。 ### 开发备注 diff --git a/packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts b/packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts deleted file mode 100644 index 76643472e0..0000000000 --- a/packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts +++ /dev/null @@ -1,375 +0,0 @@ -/** API Proxy behavior for Agent preset management and preset-scoped catalogs. */ - -import { mkdtempSync, realpathSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { Context } from '@deepseek-ai/cordis' -import AgentRegistry, { type AgentFactory } from '@deepseek-ai/dsh-agent' -import type { Agent } from '@deepseek-ai/dsh-agent' -import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session' -import { RpcId, type RpcRequest } from '../src/api/rpc.ts' -import type { ApiProxy } from '../src/api/index.ts' -import { - agentPresetProjectionDefinition, InvalidPresetIdError, PresetExistsError, UnknownPresetError, -} from '@deepseek-ai/dsh-agent-presets' -import type {} from '@deepseek-ai/dsh-agent-presets/types' -import { createApiProxy } from '../src/api-proxy.ts' -import { describe, expect, it } from 'vitest' -import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query' - -let nextRpc = 0 -function request

(payload: P): RpcRequest

{ - return { rpcId: RpcId(`preset-${String(nextRpc++)}`), payload } -} - -const sessionHarnesses = new WeakMap() - -async function createSession( - api: ApiProxy, - request: { readonly sessionId: SessionId; readonly agentPreset?: string }, -): Promise { - const harness = sessionHarnesses.get(api) - if (harness === undefined) throw new Error('Session test harness is not installed') - const presets = harness.ctx.get('agentPresets') - const agentPreset = presets === undefined - ? undefined - : (await presets.resolve(request.agentPreset)).id - await harness.ctx.agents.create({ - sessionId: request.sessionId, - meta: { - cwd: harness.cwd, - ...(agentPreset === undefined ? {} : { agentPreset }), - }, - ...(agentPreset === undefined || presets === undefined - ? {} - : { setup: async (agentCtx: Context) => { await presets.mount(agentCtx, agentPreset) } }), - }) -} - -/** Minimal live agent; the gateway only needs identity and its session. */ -function stubAgent(session: Session): Agent { - return { id: session.id, session, status: 'idle' } as unknown as Agent -} - -/** - * A roster whose `mount` is a no-op: this spec is about the gateway's identity - * rules, and the composition itself is covered by the real-composition test in - * `apps/cli`. Ids listed in `userIds` present as locally authored; the rest - * ship with the deployment. - */ -function roster(ids: readonly string[], userIds: readonly string[] = []): unknown { - const trustOf = (id: string): 'system' | 'user' => (userIds.includes(id) ? 'user' : 'system') - const presetOf = (id: string): object => - ({ id, trust: trustOf(id), path: `/presets/${id}/agent.cordis.yml` }) - return { - defaultId: ids[0], - list: () => Promise.resolve(ids.map(presetOf)), - resolve: (id?: string) => { - const wanted = id ?? ids[0] ?? '' - if (!ids.includes(wanted)) return Promise.reject(new UnknownPresetError(wanted, ids)) - return Promise.resolve(presetOf(wanted)) - }, - mount: (_ctx: Context, id?: string) => Promise.resolve(presetOf(id ?? ids[0] ?? '')), - // What a real mount leaves behind: a service instance only the agent that - // mounted it can be used to address. The doubles are per agent so a test - // can tell "this session's" from "some session's". - serviceFor: (agent: { id: unknown }, name: string) => { - const perAgent = services.get(String(agent.id)) - return perAgent?.[name] - }, - authorable: true, - read: (id: string) => Promise.resolve(`# ${id}\n- id: x\n name: y\n`), - copy: (from: string, id: string) => { - if (!ids.includes(from)) return Promise.reject(new UnknownPresetError(from, ids)) - if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) return Promise.reject(new InvalidPresetIdError(id)) - if (ids.includes(id)) return Promise.reject(new PresetExistsError(id)) - return Promise.resolve() - }, - remove: (id: string) => { - if (!ids.includes(id)) return Promise.reject(new UnknownPresetError(id, ids)) - return Promise.resolve() - }, - recompose: (_ctx: Context, id: string) => { - if (!ids.includes(id)) return Promise.reject(new UnknownPresetError(id, ids)) - return Promise.resolve({ id, trust: 'system', path: `/presets/${id}.yml` }) - }, - // The standing scope key a cold transcript read resolves presenters in. - standingKeyFor: (id?: string) => { - const wanted = id ?? ids[0] ?? '' - if (!ids.includes(wanted)) return Promise.reject(new UnknownPresetError(wanted, ids)) - let key = standingKeys.get(wanted) - if (key === undefined) { - key = { agentPreset: wanted } - standingKeys.set(wanted, key) - } - return Promise.resolve(key) - }, - } -} - -/** Standing keys minted by the roster double. */ -const standingKeys = new Map() - -/** Per-agent service instances a mounted preset would own, keyed by session id. */ -const services = new Map>() - -async function harness( - presets?: readonly string[], - options: { userIds?: readonly string[]; defaults?: Record } = {}, -) { - const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-apiproxy-preset-'))) - const ctx = new Context() - await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) - if (presets !== undefined) ctx.provide('agentPresets', roster(presets, options.userIds) as never) - ctx.provide('sessionQuery', { - observeSession: (sessionId: SessionId) => { - const session = ctx.sessions.get(sessionId) - if (session === undefined) { - return Promise.reject(new SessionQueryError( - `session "${sessionId}" not found`, - 'SESSION_QUERY_SESSION_NOT_FOUND', - )) - } - let preset = agentPresetProjectionDefinition.init(session.header) - for (const event of session.events) { - preset = agentPresetProjectionDefinition.apply(preset, event) - } - const events = Object.freeze([...session.events]) - const lease = (): SessionObservation => ({ - source: 'live' as const, - header: session.header, - events, - cursor: events.at(-1)?.seq ?? -1, - projections: { - asOfSeq: events.at(-1)?.seq ?? -1, - values: { agentPreset: preset }, - }, - retain: lease, - [Symbol.dispose]: () => {}, - }) - return Promise.resolve(lease()) - }, - } as never) - - const factory: AgentFactory = { - async createAgent(_ownerCtx, options) { - const session = ctx.sessions.create( - options.sessionId, - options.meta === undefined ? {} : { meta: options.meta }, - ) - const agent = stubAgent(session) - // Setup runs before publication against a context that carries the - // agent, and the agent reaches back through `agent.ctx` — the pair the - // gateway's own `installTarget` relies on. - const agentCtx = ctx.extend({ agent }) - ;(agent as { ctx?: Context }).ctx = agentCtx - await options.setup?.(agentCtx) - const unregister = ctx.agents.register(agent) - return { agent, dispose: () => { unregister(); return Promise.resolve() } } - }, - async resume() { - throw new Error('test harness has no persisted sessions') - }, - } - ctx.agents.setFactory(factory) - ctx.provide('sessionController', { - resolveAgent: (sessionId: SessionId) => { - const agent = ctx.agents.get(sessionId) - return Promise.resolve(agent === undefined - ? { - error: { - code: 'session-not-found', - message: `session "${sessionId}" not found`, - details: { sessionId }, - }, - } - : { agent }) - }, - inspect: (sessionId: SessionId) => { - const session = ctx.sessions.get(sessionId) - if (session === undefined) throw new Error(`session "${sessionId}" not found`) - return Promise.resolve({ meta: session.header, events: [...session.events] }) - }, - } as never) - const defaults = { - defaultModelSelection: () => ({ provider: 'test', model: 'test-model' }), - cwd, - ...options.defaults, - } - const api = createApiProxy(ctx, defaults) - sessionHarnesses.set(api, { ctx, cwd }) - return { api, ctx, cwd } -} - -/** - * A capability a preset mounts is reachable from nowhere the host normally - * looks: an `isolate` realm is what makes it per session. The gateway serves - * requests that are ABOUT a session from OUTSIDE it, so it addresses the - * instance through the agent instead of reading a root-realm singleton. - */ -describe('a capability the session\'s preset mounts', () => { - it('serves the skill catalog from the session\'s own registry', async () => { - const { api } = await harness(['standard']) - await createSession(api, { sessionId: SessionId('k1'), agentPreset: 'standard' }) - services.set('k1', { - skills: { - list: () => Promise.resolve([{ - name: 'preset-owned', - description: 'ships inside the preset directory', - invocation: { modelInvocable: true, userInvocable: true }, - }]), - }, - }) - - const response = await api.skills.list(request({ sessionId: SessionId('k1') })) - - // A preset ships its own skill directory, so the catalog IS the - // session's; reading a host singleton would answer for the wrong one. - expect(response.result).toMatchObject({ ok: true, value: { skills: [{ name: 'preset-owned' }] } }) - services.delete('k1') - }) - - it('says so when no composition mounts the capability at all', async () => { - const { api } = await harness(['standard']) - await createSession(api, { sessionId: SessionId('n1'), agentPreset: 'standard' }) - - const response = await api.skills.list(request({ sessionId: SessionId('n1') })) - - // Absent means absent — not "this session has none", which is what a - // root-realm read used to report for every presetd session. - expect(response.result.ok).toBe(false) - const failure = response.result as { ok: false; error: { message: string } } - expect(failure.error.message).toContain('neither this session') - }) -}) - -describe('opening a preset directory', () => { - it('hands the resolved directory to the native opener', async () => { - const opened: string[] = [] - const { api } = await harness(['standard', 'my-preset'], { - userIds: ['my-preset'], - defaults: { openPath: (path: string) => { opened.push(path); return Promise.resolve() } }, - }) - - const response = await api.agentPresets.openDocument( - request({ agentPreset: 'my-preset' }), new AbortController().signal) - - expect(response.result.ok).toBe(true) - if (!response.result.ok) throw new Error('unreachable') - expect(response.result.value).toEqual({ opened: true }) - // The id selected the directory; the browser supplied no path. - expect(opened).toEqual(['/presets/my-preset']) - }) - - it('answers the path as text where the deployment has no opener', async () => { - const { api } = await harness(['standard', 'my-preset'], { - userIds: ['my-preset'], - defaults: { canOpenPath: () => false }, - }) - - const response = await api.agentPresets.openDocument( - request({ agentPreset: 'my-preset' }), new AbortController().signal) - - expect(response.result.ok).toBe(true) - if (!response.result.ok) throw new Error('unreachable') - expect(response.result.value).toEqual({ opened: false, path: '/presets/my-preset' }) - }) - - it('refuses a preset that ships with the deployment', async () => { - const opened: string[] = [] - const { api } = await harness(['standard'], { - defaults: { openPath: (path: string) => { opened.push(path); return Promise.resolve() } }, - }) - - const response = await api.agentPresets.openDocument( - request({ agentPreset: 'standard' }), new AbortController().signal) - - // Pointing an editor into the install invites edits an upgrade will - // silently overwrite; the refusal mirrors copy/remove. - expect(response.result.ok).toBe(false) - if (response.result.ok) throw new Error('unreachable') - expect(response.result.error.code).toBe('agent-preset-read-only') - expect(opened).toEqual([]) - }) - - it('reports the opener capability on host.describe', async () => { - const openable = await harness(['standard'], { - defaults: { canOpenPath: () => true }, - }) - const headless = await harness(['standard'], { - defaults: { canOpenPath: () => false }, - }) - - // The capability a surface joins onto the roster to decide between opening - // a preset directory and showing its path as text. - const yes = await openable.api.host.describe(request({})) - const no = await headless.api.host.describe(request({})) - - expect(yes.result.ok && yes.result.value.canOpenPath).toBe(true) - expect(no.result.ok && no.result.value.canOpenPath).toBe(false) - }) - - it('counts an injected opener as openable', async () => { - const { api } = await harness(['standard'], { - defaults: { openPath: () => Promise.resolve() }, - }) - - const response = await api.host.describe(request({})) - - expect(response.result.ok && response.result.value.canOpenPath).toBe(true) - }) -}) - -describe('skills over the layered host registry', () => { - it('passes the live agent as the view scope to the host registry', async () => { - const { api, ctx } = await harness(['standard']) - const seen: unknown[] = [] - ctx.provide('skills', { - list: (options: { scope?: unknown }) => { - seen.push(options.scope) - return Promise.resolve([]) - }, - } as never) - await createSession(api, { sessionId: SessionId('h1'), agentPreset: 'standard' }) - - const response = await api.skills.list(request({ sessionId: SessionId('h1') })) - - expect(response.result).toMatchObject({ ok: true, value: { skills: [] } }) - expect(seen).toEqual([ctx.agents.get(SessionId('h1'))]) - }) - - it('resolves a cold session to its recorded preset standing key', async () => { - const { api, ctx } = await harness(['standard', 'minimal']) - const seen: unknown[] = [] - ctx.provide('skills', { - list: (options: { scope?: unknown }) => { - seen.push(options.scope) - return Promise.resolve([]) - }, - } as never) - ctx.sessions.create(SessionId('h2'), { meta: { cwd: '/workspace/cold', agentPreset: 'minimal' } }) - - const response = await api.skills.list(request({ sessionId: SessionId('h2') })) - - expect(response.result).toMatchObject({ ok: true, value: { skills: [] } }) - expect(seen).toEqual([standingKeys.get('minimal')]) - }) - - it('serves the global view when the roster no longer supplies the recorded preset', async () => { - const { api, ctx } = await harness(['standard']) - const seen: unknown[] = [] - ctx.provide('skills', { - list: (options: { scope?: unknown }) => { - seen.push(options.scope) - return Promise.resolve([]) - }, - } as never) - ctx.sessions.create(SessionId('h3'), { meta: { cwd: '/workspace/cold', agentPreset: 'gone' } }) - - const response = await api.skills.list(request({ sessionId: SessionId('h3') })) - - expect(response.result).toMatchObject({ ok: true, value: { skills: [] } }) - expect(seen).toEqual([undefined]) - }) -}) diff --git a/packages/host/apiproxy/tests/api-proxy-config.spec.ts b/packages/host/apiproxy/tests/api-proxy-config.spec.ts index 8152fa262e..f6ed7916b2 100644 --- a/packages/host/apiproxy/tests/api-proxy-config.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-config.spec.ts @@ -1,18 +1,14 @@ /** - * Settings and llm RPC domains and their owner events over createApiProxy: - * layered redacted describe, write-path rejection mapping, the - * directory/live-route merge, and the settings and model invalidation frames. + * Settings events consumed by Client model and permission surfaces. */ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import AgentRegistry from '@deepseek-ai/dsh-agent' import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' -import LlmRuntime, { LlmAdapter } from '@deepseek-ai/dsh-llm' -import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, StreamChunk } from '@deepseek-ai/dsh-llm' import { SettingsProvider, settingsNamespace } from '@deepseek-ai/dsh-settings' import type { SettingsNamespace } from '@deepseek-ai/dsh-settings' import { CredentialProvider } from '@deepseek-ai/dsh-credentials' @@ -25,29 +21,7 @@ import type { CredentialRef, ResolvedCredential, } from '@deepseek-ai/dsh-credentials' -import type { RpcRequest, RpcResponse } from '../src/api/rpc.ts' -import { RpcId } from '../src/api/rpc.ts' import { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-default-model' -import { createApiProxy } from '../src/api-proxy.ts' - -const DEFAULTS = { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' } - -let nextRpc = 1 -function request

(payload: P): RpcRequest

{ - return { rpcId: RpcId(`req-${String(nextRpc++)}`), payload } -} - -function expectOk(response: RpcResponse): T { - expect(response.result.ok).toBe(true) - if (!response.result.ok) throw new Error('unreachable') - return response.result.value -} - -function expectErr(response: RpcResponse): { code: string; message: string; details: unknown } { - expect(response.result.ok).toBe(false) - if (response.result.ok) throw new Error('unreachable') - return response.result.error -} /** In-memory settings provider: the Service Definition base class owns all tested behavior. */ class MemorySettings extends SettingsProvider { @@ -159,32 +133,6 @@ class MemoryCredentials extends CredentialProvider { } } -/** Catalog-serving adapter stub for the llm.models path. */ -class CatalogAdapter extends LlmAdapter { - constructor(private readonly name: string, private readonly models: readonly string[]) { - super() - } - - override providerInfo(provider: string): LlmProviderInfo { - return { id: provider, name: this.name } - } - - override listModels(provider: string): Promise { - return Promise.resolve(this.models.map(id => ({ provider, id, name: id }))) - } - - - async * stream(_options: GenerateOptions): AsyncIterable { - throw new Error('not exercised') - } -} - -class BrokenCatalogAdapter extends CatalogAdapter { - override listModels(): Promise { - return Promise.reject(new Error('catalog backend down')) - } -} - const NS = settingsNamespace('llm-deepseek') const AdapterConfig = z.object({ @@ -201,24 +149,14 @@ async function harness(options?: { preparedPath?: string } credentials?: false | { shadowed?: string[] } - /** Skip the directory registration to exercise a namespace the proxy does not expose. */ - configurableProviders?: false }): Promise { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt, { persona: '' }) await ctx.plugin(ToolRuntime) await ctx.plugin(AgentRegistry) - await ctx.plugin(LlmRuntime) if (options?.settings !== false) await ctx.plugin(MemorySettings, options?.settings) if (options?.credentials !== false) await ctx.plugin(MemoryCredentials, options?.credentials) - // Model-provider namespaces plus the explicit Web preference and product - // onboarding allowlists are the proxy's complete settings surface. - if (options?.configurableProviders !== false) { - ctx.llm.registerConfigurableProviders([ - { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] }, - ]) - } return ctx } @@ -239,93 +177,12 @@ async function captureSettingsUpdates( } } -/** Count model-adapter topology commits while one API operation runs. */ -async function countAdapterUpdates(ctx: Context, run: () => Promise): Promise { - let updates = 0 - const dispose = ctx.on('llm/adapters-updated', () => { updates += 1 }) - try { - await run() - return updates - } finally { - dispose() - } -} - /** Expected settings event tuple with its owner-assigned revision. */ function expectedSettingsUpdate(ns: string): readonly unknown[] { return [ns, expect.any(Number)] } -describe('settings domain', () => { - it('reports an actionable error when no settings provider is mounted', async () => { - const ctx = await harness({ settings: false }) - const api = createApiProxy(ctx, DEFAULTS) - const error = expectErr(await api.settings.openDocument(request({}), new AbortController().signal)) - expect(error.code).toBe('internal') - expect(error.message).toContain('dsh-settings-file') - }) - - - it('opens the provider-resolved document without accepting a browser path', async () => { - const ctx = await harness({ settings: { - documentPath: '/tmp/described-settings.yaml', - preparedPath: '/tmp/custom-settings.yaml', - } }) - const opened: string[] = [] - const api = createApiProxy(ctx, { - ...DEFAULTS, - openTextFile: (path) => { - opened.push(path) - return Promise.resolve() - }, - }) - - expect(expectOk(await api.settings.openDocument(request({}), new AbortController().signal))) - .toEqual({ opened: true }) - expect(opened).toEqual(['/tmp/custom-settings.yaml']) - }) - - it('refuses to open settings when the provider has no local document', async () => { - const ctx = await harness() - const api = createApiProxy(ctx, DEFAULTS) - expect(ctx.settings.documentPath).toBeUndefined() - const error = expectErr(await api.settings.openDocument(request({}), new AbortController().signal)) - expect(error.code).toBe('internal') - expect(error.message).toContain('no local document') - }) - - it('does not prepare or open a settings document after cancellation', async () => { - const ctx = await harness({ settings: { documentPath: '/tmp/settings.yaml' } }) - const opened: string[] = [] - const api = createApiProxy(ctx, { - ...DEFAULTS, - openTextFile: (path) => { - opened.push(path) - return Promise.resolve() - }, - }) - const prepare = vi.spyOn(ctx.settings, 'prepareDocument') - const cancelled = new AbortController() - cancelled.abort() - expect(expectErr(await api.settings.openDocument(request({}), cancelled.signal)).code) - .toBe('cancelled') - expect(prepare).not.toHaveBeenCalled() - - const pending = Promise.withResolvers() - prepare.mockReturnValueOnce(pending.promise) - const duringPrepare = new AbortController() - const opening = api.settings.openDocument(request({}), duringPrepare.signal) - await vi.waitFor(() => { expect(prepare).toHaveBeenCalledOnce() }) - duringPrepare.abort() - pending.resolve('/tmp/settings.yaml') - expect(expectErr(await opening).code).toBe('cancelled') - expect(opened).toEqual([]) - }) - - - - - +describe('settings events', () => { it('forwards a provider settings change for model-catalog consumers', async () => { // Editing `models` changes no route, so llm/adapters-updated never fires // and an open model picker would keep serving the stale catalog. Storing @@ -376,161 +233,4 @@ describe('settings domain', () => { -}) - -describe('llm domain', () => { - it('merges the configurable directory with live routes and appends undeclared ones', async () => { - const ctx = await harness({ configurableProviders: false }) - ctx.llm.registerConfigurableProviders([ - { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] }, - { provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'] }, - ]) - ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', ['deepseek-v4-flash'])) - ctx.llm.registerAdapter(['undeclared'], new CatalogAdapter('Undeclared', ['u-1'])) - // Only one namespace can answer an interrogation, so the flag follows the - // entry's namespace rather than being assumed for every row. - ctx.llm.registerModelDiscovery('llm-pi-ai', () => Promise.resolve([])) - const api = createApiProxy(ctx, DEFAULTS) - const value = expectOk(await api.llm.providers(request({}))) - expect(value.providers).toEqual([ - { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true }, - { provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: false }, - // An undeclared live route has no settings address, so nothing can be - // interrogated on its behalf either. - { provider: 'undeclared', displayName: 'Undeclared', settingsNs: '', settingsPath: [], active: true }, - ]) - }) - - it('serves the host-scoped catalog with per-provider failures contained', async () => { - const ctx = await harness() - ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', ['deepseek-v4-flash', 'deepseek-v4-pro'])) - ctx.llm.registerAdapter(['broken'], new BrokenCatalogAdapter('Broken', [])) - const api = createApiProxy(ctx, DEFAULTS) - const value = expectOk(await api.llm.models(request({}))) - expect(value.default).toEqual({ provider: 'p', model: 'm' }) - expect(value.routableProviders).toEqual(['deepseek-official', 'broken']) - expect(value.groups).toEqual([{ - id: 'deepseek-official', - name: 'DeepSeek', - models: [ - { id: 'deepseek-v4-flash', name: 'deepseek-v4-flash' }, - { id: 'deepseek-v4-pro', name: 'deepseek-v4-pro' }, - ], - }]) - expect(value.failures).toEqual([{ id: 'broken', name: 'Broken', message: 'catalog backend down' }]) - }) - - it('forwards llm/adapters-updated at every topology commit point', async () => { - const ctx = await harness() - const updates = await countAdapterUpdates(ctx, async () => { - const dispose = ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', [])) - dispose() - return Promise.resolve() - }) - expect(updates).toBe(2) - }) -}) - -describe('llm.discoverModels', () => { - it('carries a draft to its namespace and returns candidates without storing anything', async () => { - const ctx = await harness() - const seen: unknown[] = [] - ctx.llm.registerModelDiscovery('llm-pi-ai', (probe) => { - seen.push({ baseURL: probe.baseURL, api: probe.api, apiKey: probe.apiKey }) - return Promise.resolve([ - { id: 'acme-large', name: 'Acme Large', contextWindow: 65_536, maxTokens: 4096 }, - { id: 'acme-small' }, - ]) - }) - const api = createApiProxy(ctx, DEFAULTS) - - const value = expectOk(await api.llm.discoverModels(request({ - settingsNs: 'llm-pi-ai', - baseURL: 'https://gateway.acme.example/v1', - api: 'openai-completions', - apiKey: 'probe-key', - }))) - - expect(value.models).toEqual([ - { id: 'acme-large', name: 'Acme Large', contextWindow: 65_536, maxTokens: 4096 }, - { id: 'acme-small' }, - ]) - expect(seen).toEqual([{ - baseURL: 'https://gateway.acme.example/v1', - api: 'openai-completions', - apiKey: 'probe-key', - }]) - // Interrogating a draft is a read: no namespace gained a section, and no - // credential reference was written. - expect(ctx.settings.describe().map(view => String(view.ns))).not.toContain('llm-pi-ai') - }) - - it('carries the route being edited so an adapter can answer from its own registry', async () => { - const ctx = await harness() - let probe: unknown - ctx.llm.registerModelDiscovery('llm-pi-ai', (request_) => { - probe = request_ - return Promise.resolve([{ id: 'from-registry', contextWindow: 65_536, maxTokens: 4096 }]) - }) - const api = createApiProxy(ctx, DEFAULTS) - - const value = expectOk(await api.llm.discoverModels(request({ - settingsNs: 'llm-pi-ai', - provider: 'deepseek', - }))) - - // No endpoint at all: a route the adapter already describes needs none. - expect(probe).toEqual({ provider: 'deepseek' }) - expect(value.models).toEqual([{ id: 'from-registry', contextWindow: 65_536, maxTokens: 4096 }]) - }) - - it('omits a credential and protocol the draft does not name', async () => { - const ctx = await harness() - let probe: unknown - ctx.llm.registerModelDiscovery('llm-pi-ai', (request_) => { - probe = request_ - return Promise.resolve([]) - }) - const api = createApiProxy(ctx, DEFAULTS) - - expectOk(await api.llm.discoverModels(request({ - settingsNs: 'llm-pi-ai', - baseURL: 'https://gateway.acme.example/v1', - }))) - - // Absent fields stay absent rather than crossing as explicit undefined: - // the adapter distinguishes "no protocol named" from "protocol undefined". - expect(probe).toEqual({ baseURL: 'https://gateway.acme.example/v1' }) - }) - - it('reports a failed interrogation as the form\'s next move, naming no credential', async () => { - const ctx = await harness() - ctx.llm.registerModelDiscovery('llm-pi-ai', () => - Promise.reject(new Error('https://gateway.acme.example/v1/models answered 401; check the API key'))) - const api = createApiProxy(ctx, DEFAULTS) - - const error = expectErr(await api.llm.discoverModels(request({ - settingsNs: 'llm-pi-ai', - baseURL: 'https://gateway.acme.example/v1', - apiKey: 'wrong', - }))) - - expect(error.code).toBe('model-discovery-failed') - expect(error.message).toContain('answered 401; check the API key') - expect(error.details).toEqual({ settingsNs: 'llm-pi-ai', baseURL: 'https://gateway.acme.example/v1' }) - expect(JSON.stringify(error)).not.toContain('wrong') - }) - - it('reports a namespace no adapter family serves', async () => { - const ctx = await harness() - const api = createApiProxy(ctx, DEFAULTS) - - const error = expectErr(await api.llm.discoverModels(request({ - settingsNs: 'llm-deepseek', - baseURL: 'https://api.deepseek.com', - }))) - - expect(error.code).toBe('model-discovery-failed') - expect(error.message).toContain('no model discovery is registered') - }) }) diff --git a/packages/host/apiproxy/tests/api-proxy-host.spec.ts b/packages/host/apiproxy/tests/api-proxy-host.spec.ts index 681f2d0ed4..180be46507 100644 --- a/packages/host/apiproxy/tests/api-proxy-host.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-host.spec.ts @@ -25,7 +25,6 @@ function expectOk(response: { readonly result: { readonly ok: true; readonly async function harness( extras: { - openPath?: (path: string, signal: AbortSignal) => Promise canOpenPath?: () => boolean } = {}, ) { @@ -35,13 +34,12 @@ async function harness( const api = createApiProxy(ctx, { defaultModelSelection: () => ({ provider: 'test', model: 'test-model' }), cwd: '/tmp/dsh-apiproxy-host', - ...extras.openPath === undefined ? {} : { openPath: extras.openPath }, ...extras.canOpenPath === undefined ? {} : { canOpenPath: extras.canOpenPath }, }) return { api } } -describe('host.openPath', () => { +describe('host.describe', () => { it('describes whether the deployment can reach a native desktop', async () => { const visible = await harness({ canOpenPath: () => true }) const headless = await harness({ canOpenPath: () => false }) @@ -50,27 +48,4 @@ describe('host.openPath', () => { expect(expectOk(await visible.api.host.describe(request({}))).home).toBe(homedir()) }) - it('opens through the injected native boundary', async () => { - const opened: string[] = [] - const { api } = await harness({ - openPath: async (path) => { opened.push(path) }, - }) - expect((await api.host.openPath( - request({ path: '/tmp/a.txt' }), - new AbortController().signal, - )).result).toEqual({ ok: true, value: { opened: true } }) - expect(opened).toEqual(['/tmp/a.txt']) - }) - - it('propagates abort into the native boundary as a cancelled RPC error', async () => { - const { api } = await harness({ - openPath: (_path, signal) => new Promise((_resolve, reject) => { - signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) - }), - }) - const abort = new AbortController() - const pending = api.host.openPath(request({ path: '/tmp/a.txt' }), abort.signal) - abort.abort() - expect((await pending).result).toMatchObject({ ok: false, error: { code: 'cancelled' } }) - }) }) diff --git a/packages/host/apiproxy/tests/api-proxy-skills-cold.spec.ts b/packages/host/apiproxy/tests/api-proxy-skills-cold.spec.ts deleted file mode 100644 index 9883179ea0..0000000000 --- a/packages/host/apiproxy/tests/api-proxy-skills-cold.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Context } from '@deepseek-ai/cordis' -import AgentRegistry from '@deepseek-ai/dsh-agent' -import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' -import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query' -import type {} from '@deepseek-ai/dsh-skill' -import { describe, expect, it, vi } from 'vitest' -import { createApiProxy } from '../src/api-proxy.ts' -import { RpcId } from '../src/api/rpc.ts' - -describe('skill catalog Session inspection', () => { - it('reads a detached Session without resuming its Agent', async () => { - const ctx = new Context() - await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) - const sessionId = SessionId('cold-skills') - const resolveAgent = vi.fn() - const dispose = vi.fn() - const observeSession = vi.fn(() => Promise.resolve({ - source: 'live', - header: { version: 0 as const, id: sessionId, createdAt: 1, cwd: '/cold/project' }, - events: [], - cursor: -1, - projections: { asOfSeq: -1, values: {} }, - retain: () => { throw new Error('not retained') }, - [Symbol.dispose]: dispose, - } satisfies SessionObservation)) - ctx.provide('sessionQuery', { observeSession } as never) - ctx.provide('sessionController', { resolveAgent } as never) - const list = vi.fn(() => Promise.resolve([{ - name: 'review', - description: 'Review the current change.', - invocation: { modelInvocable: true, userInvocable: true }, - }])) - ctx.provide('skills', { list } as never) - const api = createApiProxy(ctx, { - defaultModelSelection: () => ({ provider: 'p', model: 'm' }), - cwd: '/default', - }) - - const response = await api.skills.list({ rpcId: RpcId('cold-skills'), payload: { sessionId } }) - - expect(response.result).toEqual({ - ok: true, - value: { - skills: [{ - name: 'review', - description: 'Review the current change.', - modelInvocable: true, - }], - }, - }) - expect(observeSession).toHaveBeenCalledWith(sessionId) - expect(dispose).toHaveBeenCalledOnce() - expect(resolveAgent).not.toHaveBeenCalled() - expect(list).toHaveBeenCalledWith({ cwd: '/cold/project', scope: undefined }) - }) - - it('preserves missing and failed cold inspection as distinct API errors', async () => { - const sessionId = SessionId('missing-skills') - for (const fixture of [ - { - error: new SessionQueryError( - 'session "missing-skills" not found', - 'SESSION_QUERY_SESSION_NOT_FOUND', - ), - code: 'session-not-found', - }, - { error: new Error('storage offline'), code: 'internal' }, - ] as const) { - const ctx = new Context() - await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) - ctx.provide('sessionQuery', { - observeSession: () => Promise.reject(fixture.error), - } as never) - ctx.provide('skills', { list: vi.fn() } as never) - const api = createApiProxy(ctx, { - defaultModelSelection: () => ({ provider: 'p', model: 'm' }), - cwd: '/default', - }) - - const response = await api.skills.list({ rpcId: RpcId(fixture.code), payload: { sessionId } }) - - expect(response.result).toMatchObject({ ok: false, error: { code: fixture.code } }) - } - }) -}) diff --git a/packages/host/apiproxy/tests/client-handler.spec.ts b/packages/host/apiproxy/tests/client-handler.spec.ts index b570fbeec7..e587346061 100644 --- a/packages/host/apiproxy/tests/client-handler.spec.ts +++ b/packages/host/apiproxy/tests/client-handler.spec.ts @@ -16,41 +16,14 @@ function ok(request: RpcRequest, value: T): Promise> /** Scripted impl: every method resolves an empty-ish OK unless a case overrides it. */ function scriptedApi(overrides: { host?: Partial - skills?: Partial - agentPresets?: Partial - settings?: Partial - llm?: Partial } = {}): ApiProxy { - const err = (r: RpcRequest): Promise> => - Promise.resolve({ rpcId: r.rpcId, result: { ok: false, error: { code: 'internal' as const, message: 'stub', details: {} } } }) return { host: { describe: r => ok(r, { version: '0-test', cwd: '/t', attachedSessions: 0, home: '/h', canOpenPath: true, }), - openPath: r => ok(r, { opened: true as const }), ...overrides.host, }, - skills: { list: r => ok(r, { skills: [] }), ...overrides.skills }, - agentPresets: { - openDocument: r => ok(r, { opened: true as const }), - ...overrides.agentPresets, - }, - settings: { - openDocument: r => ok(r, { opened: true as const }), - ...overrides.settings, - }, - llm: { - providers: r => ok(r, { providers: [] }), - models: r => ok(r, { - default: { provider: 'test', model: 'test' }, - routableProviders: [], - groups: [], - failures: [], - }), - discoverModels: err, - ...overrides.llm, - }, downloads: { sessionLog: async () => new Response('stub', { status: 404 }) }, } } @@ -59,15 +32,6 @@ function client(api: ApiProxy, timeoutMs?: number): InProcessApiClient { return new InProcessApiClient(toFetchHandler(api), timeoutMs) } -/** Wrap one scripted method to record its invocation into `seen` before responding. */ -function recorderInto(seen: { method: string; payload: unknown }[]) { - return (method: string, respond: (r: RpcRequest

) => Promise>) => - (r: RpcRequest

): Promise> => { - seen.push({ method, payload: r.payload }) - return respond(r) - } -} - describe('unary round trip', () => { it('carries payload out and value back through the full wire form', async () => { let seen: RpcRequest<{}> | undefined @@ -86,11 +50,6 @@ describe('unary round trip', () => { expect(response.result).toMatchObject({ ok: true, value: { version: '0-test' } }) }) - it('routes the agent-preset document opener through the wire', async () => { - const opened = await client(scriptedApi()).agentPresets.openDocument({ agentPreset: 'mine' }) - expect(opened.result).toEqual({ ok: true, value: { opened: true } }) - }) - it('passes business errors through as 200 + err result, not a throw', async () => { const api = scriptedApi({ host: { @@ -116,17 +75,6 @@ describe('unary round trip', () => { await expect(client(api).host.describe({})).rejects.toThrow(/rpcId mismatch/) }) - it('rejects a method/path mismatch as bad-request', async () => { - const handler = toFetchHandler(scriptedApi()) - const body = { type: 'client-request', rpcId: 'r1', method: 'host.describe', payload: {} } - const response = await handler.fetch('http://dsh.internal/api/skill.list', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }) - expect(response.status).toBe(200) - const parsed = await response.json() as { result: { ok: boolean; error?: { code: string; message: string } } } - expect(parsed.result.ok).toBe(false) - expect(parsed.result.error?.code).toBe('bad-request') - expect(parsed.result.error?.message).toMatch(/does not match path/) - }) - it('rejects a malformed envelope as bad-request, salvaging the rpcId or falling back to the sentinel', async () => { const handler = toFetchHandler(scriptedApi()) // No salvageable rpcId → the fixed invalid-request sentinel keeps the response a valid ServerResponse. @@ -278,68 +226,3 @@ describe('envelope tap', () => { expect(batches).toEqual([]) }) }) - -describe('config unary surface', () => { - it('round-trips every settings/llm method with its own payload and value shape', async () => { - const seen: { method: string; payload: unknown }[] = [] - const record = recorderInto(seen) - const providerRow = { - provider: 'openai', - displayName: 'openai', - settingsNs: 'llm-pi-ai', - settingsPath: ['providers', 'openai'], - active: false, - } - const group = { id: 'deepseek-official', name: 'DeepSeek', models: [{ id: 'deepseek-v4-flash', name: 'Flash' }] } - const api = scriptedApi({ - settings: { - openDocument: record('settings.openDocument', r => ok(r, { opened: true as const })), - }, - llm: { - providers: record('llm.providers', r => ok(r, { providers: [providerRow] })), - models: record('llm.models', r => ok(r, { - default: { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, - routableProviders: ['deepseek-official'], - groups: [group], - failures: [], - })), - discoverModels: record('llm.discoverModels', r => ok(r, { models: [{ id: 'acme-large', contextWindow: 65536 }] })), - }, - }) - const c = client(api) - - expect((await c.settings.openDocument({})).result).toEqual({ ok: true, value: { opened: true } }) - const providers = await c.llm.providers({}) - expect(providers.result).toEqual({ ok: true, value: { providers: [providerRow] } }) - const models = await c.llm.models({}) - expect(models.result).toEqual({ - ok: true, - value: { - default: { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, - routableProviders: ['deepseek-official'], - groups: [group], - failures: [], - }, - }) - const discovered = await c.llm.discoverModels({ - settingsNs: 'llm-pi-ai', - baseURL: 'https://gateway.acme.example/v1', - api: 'openai-completions', - apiKey: 'probe-key', - }) - expect(discovered.result).toEqual({ ok: true, value: { models: [{ id: 'acme-large', contextWindow: 65536 }] } }) - - expect(seen.map(call => call.method)).toEqual([ - 'settings.openDocument', - 'llm.providers', 'llm.models', 'llm.discoverModels', - ]) - // The draft crosses whole, credential included: the host needs it for this - // one interrogation and stores none of it. - expect(seen[3]?.payload).toEqual({ - settingsNs: 'llm-pi-ai', - baseURL: 'https://gateway.acme.example/v1', - api: 'openai-completions', - apiKey: 'probe-key', - }) - }) -}) diff --git a/packages/host/apiproxy/tests/fetch-carrier.spec.ts b/packages/host/apiproxy/tests/fetch-carrier.spec.ts index 284a973cb4..2cf89e675d 100644 --- a/packages/host/apiproxy/tests/fetch-carrier.spec.ts +++ b/packages/host/apiproxy/tests/fetch-carrier.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { ApiProxy } from '../src/api/index.ts' -import type { RpcMessage, RpcRequest } from '../src/api/rpc.ts' +import type { RpcMessage } from '../src/api/rpc.ts' import { toFetchHandler } from '../src/fetch/handler.ts' import { AbstractApiClient, InProcessApiClient } from '../src/fetch/client.ts' @@ -18,46 +18,6 @@ function fakeApi(overrides: Partial<{ crashOn: string }> = {}): ApiProxy { }, } }, - async openPath(request) { - return { rpcId: request.rpcId, result: { ok: true, value: { opened: true as const } } } - }, - }, - agentPresets: { - openDocument(request: RpcRequest<{ agentPreset: string }>) { - return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value: { opened: true as const } } }) - }, - }, - skills: { - async list(request) { - return { rpcId: request.rpcId, result: { ok: true, value: { skills: [{ name: 'commit-helper', description: 'Git commits', modelInvocable: true }] } } } - }, - }, - settings: { - async openDocument(request) { - return { rpcId: request.rpcId, result: { ok: false, error: { code: 'internal', message: 'stub', details: {} } } } - }, - }, - llm: { - async providers(request) { - return { rpcId: request.rpcId, result: { ok: true, value: { providers: [] } } } - }, - async models(request) { - return { - rpcId: request.rpcId, - result: { - ok: true, - value: { - default: { provider: 'test', model: 'test' }, - routableProviders: [], - groups: [], - failures: [], - }, - }, - } - }, - async discoverModels(request) { - return { rpcId: request.rpcId, result: { ok: true, value: { models: [] } } } - }, }, downloads: { async sessionLog() { @@ -79,85 +39,16 @@ describe('unary round trip (handler ⇄ client, no network)', () => { }) it('carries a business error as 200 + error result', async () => { - const response = await client().settings.openDocument({}) + const api = fakeApi() + api.host.describe = request => Promise.resolve({ + rpcId: request.rpcId, + result: { ok: false, error: { code: 'internal', message: 'stub', details: {} } }, + }) + const response = await client(api).host.describe({}) expect(response.result.ok).toBe(false) if (!response.result.ok) expect(response.result.error.code).toBe('internal') }) - it('round-trips the agent-preset document opener', async () => { - // The opener is the domain's whole carried surface: its request schema is - // registered in both halves, so a missing registration fails here rather - // than in the browser. - expect((await client().agentPresets.openDocument({ agentPreset: 'mine' })).result) - .toEqual({ ok: true, value: { opened: true } }) - }) - - it('round-trips host.openPath through the wire form', async () => { - const api = fakeApi() - let opened: string | undefined - api.host.openPath = async (request) => { - opened = request.payload.path - return { rpcId: request.rpcId, result: { ok: true, value: { opened: true as const } } } - } - const response = await client(api).host.openPath({ path: '/tmp/a.txt' }) - expect(opened).toBe('/tmp/a.txt') - expect(response.result).toEqual({ ok: true, value: { opened: true } }) - }) - - it('round-trips skill.list through the wire form', async () => { - const c = client() - const skills = await c.skills.list({ sessionId: 's' as never }) - expect(skills.result).toEqual({ ok: true, value: { skills: [{ name: 'commit-helper', description: 'Git commits', modelInvocable: true }] } }) - }) - - it('keeps caller and connection aborts on a signal-taking unary', async () => { - const api = fakeApi() - const started = Promise.withResolvers() - api.host.openPath = async (request, signal) => { - started.resolve(signal) - if (!signal.aborted) { - await new Promise((resolve) => { - signal.addEventListener('abort', () => { resolve() }, { once: true }) - }) - } - return { - rpcId: request.rpcId, - result: { ok: false, error: { code: 'cancelled', message: 'aborted', details: {} } }, - } - } - const controller = new AbortController() - const execution = client(api).host.openPath({ path: '/tmp/a.txt' }, controller.signal) - const handlerSignal = await started.promise - - controller.abort(new Error('connection closed')) - - await expect(execution).rejects.toThrow('connection closed') - expect(handlerSignal.aborted).toBe(true) - }) - - it('propagates the carrier Request signal into host.openPath', async () => { - const api = fakeApi() - api.host.openPath = async (request, signal) => { - if (!signal.aborted) { - await new Promise((resolve) => { - signal.addEventListener('abort', () => { resolve() }, { once: true }) - }) - } - return { - rpcId: request.rpcId, - result: { ok: false, error: { code: 'cancelled', message: 'aborted', details: {} } }, - } - } - const handler = toFetchHandler(api) - const controller = new AbortController() - 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() - const parsed = await (await pending).json() as { result: { error?: { code: string } } } - expect(parsed.result.error?.code).toBe('cancelled') - }) }) describe('handler carrier-layer statuses', () => { @@ -182,17 +73,9 @@ describe('handler carrier-layer statuses', () => { expect(body.result.error?.code).toBe('bad-request') }) - it('rejects a method/path mismatch echoing the envelope rpcId', async () => { - const body = JSON.stringify({ type: 'client-request', rpcId: 'r-9', method: 'host.describe', payload: {} }) - const response = await handler.fetch(new Request('http://x/api/skill.list', { method: 'POST', headers: { 'content-type': 'application/json' }, body })) - const parsed = await response.json() as { rpcId: string; result: { error?: { message: string } } } - expect(parsed.rpcId).toBe('r-9') - expect(parsed.result.error?.message).toContain('does not match path') - }) - it('rejects an invalid payload with the zod issues attached', async () => { - const body = JSON.stringify({ type: 'client-request', rpcId: 'r-10', method: 'host.openPath', payload: {} }) - const response = await handler.fetch(new Request('http://x/api/host.openPath', { method: 'POST', headers: { 'content-type': 'application/json' }, body })) + const body = JSON.stringify({ type: 'client-request', rpcId: 'r-10', method: 'host.describe', payload: null }) + const response = await handler.fetch(new Request('http://x/api/host.describe', { method: 'POST', headers: { 'content-type': 'application/json' }, body })) const parsed = await response.json() as { result: { error?: { code: string; details: { issues: unknown[] } } } } expect(parsed.result.error?.code).toBe('bad-request') expect(parsed.result.error?.details.issues.length).toBeGreaterThan(0) @@ -263,7 +146,7 @@ describe('envelope observation', () => { const c = client() const batches: (readonly RpcMessage[])[] = [] c.subscribeEnvelopes((batch) => { batches.push(batch) }) - await Promise.all([c.host.describe({}), c.skills.list({ sessionId: 's1' as never })]) + await Promise.all([c.host.describe({}), c.host.describe({})]) await new Promise((resolve) => { setTimeout(resolve, 0) }) const total = batches.reduce((n, batch) => n + batch.length, 0) expect(total).toBe(4) diff --git a/packages/host/apiproxy/tests/rpc-schemas.spec.ts b/packages/host/apiproxy/tests/rpc-schemas.spec.ts index 3f863b7817..33cf4b2eca 100644 --- a/packages/host/apiproxy/tests/rpc-schemas.spec.ts +++ b/packages/host/apiproxy/tests/rpc-schemas.spec.ts @@ -6,8 +6,6 @@ import { } from '../src/api/rpc.schema.ts' import { z } from 'zod' 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' describe('RpcId', () => { it('brands a raw string at zero runtime cost', () => { @@ -37,7 +35,6 @@ describe('rpcErrorSchema', () => { expect(rpcErrorSchema.parse({ code: 'agent-preset-not-found', message: 'm', details: { agentPreset: 'p', available: [] } }).code).toBe('agent-preset-not-found') expect(rpcErrorSchema.parse({ code: 'agent-preset-invalid', message: 'm', details: { agentPreset: 'p', reason: 'bad' } }).code).toBe('agent-preset-invalid') expect(rpcErrorSchema.parse({ code: 'agent-busy', message: 'm', details: { reason: 'r' } }).code).toBe('agent-busy') - expect(rpcErrorSchema.parse({ code: 'model-discovery-failed', message: 'm', details: { settingsNs: 'n' } }).code).toBe('model-discovery-failed') expect(rpcErrorSchema.parse({ code: 'internal', message: 'm', details: {} }).code).toBe('internal') }) @@ -97,32 +94,3 @@ describe('host domain schemas', () => { })).toThrow() }) }) - -describe('skills domain schemas', () => { - it('validates the list request/value pair', () => { - expect(skillListRequestSchema.parse({ sessionId: 's1' })).toEqual({ sessionId: 's1' }) - // The wire is session-addressed only: a sessionId-less payload fails. - expect(() => skillListRequestSchema.parse({})).toThrow() - expect(skillListValueSchema.parse({ skills: [] }).skills).toEqual([]) - const value = skillListValueSchema.parse({ skills: [ - { name: 'commit-helper', description: 'Git commits', whenToUse: 'when committing', modelInvocable: true }, - { name: 'bare', description: 'No guidance', modelInvocable: false }, - ] }) - expect(value.skills[0]?.whenToUse).toBe('when committing') - expect(value.skills[1]?.whenToUse).toBeUndefined() - expect(value.skills[1]?.modelInvocable).toBe(false) - expect(() => skillEntrySchema.parse({ name: '', description: 'd', modelInvocable: true })).toThrow() - // modelInvocable is required wire data: an entry without it fails. - expect(() => skillEntrySchema.parse({ name: 'n', description: 'd' })).toThrow() - }) -}) - -describe('agent-preset schemas', () => { - it('answers the open-document union by its discriminant', () => { - expect(agentPresetOpenDocumentValueSchema.parse({ opened: true })).toEqual({ opened: true }) - expect(agentPresetOpenDocumentValueSchema.parse({ opened: false, path: '/presets/mine' })) - .toEqual({ opened: false, path: '/presets/mine' }) - // A closed reply must carry the path the surface shows instead. - expect(() => agentPresetOpenDocumentValueSchema.parse({ opened: false })).toThrow() - }) -}) diff --git a/packages/llm/llm-pi-ai/tests/discovery.spec.ts b/packages/llm/llm-pi-ai/tests/discovery.spec.ts index 18793565d7..17d59a3712 100644 --- a/packages/llm/llm-pi-ai/tests/discovery.spec.ts +++ b/packages/llm/llm-pi-ai/tests/discovery.spec.ts @@ -293,8 +293,7 @@ describe('draft-provider model discovery', () => { }) const probe = ctx.llm.discoverModels('llm-pi-ai', { baseURL: 'https://slow.example/v1', - signal: controller.signal, - }) + }, controller.signal) await bodyRead.promise controller.abort('test cancellation') @@ -306,8 +305,7 @@ describe('draft-provider model discovery', () => { const aborted = AbortSignal.abort('test cancellation') await expect(ctx.llm.discoverModels('llm-pi-ai', { baseURL: 'http://127.0.0.1:9/v1', - signal: aborted, - })).rejects.toMatchObject({ code: 'ABORTED' }) + }, aborted)).rejects.toMatchObject({ code: 'ABORTED' }) }) it('is offered for the namespace, and refuses one it does not serve', async () => { diff --git a/packages/util/native-command/README.i18n.yaml b/packages/util/native-command/README.i18n.yaml index ffcd07c65a..65d2edff25 100644 --- a/packages/util/native-command/README.i18n.yaml +++ b/packages/util/native-command/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/util/native-command/README.md -README.md: b2d5645b35f6ea157cb3d526326f60352a421550 -README.zh.md: 93299c6a0550c087f673e2c4bfe5009732168759 +README.md: 282cefcc00d296b3c09741e64c88647343e1bef2 +README.zh.md: aecb60d2e8f68a6e9468e3f2d4b0e2d99bebefc6 diff --git a/packages/util/native-command/README.md b/packages/util/native-command/README.md index b2d5645b35..282cefcc00 100644 --- a/packages/util/native-command/README.md +++ b/packages/util/native-command/README.md @@ -1,5 +1,5 @@ --- -description: "A zero-dependency no-shell execFile runner for host-native OS integrations, with utf8 stdio capture, abort propagation, and a hidden console window on Windows." +description: "Host-native command and path-opening utilities with shell-free execution, cancellation, desktop detection, and WSL path handoff." kind: "package-library" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-native-command` runs a host executable directly — never through a shell string — and captures its utf8 stdout and stderr. The caller's abort signal terminates the child, and on Windows the transient console window stays hidden. A failed run rejects with the exit code and both captured streams attached, so callers classify a missing tool, a cancellation, or a real failure without re-running anything. The host-side consumers are the native directory chooser and the open-with-default-application hand-off. It is a library, not a plugin: no `ctx`, no state, no events. +`dsh-native-command` runs host executables without a shell and opens Host filesystem paths through the desktop. The command runner captures utf8 output, propagates cancellation, and hides transient Windows consoles. The path opener supports default-application and text-editor intents, browser-renderable documents, WSL translation, and desktop availability checks. It is a library, not a plugin: no `ctx`, no state, no events. ## Table of Contents @@ -43,6 +43,10 @@ On exit 0 the call resolves with captured stdout and stderr. On any failure it r The `NativeCommandRunner` type is the injectable command boundary for host integrations: pass the function (or a wrapper) where the integration needs a testable seam, so tests can substitute a fake runner. +### Opening a Host path + +`openNativePath(path, signal)` hands a path to the default application and prefers the named default browser for HTML and SVG where the platform can identify one. `openNativeTextFile(path, signal)` selects text-editor intent; on macOS it uses `open -t`. WSL paths are translated with `wslpath -w` before the Windows desktop receives them. `canOpenNativePath()` reports whether the current Host plausibly has a desktop target. + ----- @@ -51,13 +55,15 @@ The `NativeCommandRunner` type is the injectable command boundary for host integ

Implementation internals — click to expand -The runner is a thin wrapper over Node's `execFile` with three fixed choices: utf8 encoding, abort propagation, and Windows console hiding. +The command runner is a thin wrapper over Node's `execFile`. The path opener selects one shell-free command from platform and environment facts, while callers retain authority over which path may be opened. ### Source map | File | Role | |---|---| -| [`src/index.ts`](src/index.ts) | `runNativeCommand` and the `NativeCommandRunner` type — the whole package | +| [`src/index.ts`](src/index.ts) | Public command-runner and path-opener exports | +| [`src/runner.ts`](src/runner.ts) | Shell-free `execFile` adapter | +| [`src/path-opener.ts`](src/path-opener.ts) | Desktop detection, open intents, browser preference, and WSL translation | | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; each run is one stateless child-process round trip) | ### What execFile gives the runner @@ -74,7 +80,8 @@ The runner is a thin wrapper over Node's `execFile` with three fixed choices: ut Read these pages when you need the consumers or the general subprocess capability this utility deliberately is not. - [Native directory picker](../../host/directory-picker-native/README.md) — the OS chooser commands this runner executes. -- [Host API proxy](../../host/apiproxy/README.md) — the open-with-default-application hand-off this runner serves. +- [Session Controller](../../api/session-controller/README.md) — resolves Session-relative workspace paths before opening them. +- [Settings Controller](../../api/settings-controller/README.md) — selects settings documents and agent-preset directories. - [Subprocess capability](../../subprocess/subprocess/README.md) — the general subprocess seam, of which this package is not a part. ----- @@ -82,7 +89,7 @@ Read these pages when you need the consumers or the general subprocess capabilit ## Model Experience -None, as the host-side subprocess runner registers nothing model-facing. +None, as the host-side utilities register nothing model-facing. #### KV Cache effect diff --git a/packages/util/native-command/README.zh.md b/packages/util/native-command/README.zh.md index 93299c6a05..aecb60d2e8 100644 --- a/packages/util/native-command/README.zh.md +++ b/packages/util/native-command/README.zh.md @@ -1,5 +1,5 @@ --- -description: "供宿主原生 OS 集成使用的零依赖免 shell execFile 运行器,支持 utf8 标准流捕获、中止传播与 Windows 隐藏控制台窗口。" +description: "宿主原生命令与路径打开工具,提供无 shell 执行、取消、桌面探测与 WSL 路径交接。" kind: "package-library" --- @@ -9,7 +9,7 @@ kind: "package-library" ## 概述 -`dsh-native-command` 直接运行宿主可执行文件——绝不拼 shell 字符串——并捕获其 utf8 stdout 与 stderr。调用方的中止信号会终止子进程,在 Windows 上瞬时控制台窗口保持隐藏。失败时调用会以错误拒绝,该错误附带退出码与两路已捕获输出,因此调用方无需重跑即可区分工具缺失、取消与真实失败。宿主侧消费方是原生目录选择器与「用默认应用打开」的交接。它是库而非插件:没有 `ctx`、无状态、不发事件。 +`dsh-native-command` 无需 shell 即可运行 Host 可执行文件,并通过桌面打开 Host 文件系统路径。命令运行器捕获 utf8 输出、传播取消,并隐藏 Windows 瞬时控制台。路径打开器支持默认应用与文本编辑器意图、浏览器可渲染文档、WSL 转换与桌面可用性检查。它是库而非插件:没有 `ctx`、无状态、不发事件。 ## 目录 @@ -43,6 +43,10 @@ const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], s `NativeCommandRunner` 类型是宿主集成的可注入命令边界:在集成需要一个可测试接缝的位置传入该函数(或其包装层),测试即可替换为假运行器。 +### 打开 Host 路径 + +`openNativePath(path, signal)` 将路径交给默认应用;平台能够确定默认浏览器时,HTML 与 SVG 会优先交给该浏览器。`openNativeTextFile(path, signal)` 选择文本编辑器意图;macOS 使用 `open -t`。WSL 路径先通过 `wslpath -w` 转换,再交给 Windows 桌面。`canOpenNativePath()` 报告当前 Host 是否可能具备桌面目标。 + ----- @@ -51,13 +55,15 @@ const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], s
实现细节——点击展开 -本运行器是 Node `execFile` 的薄包装,固定三项选择:utf8 编码、中止传播与 Windows 控制台隐藏。 +命令运行器是 Node `execFile` 的薄包装。路径打开器根据平台与环境事实选择一条无 shell 命令,而调用方继续负责决定允许打开哪个路径。 ### 源码地图 | 文件 | 职责 | |---|---| -| [`src/index.ts`](src/index.ts) | `runNativeCommand` 与 `NativeCommandRunner` 类型——即整个包 | +| [`src/index.ts`](src/index.ts) | 命令运行器与路径打开器的公共导出 | +| [`src/runner.ts`](src/runner.ts) | 无 shell 的 `execFile` 适配器 | +| [`src/path-opener.ts`](src/path-opener.ts) | 桌面探测、打开意图、浏览器偏好与 WSL 转换 | | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;每次运行都是一次无状态的子进程往返) | ### execFile 给了运行器什么 @@ -74,7 +80,8 @@ const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], s 当你需要消费方或本工具刻意不属于的通用子进程能力时,阅读以下页面。 - [原生目录选择器](../../host/directory-picker-native/README.zh.md)——本运行器执行的 OS 选择器命令。 -- [宿主 API 代理](../../host/apiproxy/README.zh.md)——本运行器服务的「用默认应用打开」交接。 +- [Session Controller](../../api/session-controller/README.zh.md)——打开前解析 Session 相对 workspace 路径。 +- [Settings Controller](../../api/settings-controller/README.zh.md)——选择 settings 文档与 agent-preset 目录。 - [子进程能力](../../subprocess/subprocess/README.zh.md)——通用子进程 seam,本包并非其组成部分。 ----- @@ -82,7 +89,7 @@ const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], s ## 模型体验 -无:宿主侧子进程运行器不注册任何面向模型的内容。 +无:宿主侧工具不注册任何面向模型的内容。 #### KV Cache 影响 diff --git a/packages/util/native-command/package.json b/packages/util/native-command/package.json index 285c468c95..43dc4c7657 100644 --- a/packages/util/native-command/package.json +++ b/packages/util/native-command/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-native-command", - "description": "Zero-dependency no-shell execFile runner for host-native OS integrations: utf8 stdio capture, abort propagation, Windows hide", + "description": "Host-native command and path-opening utilities with shell-free execution, cancellation, desktop detection, and WSL handoff", "version": "0.1.1-rc.2", "publishConfig": { "access": "public" diff --git a/packages/host/apiproxy/tests/native-path-opener.spec.ts b/packages/util/native-command/tests/path-opener.spec.ts similarity index 99% rename from packages/host/apiproxy/tests/native-path-opener.spec.ts rename to packages/util/native-command/tests/path-opener.spec.ts index cf4489f864..d5cd1bb751 100644 --- a/packages/host/apiproxy/tests/native-path-opener.spec.ts +++ b/packages/util/native-command/tests/path-opener.spec.ts @@ -1,3 +1,4 @@ +/** Cross-platform native path opener behavior. */ type ExecFileCallback = ( error: (Error & { code?: string | number }) | null, stdout: string, @@ -16,7 +17,7 @@ vi.mock('node:child_process', () => ({ execFile: execFileMock })) import { release as osRelease } from 'node:os' import { describe, expect, it, vi } from 'vitest' -import { canOpenNativePath, openNativePath, openNativeTextFile, type PathOpenerRunner } from '../src/native-path-opener.ts' +import { canOpenNativePath, openNativePath, openNativeTextFile, type PathOpenerRunner } from '../src/index.ts' const signal = () => new AbortController().signal diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7aec63d5d..4eab46efb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -777,6 +777,9 @@ importers: '@deepseek-ai/dsh-client-store': specifier: workspace:^ version: link:../../client/store + '@deepseek-ai/dsh-file-reference': + specifier: workspace:^ + version: link:../../context/file-reference '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../runtime-diagnostics/invariants @@ -786,6 +789,9 @@ importers: '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm + '@deepseek-ai/dsh-native-command': + specifier: workspace:^ + version: link:../../util/native-command '@deepseek-ai/dsh-permission-presets': specifier: workspace:^ version: link:../../interaction/permission-presets @@ -810,6 +816,9 @@ importers: '@deepseek-ai/dsh-session-title': specifier: workspace:^ version: link:../../session/session-title + '@deepseek-ai/dsh-skill': + specifier: workspace:^ + version: link:../../skill/skill '@deepseek-ai/dsh-subagent': specifier: workspace:^ version: link:../../subagent/subagent @@ -831,6 +840,9 @@ importers: packages/api/settings-controller: dependencies: + '@deepseek-ai/schemastery': + specifier: link:../../../vendor/schemastery + version: link:../../../vendor/schemastery zod: specifier: ^4.4.3 version: 4.4.3 @@ -838,12 +850,18 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-agent-presets': + specifier: workspace:^ + version: link:../../preset/agent-presets '@deepseek-ai/dsh-credentials': specifier: workspace:^ version: link:../../credentials/credentials '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../runtime-diagnostics/invariants + '@deepseek-ai/dsh-native-command': + specifier: workspace:^ + version: link:../../util/native-command '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -2047,9 +2065,6 @@ importers: '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools - '@deepseek-ai/dsh-util-workspace-path': - specifier: workspace:^ - version: link:../../util/workspace-path '@types/react': specifier: ~18.3.1 version: 18.3.31 @@ -2634,9 +2649,6 @@ importers: '@deepseek-ai/dsh-api-session-controller': specifier: workspace:^ version: link:../../api/session-controller - '@deepseek-ai/dsh-client-connection': - specifier: workspace:^ - version: link:../connection '@deepseek-ai/dsh-client-locale': specifier: workspace:^ version: link:../locale @@ -3067,9 +3079,6 @@ importers: '@deepseek-ai/dsh-api-remotes': specifier: workspace:^ version: link:../../api/remotes - '@deepseek-ai/dsh-client-connection': - specifier: workspace:^ - version: link:../connection '@deepseek-ai/dsh-client-locale': specifier: workspace:^ version: link:../locale @@ -4024,9 +4033,6 @@ importers: '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../runtime-diagnostics/invariants - '@deepseek-ai/dsh-typert-protocol': - specifier: workspace:^ - version: link:../../typert/protocol packages/context/file-reference-local: dependencies: @@ -5811,21 +5817,9 @@ importers: '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand - '@deepseek-ai/dsh-commands': - specifier: workspace:^ - version: link:../../interaction/commands - '@deepseek-ai/dsh-host-directory-picker': - specifier: workspace:^ - version: link:../directory-picker - '@deepseek-ai/dsh-llm': - specifier: workspace:^ - version: link:../../llm/llm '@deepseek-ai/dsh-native-command': specifier: workspace:^ version: link:../../util/native-command - '@deepseek-ai/dsh-scope': - specifier: workspace:^ - version: link:../../core/scope '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -5835,12 +5829,6 @@ importers: '@deepseek-ai/dsh-session-query': specifier: workspace:^ version: link:../../session-query/session-query - '@deepseek-ai/dsh-settings': - specifier: workspace:^ - version: link:../../settings/settings - '@deepseek-ai/dsh-skill': - specifier: workspace:^ - version: link:../../skill/skill '@deepseek-ai/dsh-util-crypto': specifier: workspace:^ version: link:../../util/crypto @@ -5857,15 +5845,15 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis - '@deepseek-ai/dsh-agent-presets': - specifier: workspace:^ - version: link:../../preset/agent-presets '@deepseek-ai/dsh-credentials': specifier: workspace:^ version: link:../../credentials/credentials '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../runtime-diagnostics/invariants + '@deepseek-ai/dsh-settings': + specifier: workspace:^ + version: link:../../settings/settings '@deepseek-ai/dsh-typert-protocol': specifier: workspace:^ version: link:../../typert/protocol @@ -6299,6 +6287,9 @@ importers: '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery + zod: + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -6315,6 +6306,9 @@ importers: '@deepseek-ai/dsh-timeout': specifier: workspace:^ version: link:../../util/timeout + '@deepseek-ai/dsh-typert-protocol': + specifier: workspace:^ + version: link:../../typert/protocol packages/llm/llm-deepseek: dependencies: diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index d9870dabc7..5c2fcca659 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -92,10 +92,12 @@ export const SERVICE_PAGE: Record = { sandboxPolicy: 'sandbox.md', sessionPersistence: 'persistence.md', sessionQuery: 'session-query.md', + sessionFileReferences: 'session-reference.md', sessionReferenceResolver: 'session-reference.md', sessionProjectionCache: 'session-projection.md', sessionProjections: 'session-projection.md', sessionController: 'session.md', + sessionSkillCatalog: 'skills.md', sessions: 'session.md', settings: 'settings.md', sessionTitle: 'session-title.md', @@ -318,6 +320,9 @@ export const LINK_MAP: Readonly> = { SessionId: 'core.md', SessionListRequest: 'session.md', SessionListValue: 'session.md', + ModelCatalog: 'session.md', + SessionOpenWorkspacePathRequest: 'session.md', + SessionOpenWorkspacePathValue: 'session.md', SessionModels: 'session.md', SessionModelsRequest: 'session.md', SessionPage: 'session.md', @@ -533,12 +538,16 @@ export const LINK_MAP: Readonly> = { SettingsScope: 'settings.md', SettingsDescriptor: 'settings.md', SettingsDescribeValue: 'settings.md', + SettingsDocumentOpenValue: 'settings.md', + AgentPresetDirectoryOpenValue: 'settings.md', SettingsNamespaceView: 'settings.md', SettingsPathOpView: 'settings.md', SettingsSecretView: 'settings.md', SettingsPathOp: 'settings.md', SettingsDescribeOptions: 'settings.md', SettingsUpdateSource: 'settings.md', + SkillListRequest: 'skills.md', + SkillListValue: 'skills.md', AuthorizationEntry: 'credentials.md', AuthorizationFlow: 'credentials.md', AuthorizationInteraction: 'credentials.md', diff --git a/scripts/gen-cordis-inspect-catalog.ts b/scripts/gen-cordis-inspect-catalog.ts index df3dfd3ed1..cb5ae60b65 100644 --- a/scripts/gen-cordis-inspect-catalog.ts +++ b/scripts/gen-cordis-inspect-catalog.ts @@ -17,7 +17,7 @@ const CLIENT_SERVICES: Readonly> = { theme: ['getTheme', 'setTheme', 'setFontSize', 'register', 'overrideTokens'], uiWorkspace: [ 'connectWorkspace', 'startSession', 'archiveSession', 'pickDirectory', 'listDirectory', - 'createDirectory', 'openPath', + 'createDirectory', ], workspaces: ['create', 'rename', 'delete', 'insertSessionBefore', 'archiveSession'], } diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 25c60c5b52..1119f7be56 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -154,8 +154,21 @@ const SERVICE_ROLES: ServiceRole[] = [ pkg: 'api-session-controller', title: 'Host Session Remote controller', mode: 'core', - consumers: ['host-apiproxy'], - note: '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.', + note: 'Owns Session commands, cold reads, durable-event following, live control state, model catalogs, workspace opening, and Agent activation policy.', + }, + { + key: 'sessionFileReferences', + pkg: 'api-session-controller', + title: 'Session-addressed file-reference Remote adapter', + mode: 'core', + note: 'Delegates file-reference discovery through the Session Controller\'s established Agent lookup policy.', + }, + { + key: 'sessionSkillCatalog', + pkg: 'api-session-controller', + title: 'Session-addressed skill Remote adapter', + mode: 'core', + note: 'Lists the Session composition\'s user-invocable skills without activating a cold Agent.', }, { key: 'credentialsController', @@ -308,7 +321,8 @@ const SERVICE_ROLES: ServiceRole[] = [ title: 'File reference discovery', mode: 'seam', implementations: ['file-reference-local'], - note: 'The interface returns path-only completion candidates within the addressed Agent cwd through its unary Remote contract; providers own namespace access and ranking without reading file contents.', + consumers: ['api-session-controller'], + note: 'The interface returns path-only completion candidates within an Agent cwd; providers own namespace access and ranking without reading file contents.', }, { key: 'sessionReferenceResolver', From 674301721cd15852fd38bdf405b33c3693ad2b94 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:06:20 +0800 Subject: [PATCH 05/13] fix(build): correct workspace dependency declarations --- packages/api/settings-controller/package.json | 3 +-- packages/context/time-context/package.json | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json index 4f4326f47e..4de5f8d490 100644 --- a/packages/api/settings-controller/package.json +++ b/packages/api/settings-controller/package.json @@ -70,7 +70,6 @@ "@deepseek-ai/dsh-native-command": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-settings": "workspace:^", - "@deepseek-ai/dsh-typert-protocol": "workspace:^", - "@deepseek-ai/schemastery": "workspace:^" + "@deepseek-ai/dsh-typert-protocol": "workspace:^" } } diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index af2c0e4672..4ed058608b 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -37,6 +37,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, From 88f2f0aaecda19812f7ef7d384a8f62e65386b1b Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:12:24 +0800 Subject: [PATCH 06/13] test(api): complete migrated Remote coverage --- docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 3 +- docs/module-graph.zh.md | 3 +- .../tests/session-models.host.spec.ts | 4 +- .../session-open-workspace-path.host.spec.ts | 56 ++++++++++- .../tests/session-skills.host.spec.ts | 37 +++++++ .../session-controller/tests/test-remote.ts | 3 + .../tests/settings-controller.host.spec.ts | 96 +++++++++++++++++++ .../tests/store.client.spec.ts | 15 +++ .../tests/apply.client.spec.ts | 8 +- .../tests/stores.client.spec.ts | 36 +++---- 11 files changed, 234 insertions(+), 31 deletions(-) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 73c0c75403..00810b4551 100644 --- a/docs/module-graph.i18n.yaml +++ b/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: e0376d865fac1505cce48f4f3a678a11730ecd0e -module-graph.zh.md: 72af3b9a52764e0ae8ed2b7cef910eeedefa6c6c +module-graph.md: aebb6883280d5edc48355e83936cb8793529fc2a +module-graph.zh.md: 43607e291b632d1df6cd00a59e182027dc548008 diff --git a/docs/module-graph.md b/docs/module-graph.md index e0376d865f..aebb688328 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -549,6 +549,7 @@ flowchart TD pkg_file_reference --> pkg_invariants pkg_time_context --> pkg_agent pkg_time_context --> pkg_invariants + pkg_time_context --> pkg_llm pkg_time_context --> pkg_session pkg_message_feedback --> pkg_brand pkg_message_feedback --> pkg_invariants @@ -1789,7 +1790,7 @@ flowchart TD | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) | | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) | | [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants) | -| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | +| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) | | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 72af3b9a52..43607e291b 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -551,6 +551,7 @@ flowchart TD pkg_file_reference --> pkg_invariants pkg_time_context --> pkg_agent pkg_time_context --> pkg_invariants + pkg_time_context --> pkg_llm pkg_time_context --> pkg_session pkg_message_feedback --> pkg_brand pkg_message_feedback --> pkg_invariants @@ -1791,7 +1792,7 @@ flowchart TD | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) | | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) | | [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants) | -| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | +| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) | | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | diff --git a/packages/api/session-controller/tests/session-models.host.spec.ts b/packages/api/session-controller/tests/session-models.host.spec.ts index d203585459..bc67a94ca6 100644 --- a/packages/api/session-controller/tests/session-models.host.spec.ts +++ b/packages/api/session-controller/tests/session-models.host.spec.ts @@ -305,9 +305,9 @@ describe('Web session model selection', () => { model: 'private-preview', reasoningEffort: ReasoningEffortId('max'), }) - createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' }) + const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' }) - const catalog = await buildModelCatalog(ctx) + const catalog = expectValue(await remote.modelCatalog()) expect(currentSelection(ctx, sessionId)).toEqual({ provider: 'deepseek-official', model: 'private-preview', diff --git a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts index f8c89efd5e..ca7a78c589 100644 --- a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts +++ b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts @@ -2,7 +2,11 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import { describe, expect, it, vi } from 'vitest' -import { createSessionTestRemote, testSessionPersistence } from './test-remote.ts' +import { + createSessionTestController, + createSessionTestRemote, + testSessionPersistence, +} from './test-remote.ts' async function context(): Promise { const ctx = new Context() @@ -92,4 +96,54 @@ describe('session/openWorkspacePath', () => { await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }, aborted.signal)) .resolves.toMatchObject({ ok: false, error: { code: 'cancelled' } }) }) + + it('classifies inspection cancellation and non-session failures', async () => { + const ctx = await context() + const controller = createSessionTestController(ctx, { + defaultModelSelection: () => ({ provider: 'p', model: 'm' }), + cwd: '/default', + }) + const inspect = vi.spyOn(controller, 'inspect') + const aborted = new AbortController() + inspect.mockImplementationOnce(async () => { + aborted.abort(new Error('cancelled')) + throw new Error('inspection stopped') + }) + await expect(controller.openWorkspacePath({ + sessionId: SessionId('inspection-cancelled'), path: 'result.html', + }, aborted.signal)).rejects.toMatchObject({ failure: { code: 'cancelled' } }) + + inspect.mockRejectedValueOnce('storage offline') + await expect(controller.openWorkspacePath({ + sessionId: SessionId('inspection-failed'), path: 'result.html', + }, new AbortController().signal)).rejects.toMatchObject({ + failure: { code: 'internal', message: expect.stringContaining('storage offline') }, + }) + }) + + it('classifies opener cancellation and non-Error failures', async () => { + const ctx = await context() + const sessionId = SessionId('open-error-kinds') + ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } }) + const aborted = new AbortController() + const openPath = vi.fn() + .mockImplementationOnce(async () => { + aborted.abort(new Error('cancelled')) + throw new Error('opening stopped') + }) + .mockRejectedValueOnce('desktop unavailable') + const controller = createSessionTestController(ctx, { + defaultModelSelection: () => ({ provider: 'p', model: 'm' }), + cwd: '/default', + openPath, + }) + + await expect(controller.openWorkspacePath({ sessionId, path: 'first.html' }, aborted.signal)) + .rejects.toMatchObject({ failure: { code: 'cancelled' } }) + await expect(controller.openWorkspacePath({ + sessionId, path: 'second.html', + }, new AbortController().signal)).rejects.toMatchObject({ + failure: { code: 'internal', message: 'path open failed: desktop unavailable' }, + }) + }) }) diff --git a/packages/api/session-controller/tests/session-skills.host.spec.ts b/packages/api/session-controller/tests/session-skills.host.spec.ts index e0640ddfe6..32d9957d4b 100644 --- a/packages/api/session-controller/tests/session-skills.host.spec.ts +++ b/packages/api/session-controller/tests/session-skills.host.spec.ts @@ -188,4 +188,41 @@ describe('SessionSkillCatalog', () => { failure: { code: 'internal', message: expect.stringContaining('skill registry is absent') }, }) }) + + it('rejects observations without projections or a project cwd', async () => { + const ctx = await context() + const sessionId = SessionId('incomplete-skills') + const withoutProjections = { ...observation(sessionId, { cwd: '/project' }), projections: undefined } + const observeSession = vi.fn() + .mockResolvedValueOnce(withoutProjections) + .mockResolvedValueOnce(observation(sessionId)) + ctx.provide('sessionQuery', { observeSession } as never) + const catalog = new SessionSkillCatalog(ctx) + + await expect(catalog.list({ sessionId }, new AbortController().signal)) + .rejects.toMatchObject({ + failure: { code: 'internal', message: expect.stringContaining('projected Session observation') }, + }) + await expect(catalog.list({ sessionId }, new AbortController().signal)) + .rejects.toMatchObject({ + failure: { code: 'internal', message: expect.stringContaining('has no project cwd') }, + }) + }) + + it('classifies a provider listing failure', async () => { + const ctx = await context() + const sessionId = SessionId('failed-skills') + ctx.provide('sessionQuery', { + observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/project' })), + } as never) + ctx.provide('skills', { + list: () => Promise.reject(new Error('catalog offline')), + } as never) + const catalog = new SessionSkillCatalog(ctx) + + await expect(catalog.list({ sessionId }, new AbortController().signal)) + .rejects.toMatchObject({ + failure: { code: 'internal', message: 'skill listing failed: Error: catalog offline' }, + }) + }) }) diff --git a/packages/api/session-controller/tests/test-remote.ts b/packages/api/session-controller/tests/test-remote.ts index f5766382cb..77e813b324 100644 --- a/packages/api/session-controller/tests/test-remote.ts +++ b/packages/api/session-controller/tests/test-remote.ts @@ -19,6 +19,7 @@ import { } from '@deepseek-ai/dsh-typert-protocol' import SessionController from '../src/index.ts' import type { + ModelCatalog, SessionAttachmentRequest, SessionAttachmentValue, SessionCancelRequest, @@ -54,6 +55,7 @@ export interface TestSessionRemote { search(request: SessionSearchRequest, signal?: AbortSignal): Promise> create(request: SessionCreateRequest): Promise> selectModel(request: SessionSelectModelRequest): Promise> + modelCatalog(): Promise> rename(request: SessionRenameRequest): Promise> fork(request: SessionForkRequest): Promise> prompt(request: SessionPromptRequest, signal?: AbortSignal): Promise> @@ -241,6 +243,7 @@ export function createSessionTestRemote( ), create: request => remoteResult(() => direct.create(request)), selectModel: request => remoteResult(() => direct.selectModel(request)), + modelCatalog: () => remoteResult(() => direct.modelCatalog()), rename: request => remoteResult(() => direct.rename(request)), fork: request => remoteResult(() => direct.fork(request)), prompt: (request, signal = new AbortController().signal) => remoteResult( diff --git a/packages/api/settings-controller/tests/settings-controller.host.spec.ts b/packages/api/settings-controller/tests/settings-controller.host.spec.ts index 4b3b973eae..d43232ae62 100644 --- a/packages/api/settings-controller/tests/settings-controller.host.spec.ts +++ b/packages/api/settings-controller/tests/settings-controller.host.spec.ts @@ -1,6 +1,11 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' +import { + InvalidPresetIdError, + PresetExistsError, + UnknownPresetError, +} from '@deepseek-ai/dsh-agent-presets' import { settingsNamespace } from '@deepseek-ai/dsh-settings' import type { SettingsDescriptor, SettingsNamespace } from '@deepseek-ai/dsh-settings' import { TypertRemoteFailure, remoteMethods } from '@deepseek-ai/dsh-typert-protocol' @@ -306,6 +311,32 @@ describe('the settings Remote namespace a configuration page calls', () => { }) }) + it('classifies cancellation while preparing or opening the settings document', async () => { + const preparing = new Context() + await preparing.plugin(DocumentSettings) + const prepareAbort = new AbortController() + vi.spyOn(preparing.settings, 'prepareDocument').mockImplementation(async () => { + prepareAbort.abort(new Error('cancelled')) + throw new Error('preparation stopped') + }) + const preparingController = new SettingsController(preparing) + await expect(preparingController.openSettingsDocument(prepareAbort.signal)) + .rejects.toMatchObject({ failure: { code: 'cancelled' } }) + + const opening = new Context() + await opening.plugin(DocumentSettings) + vi.spyOn(opening.settings, 'prepareDocument').mockResolvedValue('/tmp/settings.yaml') + const openAbort = new AbortController() + const openingController = new SettingsController(opening, {}, { + openTextFile: async () => { + openAbort.abort(new Error('cancelled')) + throw new Error('opening stopped') + }, + }) + await expect(openingController.openSettingsDocument(openAbort.signal)) + .rejects.toMatchObject({ failure: { code: 'cancelled' } }) + }) + it('opens a user Agent preset directory or returns its path without a native opener', async () => { const ctx = new Context() ctx.provide('agentPresets', { @@ -331,6 +362,21 @@ describe('the settings Remote namespace a configuration page calls', () => { .resolves.toEqual({ opened: false, path: '/presets/mine' }) }) + it('covers native-open detection defaults and explicit overrides', () => { + const fromInjectedOpener = new SettingsController(new Context(), {}, { + openPath: () => Promise.resolve(), + }) + expect((fromInjectedOpener as unknown as { canOpenPath: () => boolean }).canOpenPath()).toBe(true) + + const detected = new SettingsController(new Context()) + expect(typeof (detected as unknown as { canOpenPath: () => boolean }).canOpenPath()).toBe('boolean') + + const override = vi.fn(() => false) + const overridden = new SettingsController(new Context(), {}, { canOpenPath: override }) + expect((overridden as unknown as { canOpenPath: () => boolean }).canOpenPath()).toBe(false) + expect(override).toHaveBeenCalledOnce() + }) + it('refuses a shipped Agent preset and a missing preset provider', async () => { const ctx = new Context() ctx.provide('agentPresets', { @@ -346,4 +392,54 @@ describe('the settings Remote namespace a configuration page calls', () => { await expect(missing.openAgentPresetDirectory('mine', new AbortController().signal)) .rejects.toMatchObject({ failure: { code: 'agent-preset-not-found' } }) }) + + it('rejects an empty Agent preset id before resolving a provider', async () => { + const resolve = vi.fn() + const ctx = new Context() + ctx.provide('agentPresets', { resolve } as never) + const controller = new SettingsController(ctx) + + await expect(controller.openAgentPresetDirectory('', new AbortController().signal)) + .rejects.toMatchObject({ failure: { code: 'bad-request' } }) + expect(resolve).not.toHaveBeenCalled() + }) + + it.each([ + [new UnknownPresetError('missing', ['standard']), 'agent-preset-not-found'], + [new InvalidPresetIdError('../bad'), 'agent-preset-invalid'], + [new PresetExistsError('taken'), 'agent-preset-invalid'], + [new TypertRemoteFailure({ code: 'cancelled', message: 'cancelled', details: {} }), 'cancelled'], + ['unexpected preset failure', 'internal'], + ] as const)('maps Agent preset resolution failure %#', async (error, code) => { + const ctx = new Context() + ctx.provide('agentPresets', { resolve: () => Promise.reject(error) } as never) + const controller = new SettingsController(ctx) + + await expect(controller.openAgentPresetDirectory('mine', new AbortController().signal)) + .rejects.toMatchObject({ failure: { code } }) + }) + + it('classifies cancellation and non-Error failures from the preset opener', async () => { + const ctx = new Context() + ctx.provide('agentPresets', { + resolve: (id: string) => Promise.resolve({ + id, trust: 'user', path: `/presets/${id}/agent.cordis.yml`, + }), + } as never) + const abort = new AbortController() + const openPath = vi.fn() + .mockImplementationOnce(async () => { + abort.abort(new Error('cancelled')) + throw new Error('opening stopped') + }) + .mockRejectedValueOnce('desktop unavailable') + const controller = new SettingsController(ctx, { nativeOpen: true }, { openPath }) + + await expect(controller.openAgentPresetDirectory('first', abort.signal)) + .rejects.toMatchObject({ failure: { code: 'cancelled' } }) + await expect(controller.openAgentPresetDirectory('second', new AbortController().signal)) + .rejects.toMatchObject({ + failure: { code: 'internal', message: 'path open failed: desktop unavailable' }, + }) + }) }) diff --git a/packages/client/ui-settings-models/tests/store.client.spec.ts b/packages/client/ui-settings-models/tests/store.client.spec.ts index 8ce1724b32..5f9aced5bb 100644 --- a/packages/client/ui-settings-models/tests/store.client.spec.ts +++ b/packages/client/ui-settings-models/tests/store.client.spec.ts @@ -181,6 +181,21 @@ describe('ModelsSettingsStore', () => { expect(store.store.getSnapshot().status).toBe('ready') }) + it('surfaces a configurable-provider directory failure', async () => { + const { face, mirror } = api() + const llm = (face as unknown as { + llm: { listConfigurableProviders: () => Promise> } + }).llm + llm.listConfigurableProviders = () => Promise.resolve(remoteFail('configuration directory down')) + const store = new ModelsSettingsStore(face, settingsSchema, mirror) + + await store.load() + + expect(store.store.getSnapshot()).toMatchObject({ + status: 'error', error: 'configuration directory down', + }) + }) + it('lets the newest load win over a stale slow response', async () => { let release: (() => void) | undefined const gate = new Promise((resolve) => { release = resolve }) diff --git a/packages/client/ui-settings-plugins/tests/apply.client.spec.ts b/packages/client/ui-settings-plugins/tests/apply.client.spec.ts index 6d118856ef..6bc979abc6 100644 --- a/packages/client/ui-settings-plugins/tests/apply.client.spec.ts +++ b/packages/client/ui-settings-plugins/tests/apply.client.spec.ts @@ -29,7 +29,7 @@ async function bench(served?: string[]) { ctx.provide('locale', locale) const describeCredentials = vi.fn(() => Promise.resolve({ ok: false, error: { code: 'internal', message: 'no provider', details: {} } })) const models = vi.fn(() => Promise.resolve({ - rpcId: 'm', result: { ok: true, value: { groups: [], failures: [] } }, + ok: true as const, value: { groups: [], failures: [] }, })) const describeSettings = vi.fn(() => Promise.resolve(served === undefined ? { ok: false, error: { code: 'internal', message: 'no provider', details: {} } } @@ -45,11 +45,11 @@ async function bench(served?: string[]) { })) const remote = new TestRemote(ctx, { credentials: { describe: describeCredentials, set: vi.fn() }, + session: { modelCatalog: models }, settings: { describe: describeSettings }, }) ctx.provide('connection', { isLoopback: true, - api: { llm: { models } }, } as never) await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await() return { @@ -66,7 +66,9 @@ function declareRoot(slots: SlotRegistry): () => void { describe('ui-settings-plugins apply', () => { it('declares the services it uses', () => { - expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'remote.credentials', 'settingsScope']) + expect(inject).toEqual([ + 'slots', 'locale', 'connection', 'remote', 'remote.credentials', 'remote.session', 'settingsScope', + ]) }) it('registers one Plugins section and declares the tab and card slots', async () => { diff --git a/packages/client/ui-settings-plugins/tests/stores.client.spec.ts b/packages/client/ui-settings-plugins/tests/stores.client.spec.ts index 28c26e73cd..2fefbb0cc8 100644 --- a/packages/client/ui-settings-plugins/tests/stores.client.spec.ts +++ b/packages/client/ui-settings-plugins/tests/stores.client.spec.ts @@ -65,12 +65,11 @@ function modelsApi(options: { error?: string } = {}) { const models = vi.fn(() => Promise.resolve({ - rpcId: 'm-1' as never, - result: options.error === undefined + ...(options.error === undefined ? { ok: true as const, value: { groups: options.groups ?? [], failures: options.failures ?? [] } } - : { ok: false as const, error: { code: 'internal_error' as never, message: options.error } }, + : { ok: false as const, error: { code: 'internal' as const, message: options.error, details: {} } }), })) - return { api: { llm: { models } } as never, models } + return { api: { modelCatalog: models } as never, models } } function deferred() { @@ -662,15 +661,14 @@ describe('SubagentModelSelectionCardController', () => { const refreshed = deferred() const models = vi.fn() .mockResolvedValueOnce({ - rpcId: 'catalog-1', - result: { ok: true, value: { + ok: true, value: { groups: [{ id: 'alpha', name: 'Alpha', models: [{ id: 'fast', name: 'Fast' }] }], failures: [], - } }, + }, }) .mockImplementationOnce(() => refreshed.promise) const controller = new SubagentModelSelectionCardController( - host.scope, { llm: { models } } as never, + host.scope, { modelCatalog: models } as never, ) const face = controller.inject() const state = () => face.hooks.subagentModelSelectionCard.getSnapshot() @@ -684,8 +682,7 @@ describe('SubagentModelSelectionCardController', () => { candidates: [expect.objectContaining({ key: 'alpha\0fast', selected: true })], }) refreshed.resolve({ - rpcId: 'catalog-2', - result: { ok: true, value: { groups: [], failures: [] } }, + ok: true, value: { groups: [], failures: [] }, } as never) await vi.waitFor(() => { expect(state().catalogStatus).toBe('ready') }) expect(state().candidates).toEqual([ @@ -738,21 +735,19 @@ describe('SubagentModelSelectionCardController', () => { }) const models = vi.fn() .mockResolvedValueOnce({ - rpcId: 'catalog-1', - result: { ok: true, value: { + ok: true, value: { groups: [{ id: 'alpha', name: 'Alpha', models: [{ id: 'fast', name: 'Fast' }] }], failures: [], - } }, + }, }) .mockResolvedValueOnce({ - rpcId: 'catalog-2', - result: { ok: true, value: { + ok: true, value: { groups: [{ id: 'beta', name: 'Beta', models: [{ id: 'new', name: 'New' }] }], failures: [], - } }, + }, }) const controller = new SubagentModelSelectionCardController( - host.scope, { llm: { models } } as never, + host.scope, { modelCatalog: models } as never, ) const state = () => controller.inject().hooks.subagentModelSelectionCard.getSnapshot() await vi.waitFor(() => { expect(state().candidates[0]?.provider).toBe('alpha') }) @@ -807,7 +802,7 @@ describe('SubagentModelSelectionCardController', () => { const pending = deferred() const models = vi.fn(() => pending.promise) - const controller = new SubagentModelSelectionCardController(host.scope, { llm: { models } } as never) + const controller = new SubagentModelSelectionCardController(host.scope, { modelCatalog: models } as never) const face = controller.inject() face.toggleEnabled() face.retryCatalog() @@ -819,14 +814,13 @@ describe('SubagentModelSelectionCardController', () => { const pendingResolve = deferred() const resolving = new SubagentModelSelectionCardController( host.scope, - { llm: { models: () => pendingResolve.promise } } as never, + { modelCatalog: () => pendingResolve.promise } as never, ) const resolvingFace = resolving.inject() resolvingFace.toggleEnabled() resolving.dispose() pendingResolve.resolve({ - rpcId: 'late' as never, - result: { ok: true, value: { groups: [], failures: [] } }, + ok: true, value: { groups: [], failures: [] }, } as never) await pendingResolve.promise }) From 812556040da79dc4a18a50d79bb393508019e84b Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:37:25 +0800 Subject: [PATCH 07/13] fix(api): restore migrated remote coverage --- packages/client/ui-skill/src/client/index.ts | 2 +- .../tests/browser-plugin.client.spec.ts | 2 +- .../host/apiproxy/tests/fetch-carrier.spec.ts | 10 ++++ packages/llm/llm/tests/service.spec.ts | 11 +++- packages/llm/llm/tests/topology.spec.ts | 52 +++++++++++++++++++ 5 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/client/ui-skill/src/client/index.ts b/packages/client/ui-skill/src/client/index.ts index 5b6a9bb44e..36d1e9778c 100644 --- a/packages/client/ui-skill/src/client/index.ts +++ b/packages/client/ui-skill/src/client/index.ts @@ -58,7 +58,7 @@ interface CatalogFetch { } /** Required services: reference source faces plus the tool-row and locale registries. */ -export const inject = ['inputTriggers', 'connection', 'sessions', 'slots', 'locale', 'remote'] +export const inject = ['inputTriggers', 'connection', 'sessions', 'slots', 'locale', 'remote', 'remote.skills'] /** * Client plugin body: register the '/' source, dictionaries, and keyed tool row. diff --git a/packages/client/ui-skill/tests/browser-plugin.client.spec.ts b/packages/client/ui-skill/tests/browser-plugin.client.spec.ts index 208baac3bc..89111675f3 100644 --- a/packages/client/ui-skill/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-skill/tests/browser-plugin.client.spec.ts @@ -102,7 +102,7 @@ const req = (query: string, signal?: AbortSignal) => describe('apply', () => { it('declares the services it binds', () => { - expect(inject).toEqual(['inputTriggers', 'connection', 'sessions', 'slots', 'locale', 'remote']) + expect(inject).toEqual(['inputTriggers', 'connection', 'sessions', 'slots', 'locale', 'remote', 'remote.skills']) }) it('registers the dedicated skill row and its locale dictionaries', async () => { diff --git a/packages/host/apiproxy/tests/fetch-carrier.spec.ts b/packages/host/apiproxy/tests/fetch-carrier.spec.ts index 2cf89e675d..844c4d5039 100644 --- a/packages/host/apiproxy/tests/fetch-carrier.spec.ts +++ b/packages/host/apiproxy/tests/fetch-carrier.spec.ts @@ -81,6 +81,16 @@ describe('handler carrier-layer statuses', () => { expect(parsed.result.error?.details.issues.length).toBeGreaterThan(0) }) + it('rejects a request whose envelope method does not match its path', async () => { + const body = JSON.stringify({ type: 'client-request', rpcId: 'r-mismatch', method: 'other.method', payload: {} }) + const response = await handler.fetch(new Request('http://x/api/host.describe', { method: 'POST', headers: { 'content-type': 'application/json' }, body })) + const parsed = await response.json() as { result: { error?: { code: string; message: string } } } + expect(parsed.result.error).toMatchObject({ + code: 'bad-request', + message: 'method "other.method" does not match path "host.describe"', + }) + }) + it('500s when the impl itself throws', async () => { const crashing = toFetchHandler(fakeApi({ crashOn: 'host.describe' })) const body = JSON.stringify({ type: 'client-request', rpcId: 'r-11', method: 'host.describe', payload: {} }) diff --git a/packages/llm/llm/tests/service.spec.ts b/packages/llm/llm/tests/service.spec.ts index 4c624fa862..47309daea1 100644 --- a/packages/llm/llm/tests/service.spec.ts +++ b/packages/llm/llm/tests/service.spec.ts @@ -525,13 +525,20 @@ describe('LlmRuntime', () => { const ctx = new Context() await ctx.plugin(LlmRuntime) const provider = { id: 'catalog', name: 'Catalog Provider' } - const model = { provider: 'catalog', id: 'fast', name: 'Fast', description: 'Low latency' } + const model = { + provider: 'catalog', + id: 'fast', + name: 'Fast', + description: 'Low latency', + inputModalities: ['text'] as const, + } ctx.llm.registerAdapter(['catalog'], new CatalogAdapter(provider, [model])) const providers = ctx.llm.listProviders() const models = await ctx.llm.listModels('catalog') expect(providers).toEqual([provider]) expect(models).toEqual([model]) + expect(models[0]!.inputModalities).not.toBe(model.inputModalities) providers[0]!.name = 'mutated' models[0]!.name = 'mutated' @@ -539,7 +546,7 @@ describe('LlmRuntime', () => { model.name = 'source mutated' expect(ctx.llm.listProviders()).toEqual([{ id: 'catalog', name: 'Catalog Provider' }]) await expect(ctx.llm.listModels('catalog')).resolves.toEqual([{ - provider: 'catalog', id: 'fast', name: 'source mutated', description: 'Low latency', + provider: 'catalog', id: 'fast', name: 'source mutated', description: 'Low latency', inputModalities: ['text'], }]) }) diff --git a/packages/llm/llm/tests/topology.spec.ts b/packages/llm/llm/tests/topology.spec.ts index 8759e5ba69..0fbe8255e6 100644 --- a/packages/llm/llm/tests/topology.spec.ts +++ b/packages/llm/llm/tests/topology.spec.ts @@ -250,6 +250,58 @@ describe('model discovery registry', () => { ]) }) + it('carries cancellation into Remote discovery and maps provider failures', async () => { + const ctx = await setup() + const discover = vi.fn() + .mockResolvedValueOnce([ + { id: 'keep', name: 'Keep', contextWindow: 1024, maxTokens: 256 }, + { id: '' }, + { id: 'keep' }, + { id: 'bare' }, + ]) + .mockRejectedValueOnce(new Error('endpoint offline')) + .mockRejectedValueOnce('provider refused') + ctx.llm.registerModelDiscovery('llm-example', discover) + const signal = new AbortController().signal + + await expect(ctx.llm.remoteDiscoverModels( + 'llm-example', + { baseURL: 'https://gateway.example/v1' }, + signal, + )).resolves.toEqual([ + { id: 'keep', name: 'Keep', contextWindow: 1024, maxTokens: 256 }, + { id: 'bare' }, + ]) + expect(discover).toHaveBeenNthCalledWith( + 1, + { baseURL: 'https://gateway.example/v1' }, + signal, + ) + + await expect(ctx.llm.remoteDiscoverModels( + 'llm-example', + { baseURL: 'https://gateway.example/v1' }, + signal, + )).rejects.toMatchObject({ + failure: { + code: 'model-discovery-failed', + message: 'endpoint offline', + details: { settingsNs: 'llm-example', baseURL: 'https://gateway.example/v1' }, + }, + }) + await expect(ctx.llm.remoteDiscoverModels( + 'llm-example', + { provider: 'known-route' }, + signal, + )).rejects.toMatchObject({ + failure: { + code: 'model-discovery-failed', + message: 'provider refused', + details: { settingsNs: 'llm-example' }, + }, + }) + }) + it('refuses a namespace nothing serves and a draft with no endpoint', async () => { const ctx = await setup() ctx.llm.registerModelDiscovery('llm-example', () => Promise.resolve([])) From 5f6293e67a4cc52085db948b163a8aa4d00e832b Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:48:58 +0800 Subject: [PATCH 08/13] test(client): update remote session fixtures --- .../tests/assembly-surfaces.client.spec.tsx | 10 +++++--- .../tests/chat-code-subcalls.client.spec.tsx | 11 ++++---- .../tests/toolview-slot.client.spec.tsx | 25 +++++++++---------- .../tests/api-catalog.client.spec.ts | 1 - 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx b/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx index f6e0ef3098..63cea1a148 100644 --- a/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx +++ b/packages/client/ui-tool/tests/assembly-surfaces.client.spec.tsx @@ -10,7 +10,7 @@ import { apply as applyChat, inject as injectChat, type ToolResultNode, } from '@deepseek-ai/dsh-client-ui-chat/client' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' -import { SlotTestRuntime, usePinnedBrowserLanguages, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, TestRemote, usePinnedBrowserLanguages, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { apply as applyConversation, inject as injectConversation } from '@deepseek-ai/dsh-client-ui-conversation/client' import { apply as applyTool, inject as injectTool } from '../src/client/apply.ts' import { toolSessionEvents } from './tool-details-render.client.tsx' @@ -76,13 +76,15 @@ async function bench(nodes: ToolResultNode[]) { isLoopback: false, hostDescription: { getSnapshot: () => undefined, subscribe: () => () => {} }, }) - // ui-theme's Appearance row binds a durable scope through these two. - runtime.ctx.provide('remote', { $on: () => () => {} }) + new TestRemote(runtime.ctx, { + session: { + openWorkspacePath: vi.fn(async () => ({ ok: true, value: { opened: true } })), + }, + }) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID), - openPath: vi.fn(async () => {}), } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) diff --git a/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx b/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx index d2f8ccec8c..be9061fbc2 100644 --- a/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx +++ b/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx @@ -7,7 +7,7 @@ import type { ChatSnapshot, RunningToolCall, ToolCallBlock, ToolResultNode, } from '@deepseek-ai/dsh-client-ui-chat/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' -import { SlotTestRuntime, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, TestRemote, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import { @@ -116,9 +116,10 @@ async function bench(snapshot: ChatSnapshot) { snapshot: { running: snapshot.legacy.runningCalls.length > 0 }, }) const layout = { openDetails: vi.fn(), closeDetails: vi.fn() } - const openPath = vi.fn(async () => {}) + const openWorkspacePath = vi.fn(async () => ({ ok: true, value: { opened: true } })) ctx.provide('layout', layout as never) - ctx.provide('uiWorkspace', { openPath } as never) + ctx.provide('uiWorkspace', {} as never) + new TestRemote(ctx, { session: { openWorkspacePath } }) ctx.provide('connection', { api: { settings: {} }, isLoopback: false, @@ -132,7 +133,7 @@ async function bench(snapshot: ChatSnapshot) { await runtime.root.declare(ROOT_CHILDREN, AppRoot) await runtime.mount({ inject: [...injectChat], apply: applyChat }) await runtime.mount({ inject: [...injectTool], apply: applyTool }) - return { runtime, layout, openPath } + return { runtime, layout, openWorkspacePath } } function mountApp(runtime: SlotTestRuntime) { @@ -222,7 +223,7 @@ describe('run_code sub-calls through the real chat machinery', () => { view.getByText('notes/demo.txt').click() expect(b.layout.openDetails).not.toHaveBeenCalled() await vi.waitFor(() => { - expect(b.openPath).toHaveBeenCalledWith('notes/demo.txt') + expect(b.openWorkspacePath).toHaveBeenCalledWith({ sessionId: SID, path: 'notes/demo.txt' }) }) view.getByText('List notes').click() expect(b.layout.openDetails).not.toHaveBeenCalled() diff --git a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx index 54a165cf40..ad5eb90a10 100644 --- a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx +++ b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx @@ -8,7 +8,7 @@ import { apply as applyChat, inject as injectChat, type ToolResultNode, } from '@deepseek-ai/dsh-client-ui-chat/client' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' -import { SlotTestRuntime, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, TestRemote, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import { apply as applyConversation, inject as injectConversation } from '@deepseek-ai/dsh-client-ui-conversation/client' import { apply as applyTool, inject as injectTool } from '@deepseek-ai/dsh-client-ui-tool/client' @@ -64,16 +64,13 @@ async function bench(nodes: ToolResultNode[]) { isLoopback: false, hostDescription: { getSnapshot: () => undefined, subscribe: () => () => {} }, }) - // ui-theme's Appearance row binds a durable scope through these two. - runtime.ctx.provide('remote', { $on: () => () => {} }) + const openWorkspacePath = vi.fn(async () => ({ ok: true, value: { opened: true } })) + new TestRemote(runtime.ctx, { session: { openWorkspacePath } }) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const layout = { openDetails: vi.fn(), closeDetails: vi.fn() } runtime.ctx.provide('layout', layout) runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID), - openPath: async (path: string) => { - runtime.workspaces.calls.push({ method: 'openPath', args: [path] }) - }, } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) @@ -91,7 +88,7 @@ async function bench(nodes: ToolResultNode[]) { await runtime.mount({ inject: [...injectConversation], apply: applyConversation }) await runtime.mount({ inject: [...injectChat], apply: applyChat }) await runtime.mount({ inject: [...injectTool], apply: applyTool }) - return { runtime, slots: runtime.slots, layout } + return { runtime, slots: runtime.slots, layout, openWorkspacePath } } describe('keyed toolview hole through the real machinery', () => { @@ -134,13 +131,13 @@ describe('keyed toolview hole through the real machinery', () => { await b.runtime.dispose() }) - it('file-path clicks travel owner openFile → chat inject → workspaces.openPath', async () => { + it('file-path clicks travel owner openFile → chat inject → session.openWorkspacePath', async () => { const b = await bench([toolResult(3, 'c1', 'read', '{"path":"src/a.ts"}')]) const view = b.runtime.renderRoot() view.getByText('src/a.ts').click() expect(b.layout.openDetails).not.toHaveBeenCalled() await vi.waitFor(() => { - expect(b.runtime.workspaces.calls).toContainEqual({ method: 'openPath', args: ['src/a.ts'] }) + expect(b.openWorkspacePath).toHaveBeenCalledWith({ sessionId: SID, path: 'src/a.ts' }) }) await b.runtime.dispose() }) @@ -150,7 +147,7 @@ describe('keyed toolview hole through the real machinery', () => { const view = b.runtime.renderRoot() view.getByText('Build').click() expect(b.layout.openDetails).not.toHaveBeenCalled() - expect(b.runtime.workspaces.calls.some(c => c.method === 'openPath')).toBe(false) + expect(b.openWorkspacePath).not.toHaveBeenCalled() await b.runtime.dispose() }) @@ -214,13 +211,15 @@ describe('registrant declaration injection', () => { isLoopback: false, hostDescription: { getSnapshot: () => undefined, subscribe: () => () => {} }, }) - // ui-theme's Appearance row binds a durable scope through these two. - runtime.ctx.provide('remote', { $on: () => () => {} }) + new TestRemote(runtime.ctx, { + session: { + openWorkspacePath: vi.fn(async () => ({ ok: true, value: { opened: true } })), + }, + }) runtime.ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) runtime.ctx.provide('uiWorkspace', { connectWorkspace: vi.fn(async () => SID), - openPath: vi.fn(async () => {}), } as never) const locale = new LocaleRuntime(runtime.ctx) runtime.ctx.provide('locale', locale) diff --git a/packages/extensions/cordis-client-runner/tests/api-catalog.client.spec.ts b/packages/extensions/cordis-client-runner/tests/api-catalog.client.spec.ts index 019d34ac90..3dbfa80b05 100644 --- a/packages/extensions/cordis-client-runner/tests/api-catalog.client.spec.ts +++ b/packages/extensions/cordis-client-runner/tests/api-catalog.client.spec.ts @@ -19,7 +19,6 @@ describe('Client Cordis inspect catalog', () => { 'pickDirectory(): Promise', 'listDirectory(path?: string, signal?: AbortSignal): Promise', 'createDirectory(path: string, name: string): Promise', - 'openPath(path: string): Promise', ]) }) From 89ee54ebb7e094314530272ff13ea9470b5e1549 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:00:42 +0800 Subject: [PATCH 09/13] test(api): align migrated client contracts --- .../session-open-workspace-path.host.spec.ts | 8 +++---- .../tests/session-skills.host.spec.ts | 21 ++++++++----------- packages/api/settings-controller/src/index.ts | 13 ++++++++---- .../tests/settings-controller.host.spec.ts | 12 ++++++----- .../client/connection/src/client/fixture.ts | 6 ++---- .../tests/stores.client.spec.ts | 8 +++---- 6 files changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts index ca7a78c589..2727fb96c2 100644 --- a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts +++ b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts @@ -114,11 +114,11 @@ describe('session/openWorkspacePath', () => { }, aborted.signal)).rejects.toMatchObject({ failure: { code: 'cancelled' } }) inspect.mockRejectedValueOnce('storage offline') - await expect(controller.openWorkspacePath({ + const failed = controller.openWorkspacePath({ sessionId: SessionId('inspection-failed'), path: 'result.html', - }, new AbortController().signal)).rejects.toMatchObject({ - failure: { code: 'internal', message: expect.stringContaining('storage offline') }, - }) + }, new AbortController().signal) + await expect(failed).rejects.toMatchObject({ failure: { code: 'internal' } }) + await expect(failed).rejects.toThrow('storage offline') }) it('classifies opener cancellation and non-Error failures', async () => { diff --git a/packages/api/session-controller/tests/session-skills.host.spec.ts b/packages/api/session-controller/tests/session-skills.host.spec.ts index 32d9957d4b..5b5e1b2a5a 100644 --- a/packages/api/session-controller/tests/session-skills.host.spec.ts +++ b/packages/api/session-controller/tests/session-skills.host.spec.ts @@ -183,10 +183,9 @@ describe('SessionSkillCatalog', () => { } as never) const catalog = new SessionSkillCatalog(ctx) - await expect(catalog.list({ sessionId }, new AbortController().signal)) - .rejects.toMatchObject({ - failure: { code: 'internal', message: expect.stringContaining('skill registry is absent') }, - }) + const failed = catalog.list({ sessionId }, new AbortController().signal) + await expect(failed).rejects.toMatchObject({ failure: { code: 'internal' } }) + await expect(failed).rejects.toThrow('skill registry is absent') }) it('rejects observations without projections or a project cwd', async () => { @@ -199,14 +198,12 @@ describe('SessionSkillCatalog', () => { ctx.provide('sessionQuery', { observeSession } as never) const catalog = new SessionSkillCatalog(ctx) - await expect(catalog.list({ sessionId }, new AbortController().signal)) - .rejects.toMatchObject({ - failure: { code: 'internal', message: expect.stringContaining('projected Session observation') }, - }) - await expect(catalog.list({ sessionId }, new AbortController().signal)) - .rejects.toMatchObject({ - failure: { code: 'internal', message: expect.stringContaining('has no project cwd') }, - }) + const unprojected = catalog.list({ sessionId }, new AbortController().signal) + await expect(unprojected).rejects.toMatchObject({ failure: { code: 'internal' } }) + await expect(unprojected).rejects.toThrow('projected Session observation') + const cwdless = catalog.list({ sessionId }, new AbortController().signal) + await expect(cwdless).rejects.toMatchObject({ failure: { code: 'internal' } }) + await expect(cwdless).rejects.toThrow('has no project cwd') }) it('classifies a provider listing failure', async () => { diff --git a/packages/api/settings-controller/src/index.ts b/packages/api/settings-controller/src/index.ts index a61dfd329e..393a1dec81 100644 --- a/packages/api/settings-controller/src/index.ts +++ b/packages/api/settings-controller/src/index.ts @@ -37,6 +37,11 @@ export type * from './types.ts' const settingsNamespaceRequestSchema = z.object({ ns: z.string().min(1) }) +/** Read abort state afresh after an awaited provider or opener call. */ +function isAborted(signal: AbortSignal): boolean { + return signal.aborted +} + /** Native document-opening policy. */ export interface Config { /** Override platform desktop-opener detection. */ @@ -185,23 +190,23 @@ export class SettingsController extends TypertRemoteService { @Remote async openSettingsDocument(signal: AbortSignal): Promise { const settings = this.provider() - if (signal.aborted) throw cancelled('settings document open was aborted') + if (isAborted(signal)) throw cancelled('settings document open was aborted') let path: string | undefined try { path = await settings.prepareDocument() } catch (error: unknown) { - if (signal.aborted) throw cancelled('settings document preparation was aborted') + if (isAborted(signal)) throw cancelled('settings document preparation was aborted') throw internal(`settings document preparation failed: ${messageOf(error)}`) } if (path === undefined) { throw internal('settings provider has no local document to open') } - if (signal.aborted) throw cancelled('settings document open was aborted') + if (isAborted(signal)) throw cancelled('settings document open was aborted') try { await this.openTextFile(path, signal) return { opened: true } } catch (error: unknown) { - if (signal.aborted) throw cancelled('settings document open was aborted') + if (isAborted(signal)) throw cancelled('settings document open was aborted') throw internal(`path open failed: ${messageOf(error)}`) } } diff --git a/packages/api/settings-controller/tests/settings-controller.host.spec.ts b/packages/api/settings-controller/tests/settings-controller.host.spec.ts index d43232ae62..aa8945b4fc 100644 --- a/packages/api/settings-controller/tests/settings-controller.host.spec.ts +++ b/packages/api/settings-controller/tests/settings-controller.host.spec.ts @@ -263,13 +263,15 @@ describe('the settings Remote namespace a configuration page calls', () => { it('preserves settings-document absence, failure, and cancellation', async () => { const absent = await boot() - await expect(absent.controller.openSettingsDocument(new AbortController().signal)) - .rejects.toMatchObject({ failure: { code: 'internal', message: expect.stringContaining('no local document') } }) + const missingDocument = absent.controller.openSettingsDocument(new AbortController().signal) + await expect(missingDocument).rejects.toMatchObject({ failure: { code: 'internal' } }) + await expect(missingDocument).rejects.toThrow('no local document') const failed = await boot(DocumentSettings) vi.spyOn(failed.ctx.settings, 'prepareDocument').mockRejectedValue(new Error('read failed')) - await expect(failed.controller.openSettingsDocument(new AbortController().signal)) - .rejects.toMatchObject({ failure: { code: 'internal', message: expect.stringContaining('read failed') } }) + const failedRead = failed.controller.openSettingsDocument(new AbortController().signal) + await expect(failedRead).rejects.toMatchObject({ failure: { code: 'internal' } }) + await expect(failedRead).rejects.toThrow('read failed') const cancelled = new AbortController() cancelled.abort(new Error('cancelled')) @@ -412,7 +414,7 @@ describe('the settings Remote namespace a configuration page calls', () => { ['unexpected preset failure', 'internal'], ] as const)('maps Agent preset resolution failure %#', async (error, code) => { const ctx = new Context() - ctx.provide('agentPresets', { resolve: () => Promise.reject(error) } as never) + ctx.provide('agentPresets', { resolve: async () => { throw error } } as never) const controller = new SettingsController(ctx) await expect(controller.openAgentPresetDirectory('mine', new AbortController().signal)) diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index a5689069c2..e39c39db01 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -3657,12 +3657,10 @@ export class FixtureApiClient extends AbstractApiClient { /** Method-key dispatch into the in-memory contract impl (a real carrier routes by URL path instead). */ private dispatch( - method: keyof RpcMethodMap, + _method: keyof RpcMethodMap, request: RpcRequest, ): Promise> { - switch (method) { - case 'host.describe': return this.api.host.describe(request) - } + return this.api.host.describe(request) } } diff --git a/packages/client/ui-settings-plugins/tests/stores.client.spec.ts b/packages/client/ui-settings-plugins/tests/stores.client.spec.ts index 2fefbb0cc8..ffdb0d8cf3 100644 --- a/packages/client/ui-settings-plugins/tests/stores.client.spec.ts +++ b/packages/client/ui-settings-plugins/tests/stores.client.spec.ts @@ -668,7 +668,7 @@ describe('SubagentModelSelectionCardController', () => { }) .mockImplementationOnce(() => refreshed.promise) const controller = new SubagentModelSelectionCardController( - host.scope, { modelCatalog: models } as never, + host.scope, { modelCatalog: models }, ) const face = controller.inject() const state = () => face.hooks.subagentModelSelectionCard.getSnapshot() @@ -747,7 +747,7 @@ describe('SubagentModelSelectionCardController', () => { }, }) const controller = new SubagentModelSelectionCardController( - host.scope, { modelCatalog: models } as never, + host.scope, { modelCatalog: models }, ) const state = () => controller.inject().hooks.subagentModelSelectionCard.getSnapshot() await vi.waitFor(() => { expect(state().candidates[0]?.provider).toBe('alpha') }) @@ -802,7 +802,7 @@ describe('SubagentModelSelectionCardController', () => { const pending = deferred() const models = vi.fn(() => pending.promise) - const controller = new SubagentModelSelectionCardController(host.scope, { modelCatalog: models } as never) + const controller = new SubagentModelSelectionCardController(host.scope, { modelCatalog: models }) const face = controller.inject() face.toggleEnabled() face.retryCatalog() @@ -814,7 +814,7 @@ describe('SubagentModelSelectionCardController', () => { const pendingResolve = deferred() const resolving = new SubagentModelSelectionCardController( host.scope, - { modelCatalog: () => pendingResolve.promise } as never, + { modelCatalog: () => pendingResolve.promise }, ) const resolvingFace = resolving.inject() resolvingFace.toggleEnabled() From 72cf4fae83e67c5f042afd3de79b366eeb271e47 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:07:24 +0800 Subject: [PATCH 10/13] fix(settings): preserve config catalog source anchor --- packages/api/settings-controller/src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/api/settings-controller/src/index.ts b/packages/api/settings-controller/src/index.ts index 393a1dec81..e822d7f489 100644 --- a/packages/api/settings-controller/src/index.ts +++ b/packages/api/settings-controller/src/index.ts @@ -37,17 +37,17 @@ export type * from './types.ts' const settingsNamespaceRequestSchema = z.object({ ns: z.string().min(1) }) -/** Read abort state afresh after an awaited provider or opener call. */ -function isAborted(signal: AbortSignal): boolean { - return signal.aborted -} - /** Native document-opening policy. */ export interface Config { /** Override platform desktop-opener detection. */ readonly nativeOpen?: boolean } +/** Read abort state afresh after an awaited provider or opener call. */ +function isAborted(signal: AbortSignal): boolean { + return signal.aborted +} + /** Host integrations replaceable by direct unit tests. */ export interface SettingsControllerInternals { readonly openPath?: (path: string, signal: AbortSignal) => Promise From 18ae39a6658d43cebfc4df859438e8df86898625 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 21:47:37 +0800 Subject: [PATCH 11/13] fix(api): preserve native path opening behavior --- ...-unary-apiproxy-remote-migration.i18n.yaml | 4 +- ...6-08-10-unary-apiproxy-remote-migration.md | 8 +-- ...8-10-unary-apiproxy-remote-migration.zh.md | 8 +-- ...-07-28-tool-call-file-open-in-os.i18n.yaml | 4 +- .../2026-07-28-tool-call-file-open-in-os.md | 2 +- ...2026-07-28-tool-call-file-open-in-os.zh.md | 2 +- apps/web/tests/produced-files.e2e.ts | 2 +- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- docs/event-producer-consumer.i18n.yaml | 4 +- docs/event-producer-consumer.md | 10 +-- docs/event-producer-consumer.zh.md | 10 +-- docs/subsystems/session.i18n.yaml | 4 +- docs/subsystems/session.md | 10 +-- docs/subsystems/session.zh.md | 10 +-- knip.json | 5 -- packages/api/session-controller/src/index.ts | 34 ++-------- packages/api/session-controller/src/types.ts | 5 +- .../session-open-workspace-path.host.spec.ts | 66 ++++--------------- .../api/session-controller/tsconfig.host.json | 1 - .../client/connection/src/client/fixture.ts | 4 +- packages/client/ui-chat/package.json | 4 +- packages/client/ui-chat/src/client/apply.ts | 6 +- .../tests/apply-inject.client.spec.tsx | 2 +- packages/client/ui-chat/tsconfig.json | 3 + .../tests/chat-code-subcalls.client.spec.tsx | 2 +- .../tests/toolview-slot.client.spec.tsx | 2 +- .../extensions/tool-cordis/src/api-catalog.ts | 8 +-- packages/host/apiproxy/src/api-proxy.ts | 4 +- pnpm-lock.yaml | 3 + 31 files changed, 85 insertions(+), 150 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml index 617d6b07db..c316742ec1 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.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/architecture/2026-08-10-unary-apiproxy-remote-migration.md -2026-08-10-unary-apiproxy-remote-migration.md: 027376cbf772043cce21f487c94288cad6bece1c -2026-08-10-unary-apiproxy-remote-migration.zh.md: f7507959a992dd17ca60883ada7769c7196fdc3a +2026-08-10-unary-apiproxy-remote-migration.md: 11254099556113d921da502f0150522886a718f3 +2026-08-10-unary-apiproxy-remote-migration.zh.md: 50b303876863e992566f6ed6fb0bd0a89326344f diff --git a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md index 027376cbf7..1125409955 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md +++ b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.md @@ -8,7 +8,7 @@ English | [中文](2026-08-10-unary-apiproxy-remote-migration.zh.md) The Host API Proxy duplicated simple unary operations across business Services, API Proxy interfaces, Zod schemas, route tables, client stubs, and Client callers. [Typert Remote calls](2026-08-02-typert-remote-method-calls.md) already let a business package own this class of call, but moving an endpoint without its lifecycle and projection policy could change observable behavior. -Agent-bound calls require particular care. Shared lookup policy reuses live Agents, resumes ordinary cold Sessions with their recorded presets, deduplicates concurrent resumes, and rejects subagent-owned identities. Skill listing instead must inspect a Session without activating its Agent. Native desktop operations must keep the browser from choosing an arbitrary Host path. +Agent-bound calls require particular care. Shared lookup policy reuses live Agents, resumes ordinary cold Sessions with their recorded presets, deduplicates concurrent resumes, and rejects subagent-owned identities. Skill listing instead must inspect a Session without activating its Agent. Settings and preset operations keep their Host-owned document paths out of browser requests; Session file links preserve their caller-resolved path behavior. ## Decision @@ -30,11 +30,11 @@ Simple unary operations live on their natural business Remote owner. The busines | `workspace.list`, `workspace.insertSessionBefore`, `workspace.archiveSession` | Equivalent `workspace/*` methods | The Workspace registry owns detached snapshots and serialized mutations. | | `skill.list` | `skills/list` | `SessionSkillCatalog` observes the Session and its recorded preset, uses a live Agent only when one already exists, and never activates an Agent for listing. | | `fileReferences/list` | `fileReferences/list` | `SessionFileReferences` supplies the Session Controller's established Agent lookup to the provider; cold lookup behavior remains unchanged. | -| `host.openPath` | `session/openWorkspacePath` | `SessionController` resolves the path against the addressed Session's workspace before native opening. | +| `host.openPath` | `session/openWorkspacePath` | The Session-aware Client resolves relative paths against the known workspace before `SessionController` hands them to the native opener. | The shared Agent and Session resolver remains the authority for endpoints that accept those objects. It provides the same live reuse, cold restoration, concurrent deduplication, preset setup, persistence failures, and subagent ownership fence that legacy API Proxy calls used. `TypertLookupFailure` preserves resolver-owned RPC errors instead of collapsing them into `internal`. -The native path implementation lives in `@deepseek-ai/dsh-native-command`. Session and Settings controllers select the target; the utility only performs platform detection, WSL translation, browser preference, text-editor intent, and shell-free command execution. +The native path implementation lives in `@deepseek-ai/dsh-native-command`. Settings controllers select Host-owned targets, while Session-aware Clients resolve workspace paths before calling `SessionController`; the utility only performs platform detection, WSL translation, browser preference, text-editor intent, and shell-free command execution. ## Browser authentication @@ -50,7 +50,7 @@ Focused Host and Client tests cover Remote calls, lookup and no-activation polic **Move every unary operation.** Rejected because `host.describe` combines deployment facts and Connection readiness, while Session export is a streamed download rather than a unary business method. -**Put native opening in one controller.** Rejected because Session, Settings, and the retained Host description consume the same platform operation. A Host utility avoids controller-to-controller imports without making the browser authoritative for filesystem targets. +**Put native opening in one controller.** Rejected because Session, Settings, and the retained Host description consume the same platform operation. A Host utility avoids controller-to-controller imports and duplicated platform logic. ## Consequences diff --git a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md index f7507959a9..50b3038768 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-10-unary-apiproxy-remote-migration.zh.md @@ -8,7 +8,7 @@ Status: implemented Host API Proxy 曾在业务 Service、API Proxy interface、Zod schema、路由表、Client stub 与 Client 调用方之间重复定义简单一元操作。[Typert Remote 调用](2026-08-02-typert-remote-method-calls.zh.md)已经允许业务包持有这类调用,但如果迁移 endpoint 时没有一并保留生命周期与投影策略,就会改变可观察行为。 -与 Agent 绑定的调用需要格外谨慎。共享 lookup 策略会复用 live Agent、用记录的 preset 恢复普通冷 Session、对并发恢复去重,并拒绝由 subagent 持有的 identity。skill 列表则必须检查 Session 而不激活 Agent。原生桌面操作必须避免让浏览器选择任意 Host 路径。 +与 Agent 绑定的调用需要格外谨慎。共享 lookup 策略会复用 live Agent、用记录的 preset 恢复普通冷 Session、对并发恢复去重,并拒绝由 subagent 持有的 identity。skill 列表则必须检查 Session 而不激活 Agent。Settings 与 preset 操作不会把 Host 持有的文档路径放进浏览器请求;Session 文件链接保留由调用方解析路径的行为。 ## 决策 @@ -30,11 +30,11 @@ Host API Proxy 曾在业务 Service、API Proxy interface、Zod schema、路由 | `workspace.list`、`workspace.insertSessionBefore`、`workspace.archiveSession` | 对应的 `workspace/*` 方法 | Workspace registry 持有脱离可变对象的 snapshot 与串行 mutation。 | | `skill.list` | `skills/list` | `SessionSkillCatalog` 观察 Session 及其记录的 preset,仅在 live Agent 已存在时使用它,列表查询绝不激活 Agent。 | | `fileReferences/list` | `fileReferences/list` | `SessionFileReferences` 向 provider 提供 Session Controller 的既有 Agent lookup;冷 lookup 行为保持不变。 | -| `host.openPath` | `session/openWorkspacePath` | `SessionController` 先基于目标 Session 的 workspace 解析路径,再执行原生打开。 | +| `host.openPath` | `session/openWorkspacePath` | Session-aware Client 先基于已知 workspace 解析相对路径,再由 `SessionController` 交给原生打开器。 | 共享 Agent 与 Session resolver 仍是接收这些对象的 endpoint 的权威。它提供与旧 API Proxy 调用相同的 live 复用、冷恢复、并发去重、preset setup、持久化失败与 subagent ownership fence。`TypertLookupFailure` 保留 resolver 持有的 RPC error,而不把它们归并为 `internal`。 -原生路径实现在 `@deepseek-ai/dsh-native-command` 中。Session 与 Settings controller 选择目标;该工具仅负责平台探测、WSL 转换、浏览器偏好、文本编辑器意图与无 shell 命令执行。 +原生路径实现在 `@deepseek-ai/dsh-native-command` 中。Settings controller 选择 Host 持有的目标,Session-aware Client 则在调用 `SessionController` 前解析 workspace 路径;该工具仅负责平台探测、WSL 转换、浏览器偏好、文本编辑器意图与无 shell 命令执行。 ## 浏览器认证 @@ -50,7 +50,7 @@ Connection 在选择 Typert interceptor 或 API Proxy fallback 前认证完整 **迁移每一个一元操作。** 否决,因为 `host.describe` 组合部署事实与 Connection readiness,而 Session export 是流式下载,不是一元业务方法。 -**把原生打开操作放入某个 controller。** 否决,因为 Session、Settings 与保留的 Host 描述都会消费同一平台操作。Host 工具可以避免 controller 间导入,同时不让浏览器成为文件系统目标的权威。 +**把原生打开操作放入某个 controller。** 否决,因为 Session、Settings 与保留的 Host 描述都会消费同一平台操作。Host 工具可以避免 controller 间导入与重复的平台逻辑。 ## 后果 diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml index e97143a911..fa703bf41d 100644 --- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.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-07-28-tool-call-file-open-in-os.md -2026-07-28-tool-call-file-open-in-os.md: c8ede5c5c2fbdc9797edd9bf80673442c39873c2 -2026-07-28-tool-call-file-open-in-os.zh.md: 1e51dd4dced25bd14272358199baef82f396635a +2026-07-28-tool-call-file-open-in-os.md: a8d5bd116b3f4cf1434d44b643dad3d883763a82 +2026-07-28-tool-call-file-open-in-os.zh.md: b486ec0972356419c2df5abbc148dc57a4586920 diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md index c8ede5c5c2..a8d5bd116b 100644 --- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md +++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.md @@ -10,7 +10,7 @@ Chat tool rows treated the whole summary line as a click target that opened the ## Decision -File-tool path summaries (`read` / `write` / `edit` args carrying `path` or `file_path`) render as links underlined at rest with a pointer cursor. Clicking the path calls `session/openWorkspacePath` through the chat view's `openFile` injection; the Host resolves relative paths against the addressed Session's cwd. File-link rows disable args expand (leading icon is inert); whole-row click, row hover fill, and the click-to-open-details gesture are removed from tool rows (including bash and todo registrations). The details panel and its inject surface remain for programmatic selection; rows no longer drive them. +File-tool path summaries (`read` / `write` / `edit` args carrying `path` or `file_path`) render as links underlined at rest with a pointer cursor. Clicking the path calls `session/openWorkspacePath` through the chat view's `openFile` injection; the chat view resolves relative paths against the addressed Session's cwd when it is known. File-link rows disable args expand (leading icon is inert); whole-row click, row hover fill, and the click-to-open-details gesture are removed from tool rows (including bash and todo registrations). The details panel and its inject surface remain for programmatic selection; rows no longer drive them. `session/openWorkspacePath` uses the authenticated Remote carrier, while the product UI offers the gesture only on a loopback page whose `host.describe.canOpenPath` is true. Platform adapters open without a shell: `open` on macOS, PowerShell `Invoke-Item` on Windows, and `xdg-open` on desktop Linux; browser-renderable documents prefer the named default browser on macOS and desktop Linux. WSL is a separate host shape despite Node reporting `linux`: the adapter recognizes its environment or Microsoft kernel release, translates the Linux path with `wslpath -w`, and passes the resulting Windows/UNC path to the same PowerShell handoff. The opener's platform facts and command runner are injectable for tests. URL-only read args (`web_fetch`) are not file links. diff --git a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md index 1e51dd4dce..b486ec0972 100644 --- a/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md +++ b/.agents/notes/implemented/feature/2026-07-28-tool-call-file-open-in-os.zh.md @@ -10,7 +10,7 @@ Status: implemented ## 决策 -文件工具的路径摘要(`read`/`write`/`edit` 参数中的 `path` 或 `file_path`)渲染为静止状态下即带下划线的链接,并使用 pointer 光标。点击路径会经聊天视图的 `openFile` injection 调用 `session/openWorkspacePath`;Host 以目标 Session 的 cwd 为基准解析相对路径。带文件链接的行关闭参数展开(左侧图标不可点);工具行(含 bash 与 todo 注册)去掉整行点击、整行悬停底色,以及点击打开 details 的手势。details 面板及其 inject 面仍保留供程序化选择;工具行不再驱动它们。 +文件工具的路径摘要(`read`/`write`/`edit` 参数中的 `path` 或 `file_path`)渲染为静止状态下即带下划线的链接,并使用 pointer 光标。点击路径会经聊天视图的 `openFile` injection 调用 `session/openWorkspacePath`;聊天视图会在目标 Session 的 cwd 已知时据此解析相对路径。带文件链接的行关闭参数展开(左侧图标不可点);工具行(含 bash 与 todo 注册)去掉整行点击、整行悬停底色,以及点击打开 details 的手势。details 面板及其 inject 面仍保留供程序化选择;工具行不再驱动它们。 `session/openWorkspacePath` 使用经过认证的 Remote carrier,而产品 UI 只在 loopback 页面且 `host.describe.canOpenPath` 为 true 时提供该手势。平台适配器不经 shell 打开:macOS 为 `open`,Windows 为 PowerShell `Invoke-Item`,桌面 Linux 为 `xdg-open`;浏览器可渲染的文档会在 macOS 与桌面 Linux 上优先使用指定的默认浏览器。尽管 Node 将 WSL 报告为 `linux`,WSL 仍是一种独立的宿主形态:适配器根据其环境或 Microsoft 内核 release 识别它,用 `wslpath -w` 转换 Linux 路径,并将所得 Windows/UNC 路径交给同一 PowerShell 交接。打开器的平台信息和命令运行器可在测试中注入。仅含 URL 的 read 参数(`web_fetch`)不是文件链接。 diff --git a/apps/web/tests/produced-files.e2e.ts b/apps/web/tests/produced-files.e2e.ts index cdc3296693..0761639de5 100644 --- a/apps/web/tests/produced-files.e2e.ts +++ b/apps/web/tests/produced-files.e2e.ts @@ -158,7 +158,7 @@ describe('web e2e: a finished turn ends with the files it produced', () => { ]) expect(response.status()).toBe(200) expect(openPath).toHaveBeenCalledTimes(1) - expect(openPath.mock.calls[0]![0]).toMatchObject({ path: '.' }) + expect(openPath.mock.calls[0]![0]).toMatchObject({ path: `${scaffold.workspaceCwd}/.` }) } finally { openPath.mockRestore() } diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 2416c74af9..438a3ef9c8 100644 --- a/docs/config-catalog.i18n.yaml +++ b/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: 0a08455a27fa94f1bb69628b61dd8eb23d318ded -config-catalog.zh.md: ad6a9c0481750ec41c0701d3a9e3989fd0e9ebf4 +config-catalog.md: 05bd17a600869782409038188457db6d362f07a4 +config-catalog.zh.md: 1095b31530a28af2c4a23520fd5e68e067b46c11 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 0a08455a27..05bd17a600 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -304,7 +304,7 @@ export interface Config { } ``` -Source: [`packages/api/session-controller/src/index.ts:69`](../packages/api/session-controller/src/index.ts) +Source: [`packages/api/session-controller/src/index.ts:67`](../packages/api/session-controller/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index ad6a9c0481..1095b31530 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -306,7 +306,7 @@ export interface Config { } ``` -来源:[`packages/api/session-controller/src/index.ts:69`](../packages/api/session-controller/src/index.ts) +来源:[`packages/api/session-controller/src/index.ts:67`](../packages/api/session-controller/src/index.ts) diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index 468327d243..749e1e0695 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.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/event-producer-consumer.md -event-producer-consumer.md: 2c443095b796eb274fa099b4b7d91b4454311a37 -event-producer-consumer.zh.md: b35e1c56d7f51d3aed7f3f81aec228708cb952e3 +event-producer-consumer.md: 8c30d5a945e2abb21c1e8dad5d6aa4e472c18b23 +event-producer-consumer.zh.md: 29304d9ac855a36ffc8a5e6766bedc841cf2738c diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 2c443095b7..8c30d5a945 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -21,11 +21,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:185`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:538`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:518`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:545`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:524`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:531`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:537`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:517`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:544`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:523`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:530`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) | | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index b35e1c56d7..29304d9ac8 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -23,11 +23,11 @@ | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:224`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:185`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:538`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:518`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:545`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:524`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:531`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:537`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:517`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:544`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:523`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:530`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) | | `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` | diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 423b9a6668..f917871371 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.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/session.md -session.md: fe3f71de6aeb6b5d9924fa88c94688f7eac8ff0a -session.zh.md: 844fd7a057c853fcbda6add875b997069ab024de +session.md: 919a8eff583886610b56b34294ae1c13a06493b0 +session.zh.md: 8ffc7c8256311328c9e56e63625f3fbfcb5241a7 diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index fe3f71de6a..919a8eff58 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -581,7 +581,7 @@ The backends that consume this contract are on [persistence.md](persistence.md). `ModelCatalog` is the Host-generation model directory returned by `session/modelCatalog`: it carries the deployment default, routable provider ids, successful provider groups, and isolated provider failures. It is not derived from one Session and remains separate from Session projections. -`SessionOpenWorkspacePathRequest` carries a `sessionId` and an absolute or Session-workspace-relative `path`. `SessionOpenWorkspacePathValue` confirms that the Host accepted the native handoff. The controller inspects the Session without activating its Agent, resolves a relative path against the recorded cwd, and reports missing Sessions, cancellation, and opener failures through the Session Remote error vocabulary. +`SessionOpenWorkspacePathRequest` carries an absolute or workspace-resolved `path`. `SessionOpenWorkspacePathValue` confirms that the Host accepted the native handoff. A Session-aware Client resolves relative paths against its current Session cwd when known; the controller hands the path to the opener unchanged and reports invalid requests, cancellation, and opener failures through the Session Remote error vocabulary. @@ -650,11 +650,11 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH @Remote('modelCatalog') modelCatalog(): Promise /** - * Open a path resolved against one Session's workspace on the Host desktop. - * @param request - Session identity and absolute or workspace-relative path. - * @param signal - caller lifetime; abort terminates inspection or the native command. + * Open one path prepared by a Session-aware caller on the Host desktop. + * @param request - path after best-effort Session workspace resolution. + * @param signal - caller lifetime; abort terminates the native command. * @returns confirmation after the native opener accepts the path. - * @throws TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails. + * @throws TypertRemoteFailure when the request is invalid, cancelled, or the opener fails. */ @Remote('openWorkspacePath') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index 844fd7a057..8ffc7c8256 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -585,7 +585,7 @@ interface TurnEndReasonMap { `ModelCatalog` 是 `session/modelCatalog` 返回的 Host generation 模型目录:它携带部署默认值、可路由 provider id、成功的 provider 分组与相互隔离的 provider 失败。它不由某个 Session 派生,因此与 Session projection 分开保存。 -`SessionOpenWorkspacePathRequest` 携带 `sessionId` 与绝对路径或相对于 Session workspace 的 `path`。`SessionOpenWorkspacePathValue` 确认 Host 已接受原生交接。controller 在不激活 Agent 的前提下检查 Session,基于记录的 cwd 解析相对路径,并通过 Session Remote 错误词汇表报告 Session 缺失、取消与打开器失败。 +`SessionOpenWorkspacePathRequest` 携带绝对路径或已按 workspace 解析的 `path`。`SessionOpenWorkspacePathValue` 确认 Host 已接受原生交接。Session-aware Client 会在已知当前 Session cwd 时据此解析相对路径;controller 将路径原样交给打开器,并通过 Session Remote 错误词汇表报告无效请求、取消与打开器失败。 @@ -654,11 +654,11 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH @Remote('modelCatalog') modelCatalog(): Promise /** - * Open a path resolved against one Session's workspace on the Host desktop. - * @param request - Session identity and absolute or workspace-relative path. - * @param signal - caller lifetime; abort terminates inspection or the native command. + * Open one path prepared by a Session-aware caller on the Host desktop. + * @param request - path after best-effort Session workspace resolution. + * @param signal - caller lifetime; abort terminates the native command. * @returns confirmation after the native opener accepts the path. - * @throws TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails. + * @throws TypertRemoteFailure when the request is invalid, cancelled, or the opener fails. */ @Remote('openWorkspacePath') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise diff --git a/knip.json b/knip.json index b22ef3caaa..a1c247f0d5 100644 --- a/knip.json +++ b/knip.json @@ -460,11 +460,6 @@ "tests/**/*.ts" ] }, - "packages/context/file-reference": { - "ignoreDependencies": [ - "zod" - ] - }, "packages/context/session-reference": { "ignoreDependencies": [ "zod" diff --git a/packages/api/session-controller/src/index.ts b/packages/api/session-controller/src/index.ts index dadf1db607..b58cdfcbad 100644 --- a/packages/api/session-controller/src/index.ts +++ b/packages/api/session-controller/src/index.ts @@ -7,9 +7,7 @@ import { openNativePath } from '@deepseek-ai/dsh-native-command' import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session' import type { SessionObservation } from '@deepseek-ai/dsh-session-query' import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' -import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path' import { - ApiSessionNotFound, ApiSessionAgentController, inspectApiSession, type ApiSessionAgentResult, @@ -245,11 +243,11 @@ export class SessionController extends TypertRemoteService { } /** - * Open a path resolved against one Session's workspace on the Host desktop. - * @param request - Session identity and absolute or workspace-relative path. - * @param signal - caller lifetime; abort terminates inspection or the native command. + * Open one path prepared by a Session-aware caller on the Host desktop. + * @param request - path after best-effort Session workspace resolution. + * @param signal - caller lifetime; abort terminates the native command. * @returns confirmation after the native opener accepts the path. - * @throws TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails. + * @throws TypertRemoteFailure when the request is invalid, cancelled, or the opener fails. */ @Remote('openWorkspacePath') async openWorkspacePath( @@ -264,30 +262,8 @@ export class SessionController extends TypertRemoteService { }) } signal.throwIfAborted() - let cwd: string | undefined try { - cwd = (await this.inspect(request.sessionId, signal)).meta.cwd - } catch (error: unknown) { - if (signal.aborted) { - throw new TypertRemoteFailure({ - code: 'cancelled', message: 'path open was aborted', details: {}, - }) - } - if (error instanceof ApiSessionNotFound) { - throw new TypertRemoteFailure({ - code: 'session-not-found', - message: error.message, - details: { sessionId: request.sessionId }, - }) - } - throw new TypertRemoteFailure({ - code: 'internal', - message: `session "${request.sessionId}" could not be inspected: ${String(error)}`, - details: {}, - }) - } - try { - await this.openPath(resolveWorkspacePath(cwd, request.path), signal) + await this.openPath(request.path, signal) return { opened: true } } catch (error: unknown) { if (signal.aborted) { diff --git a/packages/api/session-controller/src/types.ts b/packages/api/session-controller/src/types.ts index 24e9232d07..167937e3d3 100644 --- a/packages/api/session-controller/src/types.ts +++ b/packages/api/session-controller/src/types.ts @@ -362,10 +362,9 @@ export interface SessionCancelValue { readonly accepted: true } -/** Session-addressed request to open one workspace path on the Host desktop. */ +/** Request to open one path prepared by a Session-aware caller on the Host desktop. */ export interface SessionOpenWorkspacePathRequest { - readonly sessionId: SessionId - /** Absolute or Session-workspace-relative path. */ + /** Path after best-effort Session workspace resolution, in Host filesystem syntax. */ readonly path: string } diff --git a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts index 2727fb96c2..ee24947227 100644 --- a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts +++ b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts @@ -1,11 +1,10 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' -import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' +import SessionStore from '@deepseek-ai/dsh-session' import { describe, expect, it, vi } from 'vitest' import { createSessionTestController, createSessionTestRemote, - testSessionPersistence, } from './test-remote.ts' async function context(): Promise { @@ -16,10 +15,8 @@ async function context(): Promise { } describe('session/openWorkspacePath', () => { - it('resolves a relative path against the attached Session cwd', async () => { + it('hands a Client-resolved workspace path to the Host opener unchanged', async () => { const ctx = await context() - const sessionId = SessionId('open-relative') - ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } }) const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), @@ -28,18 +25,14 @@ describe('session/openWorkspacePath', () => { }) const signal = new AbortController().signal - await expect(remote.openWorkspacePath({ sessionId, path: 'src/a.ts' }, signal)) + await expect(remote.openWorkspacePath({ path: '/workspace/project/src/a.ts' }, signal)) .resolves.toEqual({ ok: true, value: { opened: true } }) expect(openPath).toHaveBeenCalledWith('/workspace/project/src/a.ts', signal) expect(ctx.agents.list()).toEqual([]) }) - it('preserves absolute paths and cwd-less Session paths', async () => { + it('preserves relative and absolute Host-resolvable paths', async () => { const ctx = await context() - const withCwd = SessionId('open-absolute') - const withoutCwd = SessionId('open-without-cwd') - ctx.sessions.create(withCwd, { meta: { cwd: '/workspace/project' } }) - ctx.sessions.create(withoutCwd) const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), @@ -47,18 +40,13 @@ describe('session/openWorkspacePath', () => { openPath, }) - await remote.openWorkspacePath({ sessionId: withCwd, path: '/tmp/result.html' }) - await remote.openWorkspacePath({ sessionId: withoutCwd, path: 'result.html' }) + await remote.openWorkspacePath({ path: '/tmp/result.html' }) + await remote.openWorkspacePath({ path: 'result.html' }) expect(openPath.mock.calls.map(call => call[0])).toEqual(['/tmp/result.html', 'result.html']) }) - it('rejects empty paths and missing Sessions before opening anything', async () => { + it('rejects empty paths before opening anything', async () => { const ctx = await context() - const sessionId = SessionId('open-validation') - ctx.provide('sessionPersistence', testSessionPersistence(ctx, { - list: () => Promise.resolve([]), - inspect: () => Promise.resolve(undefined), - }) as never) const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), @@ -66,17 +54,13 @@ describe('session/openWorkspacePath', () => { openPath, }) - await expect(remote.openWorkspacePath({ sessionId, path: '' })) + await expect(remote.openWorkspacePath({ path: '' })) .resolves.toMatchObject({ ok: false, error: { code: 'bad-request' } }) - await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' })) - .resolves.toMatchObject({ ok: false, error: { code: 'session-not-found' } }) expect(openPath).not.toHaveBeenCalled() }) it('preserves native opener failure and cancellation results', async () => { const ctx = await context() - const sessionId = SessionId('open-failure') - ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } }) const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.reject(new Error('desktop unavailable'))) const remote = createSessionTestRemote(ctx, { @@ -85,7 +69,7 @@ describe('session/openWorkspacePath', () => { openPath, }) - await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' })) + await expect(remote.openWorkspacePath({ path: 'result.html' })) .resolves.toMatchObject({ ok: false, error: { code: 'internal', message: 'path open failed: desktop unavailable' }, @@ -93,38 +77,12 @@ describe('session/openWorkspacePath', () => { const aborted = new AbortController() aborted.abort(new Error('cancelled')) - await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }, aborted.signal)) + await expect(remote.openWorkspacePath({ path: 'result.html' }, aborted.signal)) .resolves.toMatchObject({ ok: false, error: { code: 'cancelled' } }) }) - it('classifies inspection cancellation and non-session failures', async () => { - const ctx = await context() - const controller = createSessionTestController(ctx, { - defaultModelSelection: () => ({ provider: 'p', model: 'm' }), - cwd: '/default', - }) - const inspect = vi.spyOn(controller, 'inspect') - const aborted = new AbortController() - inspect.mockImplementationOnce(async () => { - aborted.abort(new Error('cancelled')) - throw new Error('inspection stopped') - }) - await expect(controller.openWorkspacePath({ - sessionId: SessionId('inspection-cancelled'), path: 'result.html', - }, aborted.signal)).rejects.toMatchObject({ failure: { code: 'cancelled' } }) - - inspect.mockRejectedValueOnce('storage offline') - const failed = controller.openWorkspacePath({ - sessionId: SessionId('inspection-failed'), path: 'result.html', - }, new AbortController().signal) - await expect(failed).rejects.toMatchObject({ failure: { code: 'internal' } }) - await expect(failed).rejects.toThrow('storage offline') - }) - it('classifies opener cancellation and non-Error failures', async () => { const ctx = await context() - const sessionId = SessionId('open-error-kinds') - ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } }) const aborted = new AbortController() const openPath = vi.fn() .mockImplementationOnce(async () => { @@ -138,10 +96,10 @@ describe('session/openWorkspacePath', () => { openPath, }) - await expect(controller.openWorkspacePath({ sessionId, path: 'first.html' }, aborted.signal)) + await expect(controller.openWorkspacePath({ path: 'first.html' }, aborted.signal)) .rejects.toMatchObject({ failure: { code: 'cancelled' } }) await expect(controller.openWorkspacePath({ - sessionId, path: 'second.html', + path: 'second.html', }, new AbortController().signal)).rejects.toMatchObject({ failure: { code: 'internal', message: 'path open failed: desktop unavailable' }, }) diff --git a/packages/api/session-controller/tsconfig.host.json b/packages/api/session-controller/tsconfig.host.json index e3f40c885d..bea21672b7 100644 --- a/packages/api/session-controller/tsconfig.host.json +++ b/packages/api/session-controller/tsconfig.host.json @@ -44,7 +44,6 @@ { "path": "../../subagent/subagent" }, { "path": "../../typert/protocol" }, { "path": "../../typert/registry" }, - { "path": "../../util/workspace-path" }, { "path": "../../workspace/workspace" } ] } diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index e39c39db01..a2a7617a7e 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -3501,9 +3501,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld { }) } case 'session/openWorkspacePath': { - const pathRequest = request as { readonly sessionId: SessionId; readonly path: string } - const missing = requireRemoteSession(pathRequest) - return missing ?? sessionOk({ opened: true as const }) + return sessionOk({ opened: true as const }) } case 'session/modelCatalog': return Promise.resolve({ ok: true, diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index fcafb63b8b..10382a63bf 100644 --- a/packages/client/ui-chat/package.json +++ b/packages/client/ui-chat/package.json @@ -74,7 +74,8 @@ "@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-tools": "workspace:^", + "@deepseek-ai/dsh-util-workspace-path": "workspace:^" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:^", @@ -104,6 +105,7 @@ "@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" }, diff --git a/packages/client/ui-chat/src/client/apply.ts b/packages/client/ui-chat/src/client/apply.ts index 829b36bd0f..be061dff13 100644 --- a/packages/client/ui-chat/src/client/apply.ts +++ b/packages/client/ui-chat/src/client/apply.ts @@ -5,6 +5,7 @@ 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' @@ -117,7 +118,10 @@ export function apply(ctx: Context): void { }, fileMentions: (owner: TurnTailOwnerProps) => ctx.get('chatFileMentions')?.forClosing(owner), openFile: async (path) => { - const result = await ctx.remote.session.openWorkspacePath({ sessionId, path }) + const cwd = ctx.sessions.list.getSnapshot().byId[sessionId]?.cwd + const result = await ctx.remote.session.openWorkspacePath({ + path: resolveWorkspacePath(cwd, path), + }) if (!result.ok) throw new Error(`path open failed: ${result.error.message}`) }, loadOlder: () => { void session.loadOlder() }, diff --git a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx index 2a0d9b045c..9ceef64dd7 100644 --- a/packages/client/ui-chat/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-chat/tests/apply-inject.client.spec.tsx @@ -123,7 +123,7 @@ describe('Chat inject API', () => { const b = await bench() const { injected } = b.chatViewApi(ROOT) await injected.openFile('src/a.ts') - expect(b.openWorkspacePath).toHaveBeenCalledWith({ sessionId: ROOT, path: 'src/a.ts' }) + expect(b.openWorkspacePath).toHaveBeenCalledWith({ path: '/proj/src/a.ts' }) b.openWorkspacePath.mockResolvedValueOnce({ ok: false, diff --git a/packages/client/ui-chat/tsconfig.json b/packages/client/ui-chat/tsconfig.json index 0800260fbb..4d42320885 100644 --- a/packages/client/ui-chat/tsconfig.json +++ b/packages/client/ui-chat/tsconfig.json @@ -50,6 +50,9 @@ { "path": "../../runtime-diagnostics/invariants" }, + { + "path": "../../util/workspace-path" + }, { "path": "../../session/session-stats" }, diff --git a/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx b/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx index be9061fbc2..a23e003800 100644 --- a/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx +++ b/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx @@ -223,7 +223,7 @@ describe('run_code sub-calls through the real chat machinery', () => { view.getByText('notes/demo.txt').click() expect(b.layout.openDetails).not.toHaveBeenCalled() await vi.waitFor(() => { - expect(b.openWorkspacePath).toHaveBeenCalledWith({ sessionId: SID, path: 'notes/demo.txt' }) + expect(b.openWorkspacePath).toHaveBeenCalledWith({ path: 'notes/demo.txt' }) }) view.getByText('List notes').click() expect(b.layout.openDetails).not.toHaveBeenCalled() diff --git a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx index ad5eb90a10..719dbafb1a 100644 --- a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx +++ b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx @@ -137,7 +137,7 @@ describe('keyed toolview hole through the real machinery', () => { view.getByText('src/a.ts').click() expect(b.layout.openDetails).not.toHaveBeenCalled() await vi.waitFor(() => { - expect(b.openWorkspacePath).toHaveBeenCalledWith({ sessionId: SID, path: 'src/a.ts' }) + expect(b.openWorkspacePath).toHaveBeenCalledWith({ path: 'src/a.ts' }) }) await b.runtime.dispose() }) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index eb1992f66d..37d29ce704 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1384,10 +1384,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: '@Remote(\'openWorkspacePath\') async openWorkspacePath( request: SessionOpenWorkspacePathRequest, signal: AbortSignal, ): Promise', - description: 'Open a path resolved against one Session\'s workspace on the Host desktop.', - parameters: [{ name: 'request', description: 'Session identity and absolute or workspace-relative path.' }, { name: 'signal', description: 'caller lifetime; abort terminates inspection or the native command.' }], + description: 'Open one path prepared by a Session-aware caller on the Host desktop.', + parameters: [{ name: 'request', description: 'path after best-effort Session workspace resolution.' }, { name: 'signal', description: 'caller lifetime; abort terminates the native command.' }], returns: 'confirmation after the native opener accepts the path.', - throws: ['TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails.'], + throws: ['TypertRemoteFailure when the request is invalid, cancelled, or the opener fails.'], }, { signature: '@Remote(\'rename\') rename(request: SessionRenameRequest): Promise', @@ -4960,7 +4960,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionOpenWorkspacePathRequest', - declaration: 'export interface SessionOpenWorkspacePathRequest {\n readonly sessionId: SessionId;\n readonly path: string;\n}', + declaration: 'export interface SessionOpenWorkspacePathRequest {\n readonly path: string;\n}', }, { name: 'SessionOpenWorkspacePathValue', diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 29ebd6b732..6e502172f7 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -34,9 +34,7 @@ export interface ApiProxyDefaults { /** Validated DEFLATE level for session-log ZIP entries; defaults to 6. */ sessionExportCompressionLevel?: SessionLogCompressionLevel /** - * Whether handing a path to the native opener can work at all — the - * `hasDocument` capability the preset roster reports, and the switch - * between opening a preset directory and answering its path as text. + * Whether `host.describe` reports that the Client may offer native path actions. * Absent, platform detection decides ({@link canOpenNativePath}). */ canOpenPath?: () => boolean diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4eab46efb1..5ac1fcc288 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2065,6 +2065,9 @@ importers: '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools + '@deepseek-ai/dsh-util-workspace-path': + specifier: workspace:^ + version: link:../../util/workspace-path '@types/react': specifier: ~18.3.1 version: 18.3.31 From 2ff3a0c09f76affec2bd68b04bc0f3bc6ef65344 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 21:49:39 +0800 Subject: [PATCH 12/13] fix(file-reference): remove unused zod dependency --- packages/context/file-reference/package.json | 3 --- pnpm-lock.yaml | 4 ---- 2 files changed, 7 deletions(-) diff --git a/packages/context/file-reference/package.json b/packages/context/file-reference/package.json index f1e45418b6..1713c8d341 100644 --- a/packages/context/file-reference/package.json +++ b/packages/context/file-reference/package.json @@ -49,8 +49,5 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/cordis": "workspace:^" - }, - "dependencies": { - "zod": "^4.4.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ac1fcc288..55ffa061ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4022,10 +4022,6 @@ importers: version: link:../../core/tools packages/context/file-reference: - dependencies: - zod: - specifier: ^4.4.3 - version: 4.4.3 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ From ea07f465acab3218aeccad11bbbe7b87684388f3 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 27 Aug 2026 22:00:58 +0800 Subject: [PATCH 13/13] docs: refresh module graph --- docs/module-graph.i18n.yaml | 4 ++-- docs/module-graph.md | 3 ++- docs/module-graph.zh.md | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 00810b4551..558d4a4200 100644 --- a/docs/module-graph.i18n.yaml +++ b/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: aebb6883280d5edc48355e83936cb8793529fc2a -module-graph.zh.md: 43607e291b632d1df6cd00a59e182027dc548008 +module-graph.md: 7abbcb938f2b2a203523e569422ef4cace658fe8 +module-graph.zh.md: 862f24dfc374e8b783bf94cebe29a58d52d7a02b diff --git a/docs/module-graph.md b/docs/module-graph.md index aebb688328..7abbcb938f 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -1545,6 +1545,7 @@ flowchart TD pkg_client_ui_chat --> pkg_settings pkg_client_ui_chat --> pkg_token_meter pkg_client_ui_chat --> pkg_tools + pkg_client_ui_chat --> pkg_util_workspace_path pkg_client_ui_commands --> pkg_api_remotes pkg_client_ui_commands --> pkg_api_session_controller pkg_client_ui_commands --> pkg_client_locale @@ -1935,7 +1936,7 @@ flowchart TD | [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`attachment`](../packages/attachment/attachment), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`client-ui-user-questions`](../packages/client/ui-user-questions) | `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), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions) | | [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`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-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | -| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | +| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`util-workspace-path`](../packages/util/workspace-path) | | [`client-ui-commands`](../packages/client/ui-commands) | `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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) | | [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 43607e291b..862f24dfc3 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -1547,6 +1547,7 @@ flowchart TD pkg_client_ui_chat --> pkg_settings pkg_client_ui_chat --> pkg_token_meter pkg_client_ui_chat --> pkg_tools + pkg_client_ui_chat --> pkg_util_workspace_path pkg_client_ui_commands --> pkg_api_remotes pkg_client_ui_commands --> pkg_api_session_controller pkg_client_ui_commands --> pkg_client_locale @@ -1937,7 +1938,7 @@ flowchart TD | [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`attachment`](../packages/attachment/attachment), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`client-ui-user-questions`](../packages/client/ui-user-questions) | `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), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions) | | [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`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-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-agent-team`](../packages/experimental/agent-team), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | -| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | +| [`client-ui-chat`](../packages/client/ui-chat) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-locale`](../packages/client/locale), [`client-ui-approval`](../packages/client/ui-approval), [`client-ui-conversation`](../packages/client/ui-conversation), [`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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`util-workspace-path`](../packages/util/workspace-path) | | [`client-ui-commands`](../packages/client/ui-commands) | `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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`client-ui-reference`](../packages/client/ui-reference) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`file-reference`](../packages/context/file-reference), [`invariants`](../packages/runtime-diagnostics/invariants), [`session-reference`](../packages/context/session-reference), [`typert-protocol`](../packages/typert/protocol), [`util-workspace-path`](../packages/util/workspace-path) | | [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |