Merge remote-tracking branch 'origin/master' into worktree/web-theme-settings-integration-fde706
# Conflicts: # .agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.i18n.yaml # .agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.zh.md # .agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.i18n.yaml # .agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.zh.md # .agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.i18n.yaml # .agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.zh.md # .agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml # apps/web/tests/scaffold.ts # docs/module-graph.md # packages/client/runtime/README.i18n.yaml # packages/client/runtime/package.json # packages/client/test-runtime/README.i18n.yaml # packages/client/test-runtime/README.zh.md # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/README.zh.md # packages/client/ui-conversation/package.json # packages/client/ui-conversation/src/client/apply.ts # packages/client/ui-theme/README.i18n.yaml # packages/client/ui-theme/README.md # packages/client/ui-theme/README.zh.md # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.zh.md # pnpm-lock.yaml
This commit is contained in:
File diff suppressed because it is too large
Load Diff
88
packages/host/apiproxy/src/api/agent-presets.schema.ts
Normal file
88
packages/host/apiproxy/src/api/agent-presets.schema.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* agent-presets domain zod schemas (names derived from map keys:
|
||||
* agentPresetListRequestSchema / agentPresetListValueSchema).
|
||||
*/
|
||||
|
||||
import { z } from 'zod'
|
||||
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
|
||||
import type { Wire } from './rpc.schema.ts'
|
||||
import { sessionIdSchema } from './sessions.schema.ts'
|
||||
import type { AgentPresetEntry } from './agent-presets.ts'
|
||||
|
||||
/** AgentPresetEntry row of agentPreset.list. */
|
||||
export const agentPresetEntrySchema = z.object({
|
||||
id: z.string().min(1),
|
||||
trust: z.union([z.literal('system'), z.literal('user')]),
|
||||
isDefault: z.boolean(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
broken: z.string().min(1).optional(),
|
||||
}) satisfies z.ZodType<Wire<AgentPresetEntry>>
|
||||
|
||||
/** agentPreset.list request payload. */
|
||||
export const agentPresetListRequestSchema = z.object({
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.list'>>>
|
||||
|
||||
/** agentPreset.list response value. */
|
||||
export const agentPresetListValueSchema = z.object({
|
||||
presets: z.array(agentPresetEntrySchema),
|
||||
authorable: z.boolean(),
|
||||
hasDocument: z.boolean(),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.list'>>>
|
||||
|
||||
/** agentPreset.select request payload. */
|
||||
export const agentPresetSelectRequestSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
agentPreset: z.string().min(1),
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.select'>>>
|
||||
|
||||
/** agentPreset.select response value. */
|
||||
export const agentPresetSelectValueSchema = z.object({
|
||||
agentPreset: z.string(),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.select'>>>
|
||||
|
||||
/** agentPreset.read request payload. */
|
||||
export const agentPresetReadRequestSchema = z.object({
|
||||
agentPreset: z.string().min(1),
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.read'>>>
|
||||
|
||||
/** agentPreset.read response value. */
|
||||
export const agentPresetReadValueSchema = z.object({
|
||||
agentPreset: z.string(),
|
||||
trust: z.union([z.literal('system'), z.literal('user')]),
|
||||
content: z.string(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.read'>>>
|
||||
|
||||
/** agentPreset.copy request payload. */
|
||||
export const agentPresetCopyRequestSchema = z.object({
|
||||
from: z.string().min(1),
|
||||
agentPreset: z.string().min(1),
|
||||
name: z.string().optional(),
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.copy'>>>
|
||||
|
||||
/** agentPreset.copy response value. */
|
||||
export const agentPresetCopyValueSchema = z.object({
|
||||
agentPreset: z.string(),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.copy'>>>
|
||||
|
||||
/** agentPreset.openDocument request payload. */
|
||||
export const agentPresetOpenDocumentRequestSchema = z.object({
|
||||
agentPreset: z.string().min(1),
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.openDocument'>>>
|
||||
|
||||
/** 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<Wire<ResponseValue<'agentPreset.openDocument'>>>
|
||||
|
||||
/** agentPreset.remove request payload. */
|
||||
export const agentPresetRemoveRequestSchema = z.object({
|
||||
agentPreset: z.string().min(1),
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'agentPreset.remove'>>>
|
||||
|
||||
/** agentPreset.remove response value. */
|
||||
export const agentPresetRemoveValueSchema = z.object({
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'agentPreset.remove'>>>
|
||||
116
packages/host/apiproxy/src/api/agent-presets.ts
Normal file
116
packages/host/apiproxy/src/api/agent-presets.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* agent-presets domain contract: the roster a browser offers when starting a
|
||||
* session, plus the authoring calls behind it.
|
||||
*
|
||||
* `list` is ordinary: it carries ids and trust, and every preset picker needs
|
||||
* it. The authoring calls are privileged and loopback-pinned — a composition
|
||||
* names the plugins a session runs, so reading one is reconnaissance, and
|
||||
* although authoring is copy-only (no caller supplies composition text or a
|
||||
* path), copying and deleting still rearrange what the deployment offers.
|
||||
*/
|
||||
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { RpcRequest, RpcResponse } from './rpc.ts'
|
||||
|
||||
/** One preset the deployment can compose a session's agent from. */
|
||||
export interface AgentPresetEntry {
|
||||
/** Stable identifier, also the display name until presets carry metadata. */
|
||||
readonly id: string
|
||||
/**
|
||||
* Whether the preset ships with the deployment or was authored locally.
|
||||
* A `user` preset is exactly as privileged as the plugins it names, so a
|
||||
* surface offering one should say so rather than present it as vetted.
|
||||
*/
|
||||
readonly trust: 'system' | 'user'
|
||||
/** Whether a session that names no preset gets this one. */
|
||||
readonly isDefault: boolean
|
||||
/**
|
||||
* Display name the preset published, absent when it published none. A
|
||||
* surface falls back to {@link id}; it is never a second identity, and it
|
||||
* never decides trust — a locally authored preset cannot name itself into
|
||||
* the shipped set.
|
||||
*/
|
||||
readonly name?: string
|
||||
/** One sentence on what the preset is for, when it published one. */
|
||||
readonly description?: string
|
||||
/**
|
||||
* Why this preset cannot compose a session, absent when it can. A broken
|
||||
* preset stays listed — its directory still occupies the id, so a surface
|
||||
* must be able to show and delete it — but offering it for selection would
|
||||
* only defer this reason to a failed session start.
|
||||
*/
|
||||
readonly broken?: string
|
||||
}
|
||||
|
||||
/** agent-preset-domain unary methods (the map key agentPreset.* of RpcMethodMap). */
|
||||
export interface AgentPresetsApi {
|
||||
/**
|
||||
* Lists every preset the deployment currently supplies, in root-precedence
|
||||
* order — the roots as configured, each root's own presets sorted by id,
|
||||
* and the first root to supply an id wins. The order is not globally
|
||||
* sorted: a user root's preset sits in that root's block, not among the
|
||||
* shipped ids.
|
||||
* An empty roster means the deployment composes no presets at all, and
|
||||
* every session shares the host composition. `authorable` reports whether
|
||||
* the deployment configures a root new presets can be written to, and
|
||||
* `hasDocument` whether `openDocument` can hand a preset directory to a
|
||||
* native opener — both deployment facts rather than per-preset ones, and
|
||||
* neither exposes a Host path.
|
||||
*/
|
||||
list(request: RpcRequest<{}>):
|
||||
Promise<RpcResponse<{ presets: readonly AgentPresetEntry[]; authorable: boolean; hasDocument: boolean }>>
|
||||
|
||||
/**
|
||||
* Recompose one session's agent from a different preset.
|
||||
*
|
||||
* Allowed only while the session is blank — no turn has run. Once a
|
||||
* conversation starts, its history was produced under that preset's tools,
|
||||
* and swapping them would leave logged tool calls the new composition cannot
|
||||
* make; the attempt answers `agent-preset-locked`.
|
||||
*/
|
||||
select(request: RpcRequest<{ sessionId: SessionId; agentPreset: string }>):
|
||||
Promise<RpcResponse<{ agentPreset: string }>>
|
||||
|
||||
/**
|
||||
* Read one preset's composition text, for the read-only viewer.
|
||||
*
|
||||
* Privileged: a composition names the plugins a session runs, so reading
|
||||
* one is reconnaissance.
|
||||
*/
|
||||
read(request: RpcRequest<{ agentPreset: string }>):
|
||||
Promise<RpcResponse<{
|
||||
agentPreset: string
|
||||
trust: 'system' | 'user'
|
||||
content: string
|
||||
name?: string
|
||||
description?: string
|
||||
}>>
|
||||
|
||||
/**
|
||||
* Create a locally authored preset by copying an existing one whole.
|
||||
*
|
||||
* The only authoring write. No composition text and no path crosses the
|
||||
* wire: `from` and `agentPreset` are ids the Host resolves against its own
|
||||
* roots, so a copy is exactly as loadable as its source and grants nothing
|
||||
* the roster did not already carry. The copy keeps the source's description
|
||||
* (the file is the author's to edit afterwards) but not its name — `name`
|
||||
* here or the id fallback is what distinguishes the rows.
|
||||
*/
|
||||
copy(request: RpcRequest<{ from: string; agentPreset: string; name?: string }>):
|
||||
Promise<RpcResponse<{ agentPreset: string }>>
|
||||
|
||||
/**
|
||||
* Hand one locally authored preset's DIRECTORY to the platform opener, for
|
||||
* editing the files that are now 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 (`hasDocument: false` on `list`), 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<RpcResponse<{ opened: true } | { opened: false; path: string }>>
|
||||
|
||||
/** Delete a locally authored preset. Shipped presets are refused. */
|
||||
remove(request: RpcRequest<{ agentPreset: string }>): Promise<RpcResponse<{}>>
|
||||
}
|
||||
@@ -73,6 +73,7 @@ export const hostFrameSchema = z.discriminatedUnion('type', [
|
||||
parentSessionId: sessionIdSchema.optional(),
|
||||
origin: z.literal('subagent').optional(),
|
||||
cwd: z.string().optional(),
|
||||
agentPreset: z.string().optional(),
|
||||
}),
|
||||
z.object({ type: z.literal('host/session-removed'), sessionId: sessionIdSchema }),
|
||||
z.object({ type: z.literal('host/session-status'), sessionId: sessionIdSchema, running: z.boolean() }),
|
||||
|
||||
@@ -49,7 +49,7 @@ export interface EventsApi {
|
||||
* attached session, then replays each session's still-pending approval/question requested
|
||||
* frames (rpcId reused verbatim — the refresh-recovery baseline). Session titles ride the
|
||||
* generic projection pair (history-tail projections block + session/projection frames).
|
||||
* since: resume seam, unimplemented in v1 (ignored if passed); reconnection = reopen the
|
||||
* since: resume hook, unimplemented in v1 (ignored if passed); reconnection = reopen the
|
||||
* stream + refetch history.
|
||||
*/
|
||||
mux(request: RpcRequest<{ since?: Record<SessionId, number> }>, signal: AbortSignal): AsyncIterable<RpcRequest<MuxFrame>>
|
||||
@@ -116,6 +116,7 @@ export type HostFrame =
|
||||
parentSessionId?: SessionId
|
||||
origin?: 'subagent'
|
||||
cwd?: string
|
||||
agentPreset?: string
|
||||
}
|
||||
| { type: 'host/session-removed'; sessionId: SessionId }
|
||||
| { type: 'host/session-status'; sessionId: SessionId; running: boolean }
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { SessionsApi } from './sessions.ts'
|
||||
import type { HostApi } from './host.ts'
|
||||
import type { WorkspaceApi } from './workspace.ts'
|
||||
import type { CommandsApi } from './commands.ts'
|
||||
import type { AgentPresetsApi } from './agent-presets.ts'
|
||||
import type { SkillsApi } from './skills.ts'
|
||||
import type { SubagentsApi } from './subagents.ts'
|
||||
import type { EventsApi } from './events.ts'
|
||||
@@ -25,6 +26,7 @@ export interface ApiProxy {
|
||||
workspace: WorkspaceApi
|
||||
commands: CommandsApi
|
||||
skills: SkillsApi
|
||||
agentPresets: AgentPresetsApi
|
||||
events: EventsApi
|
||||
goals: GoalsApi
|
||||
settings: SettingsApi
|
||||
@@ -37,16 +39,18 @@ export interface ApiProxy {
|
||||
// ---- Domain interfaces and payload entities ----
|
||||
export type {
|
||||
HistoryEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
|
||||
ModelReasoningEffort, ModelTarget, QueueAction, SessionModels, SessionProjectionsBlock, SessionSearchItem,
|
||||
ModelReasoningEffort, ModelSelection, QueueAction, SessionModels, SessionProjectionsBlock, SessionSearchItem,
|
||||
SessionsApi, SessionSummary,
|
||||
} from './sessions.ts'
|
||||
export type { DirectoryEntry, DirectoryListing, HostApi } from './host.ts'
|
||||
export type {
|
||||
SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, SubagentsApi,
|
||||
SubagentAddress, SubagentCatalog, SubagentInterruptReceipt, SubagentListEntry,
|
||||
SubagentPromptReceipt, SubagentsApi,
|
||||
} from './subagents.ts'
|
||||
export type { WorkspaceApi, WorkspaceId, WorkspaceView } from './workspace.ts'
|
||||
export type { CommandsApi, CommandDescriptor } from './commands.ts'
|
||||
export type { SkillsApi, SkillEntry } from './skills.ts'
|
||||
export type { AgentPresetsApi, AgentPresetEntry } from './agent-presets.ts'
|
||||
export type { EventsApi, MuxFrame, HostFrame, QueuedInboxItem, ToolCallView, ToolEventView, ToolResultView } from './events.ts'
|
||||
export type { GoalsApi, GoalId, GoalRef } from './goals.ts'
|
||||
export type { SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView } from './settings.ts'
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* 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
|
||||
* (the same groups as `session.models`, without the per-session current
|
||||
* target). Both invalidate on the `host/models-changed` frame.
|
||||
* (the same groups as `session.models`, without a per-session selection).
|
||||
* Both invalidate on the `host/models-changed` frame.
|
||||
*/
|
||||
|
||||
import type { RpcRequest, RpcResponse } from './rpc.ts'
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { SessionsApi } from './sessions.ts'
|
||||
import type { HostApi } from './host.ts'
|
||||
import type { WorkspaceApi } from './workspace.ts'
|
||||
import type { CommandsApi } from './commands.ts'
|
||||
import type { AgentPresetsApi } from './agent-presets.ts'
|
||||
import type { SkillsApi } from './skills.ts'
|
||||
import type { GoalsApi } from './goals.ts'
|
||||
import type { SettingsApi } from './settings.ts'
|
||||
@@ -36,6 +37,7 @@ export interface RpcMethodMap {
|
||||
'subagent.list': SubagentsApi['list']
|
||||
'subagent.history': SubagentsApi['history']
|
||||
'subagent.prompt': SubagentsApi['prompt']
|
||||
'subagent.interrupt': SubagentsApi['interrupt']
|
||||
'host.describe': HostApi['describe']
|
||||
'host.pickDirectory': HostApi['pickDirectory']
|
||||
'host.listDirectory': HostApi['listDirectory']
|
||||
@@ -50,6 +52,12 @@ export interface RpcMethodMap {
|
||||
'command.list': CommandsApi['list']
|
||||
'command.execute': CommandsApi['execute']
|
||||
'skill.list': SkillsApi['list']
|
||||
'agentPreset.list': AgentPresetsApi['list']
|
||||
'agentPreset.select': AgentPresetsApi['select']
|
||||
'agentPreset.read': AgentPresetsApi['read']
|
||||
'agentPreset.copy': AgentPresetsApi['copy']
|
||||
'agentPreset.openDocument': AgentPresetsApi['openDocument']
|
||||
'agentPreset.remove': AgentPresetsApi['remove']
|
||||
'goal.create': GoalsApi['create']
|
||||
'goal.edit': GoalsApi['edit']
|
||||
'goal.pause': GoalsApi['pause']
|
||||
|
||||
@@ -46,6 +46,11 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
|
||||
z.object({ code: z.literal('directory-exists'), message: z.string(), details: z.object({ path: z.string() }) }),
|
||||
z.object({ code: z.literal('directory-create-failed'), message: z.string(), details: z.object({ path: z.string() }) }),
|
||||
z.object({ code: z.literal('directory-picker-unavailable'), message: z.string(), details: z.object({ capability: z.string() }) }),
|
||||
z.object({ code: z.literal('agent-preset-read-only'), message: z.string(), details: z.object({ agentPreset: z.string(), reason: z.string() }) }),
|
||||
z.object({ code: z.literal('agent-preset-locked'), message: z.string(), details: z.object({ sessionId: z.string(), agentPreset: z.string() }) }),
|
||||
z.object({ code: z.literal('agent-preset-conflict'), message: z.string(), details: z.object({ sessionId: z.string(), requestedPreset: z.string(), existingPreset: z.string().optional() }) }),
|
||||
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('queue-item-not-found'), message: z.string(), details: z.object({ itemId: z.string() }) }),
|
||||
z.object({ code: z.literal('steer-unavailable'), message: z.string(), details: z.object({ itemId: z.string() }) }),
|
||||
|
||||
@@ -44,6 +44,11 @@ export interface RpcErrorDetailsMap {
|
||||
'directory-exists': { path: string }
|
||||
'directory-create-failed': { path: string }
|
||||
'directory-picker-unavailable': { capability: string }
|
||||
'agent-preset-read-only': { agentPreset: string; reason: string }
|
||||
'agent-preset-locked': { sessionId: SessionId; agentPreset: string }
|
||||
'agent-preset-conflict': { sessionId: SessionId; requestedPreset: string; existingPreset?: string }
|
||||
'agent-preset-not-found': { agentPreset: string; available: string[] }
|
||||
'agent-preset-invalid': { agentPreset: string; reason: string }
|
||||
'agent-busy': { reason: string }
|
||||
'queue-item-not-found': { itemId: MessageId }
|
||||
'steer-unavailable': { itemId: MessageId }
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { RequestPayload, ResponseValue } from './rpc-map.ts'
|
||||
import type { Wire } from './rpc.schema.ts'
|
||||
import type {
|
||||
HistoryEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
|
||||
ModelReasoningEffort, ModelTarget, SessionProjectionsBlock, SessionSearchItem, SessionSummary,
|
||||
ModelReasoningEffort, ModelSelection, SessionProjectionsBlock, SessionSearchItem, SessionSummary,
|
||||
} from './sessions.ts'
|
||||
import type { ToolEventView } from './events.ts'
|
||||
import type { WorkspaceId } from './workspace.ts'
|
||||
@@ -55,6 +55,7 @@ export const sessionSummarySchema = z.object({
|
||||
parentSessionId: sessionIdSchema.optional(),
|
||||
origin: z.literal('subagent').optional(),
|
||||
cwd: z.string().optional(),
|
||||
agentPreset: z.string().optional(),
|
||||
projections: z.lazy(() => sessionProjectionsBlockSchema).optional(),
|
||||
}) as unknown as z.ZodType<Wire<SessionSummary>>
|
||||
|
||||
@@ -100,6 +101,7 @@ export const sessionCreateRequestSchema = z.object({
|
||||
workspaceId: workspaceIdSchema.optional(),
|
||||
cwd: z.string().optional(),
|
||||
sessionId: sessionIdSchema.optional(),
|
||||
agentPreset: z.string().optional(),
|
||||
}).refine(
|
||||
payload => payload.workspaceId === undefined || payload.cwd === undefined,
|
||||
{ message: 'session.create accepts workspaceId or cwd, not both' },
|
||||
@@ -108,6 +110,7 @@ export const sessionCreateRequestSchema = z.object({
|
||||
/** session.create response value. */
|
||||
export const sessionCreateValueSchema = z.object({
|
||||
sessionId: sessionIdSchema,
|
||||
agentPreset: z.string().optional(),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'session.create'>>>
|
||||
|
||||
/** session.rename request payload (raw title; host-side normalization decides acceptance). */
|
||||
@@ -140,12 +143,12 @@ export const sessionHistoryRequestSchema = z.object({
|
||||
maxMessages: z.number().int().positive().optional(),
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'session.history'>>>
|
||||
|
||||
/** Complete provider/model target. */
|
||||
export const modelTargetSchema = z.object({
|
||||
/** Complete provider/model selection. */
|
||||
export const modelSelectionSchema = z.object({
|
||||
provider: z.string().min(1),
|
||||
model: z.string().min(1),
|
||||
reasoningEffort: z.string().min(1).optional(),
|
||||
}) satisfies z.ZodType<Wire<ModelTarget>>
|
||||
}) satisfies z.ZodType<Wire<ModelSelection>>
|
||||
|
||||
/** One adapter-owned reasoning effort. */
|
||||
export const modelReasoningEffortSchema = z.object({
|
||||
@@ -224,7 +227,7 @@ export const sessionModelsRequestSchema = z.object({
|
||||
|
||||
/** session.models response value. */
|
||||
export const sessionModelsValueSchema = z.object({
|
||||
current: modelTargetSchema,
|
||||
current: modelSelectionSchema,
|
||||
routable: z.boolean(),
|
||||
groups: z.array(modelProviderGroupSchema),
|
||||
failures: z.array(modelCatalogFailureSchema),
|
||||
@@ -240,7 +243,7 @@ export const sessionSelectModelRequestSchema = z.object({
|
||||
|
||||
/** session.selectModel response value. */
|
||||
export const sessionSelectModelValueSchema = z.object({
|
||||
selected: modelTargetSchema,
|
||||
selected: modelSelectionSchema,
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'session.selectModel'>>>
|
||||
|
||||
/** ContentBlock passthrough: core is merge-extensible — the type discriminant envelope is strict, the rest stays wide. */
|
||||
|
||||
@@ -53,8 +53,8 @@ export interface SessionProjectionsBlock {
|
||||
values: Partial<SessionProjectionMap>
|
||||
}
|
||||
|
||||
/** Complete model target selected for one session. */
|
||||
export interface ModelTarget {
|
||||
/** Complete model selection for one session. */
|
||||
export interface ModelSelection {
|
||||
/** Registered provider route. */
|
||||
provider: string
|
||||
/** Provider-owned model id. */
|
||||
@@ -115,8 +115,8 @@ export interface ModelCatalogFailure {
|
||||
|
||||
/** Detached model-directory snapshot for one session. */
|
||||
export interface SessionModels {
|
||||
/** Target selected for the session's next assembled step. */
|
||||
current: ModelTarget
|
||||
/** Model selection for the session's next assembled step. */
|
||||
current: ModelSelection
|
||||
/**
|
||||
* Whether an adapter currently serves `current.provider`, and therefore
|
||||
* whether this session can start a turn at all. Deliberately NOT derivable
|
||||
@@ -165,6 +165,13 @@ export interface SessionSummary {
|
||||
origin?: 'subagent'
|
||||
/** Session working directory (header.cwd passthrough); absent when unrecorded. */
|
||||
cwd?: string
|
||||
/**
|
||||
* Agent preset this session's agent was composed from (header passthrough);
|
||||
* absent when the deployment composes no presets. A surface offering a
|
||||
* switch reads this to show what the session actually runs rather than what
|
||||
* the deployment currently defaults to.
|
||||
*/
|
||||
agentPreset?: string
|
||||
/**
|
||||
* Projection baseline for this row, with zero log loads: attached sessions
|
||||
* read the registry's live watermark cut; cold sessions read the persisted
|
||||
@@ -208,15 +215,22 @@ export interface SessionsApi {
|
||||
* session, while a different cwd fails with `session-conflict`. Workspace
|
||||
* creation attaches the session after publication; an attach failure
|
||||
* returns `workspace-attach-failed` with the published session id.
|
||||
*
|
||||
* `agentPreset` names the composition the new session's agent is built
|
||||
* from; omitted, the effective default applies — the user's stored choice
|
||||
* where one exists, else the deployment's own. The resolved id is stored on
|
||||
* the session header, so a later resume rebuilds the same agent. An unknown
|
||||
* id fails with `agent-preset-not-found`, and a preset whose composition
|
||||
* cannot be mounted fails with `agent-preset-invalid`.
|
||||
*/
|
||||
create(request: RpcRequest<{ workspaceId?: WorkspaceId; cwd?: string; sessionId?: SessionId }>):
|
||||
Promise<RpcResponse<{ sessionId: SessionId }>>
|
||||
create(request: RpcRequest<{ workspaceId?: WorkspaceId; cwd?: string; sessionId?: SessionId; agentPreset?: string }>):
|
||||
Promise<RpcResponse<{ sessionId: SessionId; agentPreset?: string }>>
|
||||
|
||||
/**
|
||||
* Reads a window of history events; page boundaries align to append-origin message
|
||||
* boundaries: one page = all raw events owned by a whole number of such messages (including
|
||||
* their chunk / tool events), never cut mid-message. Model-only replacement copies consume no
|
||||
* `maxMessages`, so a compaction's provenance stays on the page of its replacement. The tail
|
||||
* `maxMessages`, so a compaction's `compact/summary` record stays on the page of its replacement. The tail
|
||||
* page (beforeSeq absent) additionally carries the in-flight
|
||||
* partial — chunk events already emitted for the last unfinalized message.
|
||||
* Each entry pairs the raw SessionEvent with the host-computed view (tool events whose
|
||||
@@ -240,7 +254,7 @@ export interface SessionsApi {
|
||||
models(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<SessionModels>>
|
||||
|
||||
/**
|
||||
* Selects the complete target for this session. Exact model metadata
|
||||
* Selects the complete model selection for this session. Exact model metadata
|
||||
* validates an optional reasoning effort, while catalog membership remains
|
||||
* advisory. Session-backed subagents reject with `agent-busy`.
|
||||
*/
|
||||
@@ -250,7 +264,7 @@ export interface SessionsApi {
|
||||
model: string
|
||||
reasoningEffort?: string
|
||||
}>):
|
||||
Promise<RpcResponse<{ selected: ModelTarget }>>
|
||||
Promise<RpcResponse<{ selected: ModelSelection }>>
|
||||
|
||||
/**
|
||||
* Renames a session: appends a `session/title` event with the `user`
|
||||
|
||||
@@ -69,6 +69,18 @@ export const subagentPromptRequestSchema = z.object({
|
||||
content: z.array(contentBlockSchema),
|
||||
}) as unknown as z.ZodType<RequestPayload<'subagent.prompt'>>
|
||||
|
||||
/** subagent.interrupt request payload. */
|
||||
export const subagentInterruptRequestSchema = z.object({
|
||||
parentSessionId: sessionIdSchema,
|
||||
childSessionId: sessionIdSchema,
|
||||
mode: z.literal('continuable'),
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'subagent.interrupt'>>>
|
||||
|
||||
/** subagent.interrupt response value. */
|
||||
export const subagentInterruptValueSchema = z.object({
|
||||
accepted: z.literal(true),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'subagent.interrupt'>>>
|
||||
|
||||
const messageIdSchema = z.string() as unknown as z.ZodType<MessageId>
|
||||
|
||||
/** subagent.prompt response value. */
|
||||
|
||||
@@ -40,6 +40,11 @@ export interface SubagentPromptReceipt {
|
||||
messageId: MessageId
|
||||
}
|
||||
|
||||
/** Uniform acknowledgement that one interrupt request was admitted. */
|
||||
export interface SubagentInterruptReceipt {
|
||||
accepted: true
|
||||
}
|
||||
|
||||
/** Durable parent/child address that selects subagent transport in the client. */
|
||||
export type SubagentAddress =
|
||||
& {
|
||||
@@ -94,4 +99,17 @@ export interface SubagentsApi {
|
||||
>,
|
||||
signal: AbortSignal,
|
||||
): Promise<RpcResponse<SubagentPromptReceipt>>
|
||||
|
||||
/**
|
||||
* Interrupts a live continuable child's current turn under the address's
|
||||
* durable direct-parent authority, without requiring a live parent Agent,
|
||||
* consulting the catalog, or resuming anything. Fire-and-return: `accepted`
|
||||
* acknowledges the admitted cancel signal, not target quiescence, so the
|
||||
* child may remain visibly running briefly. Unclaimed queued follow-ups are
|
||||
* kept and parked; an absent, idle, or already-completed target is likewise
|
||||
* `accepted`.
|
||||
*/
|
||||
interrupt(
|
||||
request: RpcRequest<Extract<SubagentAddress, { mode: 'continuable' }>>,
|
||||
): Promise<RpcResponse<SubagentInterruptReceipt>>
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ import {
|
||||
} from '../api/workspace.schema.ts'
|
||||
import { commandExecuteValueSchema, commandListValueSchema } from '../api/commands.schema.ts'
|
||||
import { skillListValueSchema } from '../api/skills.schema.ts'
|
||||
import {
|
||||
agentPresetCopyValueSchema, agentPresetListValueSchema, agentPresetOpenDocumentValueSchema,
|
||||
agentPresetReadValueSchema, agentPresetRemoveValueSchema, agentPresetSelectValueSchema,
|
||||
} from '../api/agent-presets.schema.ts'
|
||||
import {
|
||||
goalCreateValueSchema,
|
||||
goalEditValueSchema,
|
||||
@@ -58,6 +62,7 @@ import {
|
||||
import { llmDiscoverModelsValueSchema, llmModelsValueSchema, llmProvidersValueSchema } from '../api/llm.schema.ts'
|
||||
import {
|
||||
subagentHistoryValueSchema,
|
||||
subagentInterruptValueSchema,
|
||||
subagentListValueSchema,
|
||||
subagentPromptValueSchema,
|
||||
} from '../api/subagents.schema.ts'
|
||||
@@ -96,6 +101,7 @@ export interface IApiClient {
|
||||
list(payload: RequestPayload<'subagent.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'subagent.list'>>>
|
||||
history(payload: RequestPayload<'subagent.history'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'subagent.history'>>>
|
||||
prompt(payload: RequestPayload<'subagent.prompt'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'subagent.prompt'>>>
|
||||
interrupt(payload: RequestPayload<'subagent.interrupt'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'subagent.interrupt'>>>
|
||||
}
|
||||
host: {
|
||||
describe(payload: RequestPayload<'host.describe'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'host.describe'>>>
|
||||
@@ -119,6 +125,14 @@ export interface IApiClient {
|
||||
skills: {
|
||||
list(payload: RequestPayload<'skill.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'skill.list'>>>
|
||||
}
|
||||
agentPresets: {
|
||||
list(payload: RequestPayload<'agentPreset.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.list'>>>
|
||||
select(payload: RequestPayload<'agentPreset.select'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.select'>>>
|
||||
read(payload: RequestPayload<'agentPreset.read'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.read'>>>
|
||||
copy(payload: RequestPayload<'agentPreset.copy'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.copy'>>>
|
||||
openDocument(payload: RequestPayload<'agentPreset.openDocument'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.openDocument'>>>
|
||||
remove(payload: RequestPayload<'agentPreset.remove'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.remove'>>>
|
||||
}
|
||||
events: {
|
||||
mux(payload: Parameters<ApiProxy['events']['mux']>[0]['payload'], signal: AbortSignal, onOpen?: () => void): AsyncIterable<RpcRequest<MuxFrame>>
|
||||
host(payload: Parameters<ApiProxy['events']['host']>[0]['payload'], signal: AbortSignal, onOpen?: () => void): AsyncIterable<RpcRequest<HostFrame>>
|
||||
@@ -171,6 +185,7 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
|
||||
'subagent.list': subagentListValueSchema,
|
||||
'subagent.history': subagentHistoryValueSchema,
|
||||
'subagent.prompt': subagentPromptValueSchema,
|
||||
'subagent.interrupt': subagentInterruptValueSchema,
|
||||
'host.describe': hostDescribeValueSchema,
|
||||
'host.pickDirectory': hostPickDirectoryValueSchema,
|
||||
'host.listDirectory': hostListDirectoryValueSchema,
|
||||
@@ -185,6 +200,12 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
|
||||
'command.list': commandListValueSchema,
|
||||
'command.execute': commandExecuteValueSchema,
|
||||
'skill.list': skillListValueSchema,
|
||||
'agentPreset.list': agentPresetListValueSchema,
|
||||
'agentPreset.select': agentPresetSelectValueSchema,
|
||||
'agentPreset.read': agentPresetReadValueSchema,
|
||||
'agentPreset.copy': agentPresetCopyValueSchema,
|
||||
'agentPreset.openDocument': agentPresetOpenDocumentValueSchema,
|
||||
'agentPreset.remove': agentPresetRemoveValueSchema,
|
||||
'goal.create': goalCreateValueSchema,
|
||||
'goal.edit': goalEditValueSchema,
|
||||
'goal.pause': goalPauseValueSchema,
|
||||
@@ -407,6 +428,7 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
list: (payload, signal) => this.callUnary('subagent.list', payload, signal),
|
||||
history: (payload, signal) => this.callUnary('subagent.history', payload, signal),
|
||||
prompt: (payload, signal) => this.callUnary('subagent.prompt', payload, signal),
|
||||
interrupt: (payload, signal) => this.callUnary('subagent.interrupt', payload, signal),
|
||||
}
|
||||
|
||||
readonly host: IApiClient['host'] = {
|
||||
@@ -443,6 +465,20 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
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'] = {
|
||||
list: (payload, signal) => this.callUnary('agentPreset.list', payload, signal),
|
||||
select: (payload, signal) => this.callUnary('agentPreset.select', payload, signal),
|
||||
read: (payload, signal) => this.callUnary('agentPreset.read', payload, signal),
|
||||
copy: (payload, signal) => this.callUnary('agentPreset.copy', payload, signal),
|
||||
openDocument: (payload, signal) => this.callUnary('agentPreset.openDocument', payload, signal),
|
||||
remove: (payload, signal) => this.callUnary('agentPreset.remove', payload, signal),
|
||||
}
|
||||
|
||||
readonly goals: IApiClient['goals'] = {
|
||||
create: (payload, signal) => this.callUnary('goal.create', payload, signal),
|
||||
edit: (payload, signal) => this.callUnary('goal.edit', payload, signal),
|
||||
|
||||
@@ -42,6 +42,10 @@ import {
|
||||
} from '../api/workspace.schema.ts'
|
||||
import { commandExecuteRequestSchema, commandListRequestSchema } from '../api/commands.schema.ts'
|
||||
import { skillListRequestSchema } from '../api/skills.schema.ts'
|
||||
import {
|
||||
agentPresetCopyRequestSchema, agentPresetListRequestSchema, agentPresetOpenDocumentRequestSchema,
|
||||
agentPresetReadRequestSchema, agentPresetRemoveRequestSchema, agentPresetSelectRequestSchema,
|
||||
} from '../api/agent-presets.schema.ts'
|
||||
import {
|
||||
goalCreateRequestSchema,
|
||||
goalEditRequestSchema,
|
||||
@@ -60,6 +64,7 @@ import {
|
||||
import { llmDiscoverModelsRequestSchema, llmModelsRequestSchema, llmProvidersRequestSchema } from '../api/llm.schema.ts'
|
||||
import {
|
||||
subagentHistoryRequestSchema,
|
||||
subagentInterruptRequestSchema,
|
||||
subagentListRequestSchema,
|
||||
subagentPromptRequestSchema,
|
||||
} from '../api/subagents.schema.ts'
|
||||
@@ -95,6 +100,7 @@ const UNARY_ROUTES: UnaryRoutes = {
|
||||
'subagent.list': { schema: subagentListRequestSchema, invoke: (api, r, signal) => api.subagents.list(r, signal) },
|
||||
'subagent.history': { schema: subagentHistoryRequestSchema, invoke: (api, r, signal) => api.subagents.history(r, signal) },
|
||||
'subagent.prompt': { schema: subagentPromptRequestSchema, invoke: (api, r, signal) => api.subagents.prompt(r, signal) },
|
||||
'subagent.interrupt': { schema: subagentInterruptRequestSchema, invoke: (api, r) => api.subagents.interrupt(r) },
|
||||
'host.describe': { schema: hostDescribeRequestSchema, invoke: (api, r) => api.host.describe(r) },
|
||||
'host.pickDirectory': { schema: hostPickDirectoryRequestSchema, invoke: (api, r, signal) => api.host.pickDirectory(r, signal) },
|
||||
'host.listDirectory': { schema: hostListDirectoryRequestSchema, invoke: (api, r, signal) => api.host.listDirectory(r, signal) },
|
||||
@@ -109,6 +115,12 @@ const UNARY_ROUTES: UnaryRoutes = {
|
||||
'command.list': { schema: commandListRequestSchema, invoke: (api, r) => api.commands.list(r) },
|
||||
'command.execute': { schema: commandExecuteRequestSchema, invoke: (api, r, signal) => api.commands.execute(r, signal) },
|
||||
'skill.list': { schema: skillListRequestSchema, invoke: (api, r) => api.skills.list(r) },
|
||||
'agentPreset.list': { schema: agentPresetListRequestSchema, invoke: (api, r) => api.agentPresets.list(r) },
|
||||
'agentPreset.select': { schema: agentPresetSelectRequestSchema, invoke: (api, r) => api.agentPresets.select(r) },
|
||||
'agentPreset.read': { schema: agentPresetReadRequestSchema, invoke: (api, r) => api.agentPresets.read(r) },
|
||||
'agentPreset.copy': { schema: agentPresetCopyRequestSchema, invoke: (api, r) => api.agentPresets.copy(r) },
|
||||
'agentPreset.openDocument': { schema: agentPresetOpenDocumentRequestSchema, invoke: (api, r, signal) => api.agentPresets.openDocument(r, signal) },
|
||||
'agentPreset.remove': { schema: agentPresetRemoveRequestSchema, invoke: (api, r) => api.agentPresets.remove(r) },
|
||||
'goal.create': { schema: goalCreateRequestSchema, invoke: (api, r) => api.goals.create(r) },
|
||||
'goal.edit': { schema: goalEditRequestSchema, invoke: (api, r) => api.goals.edit(r) },
|
||||
'goal.pause': { schema: goalPauseRequestSchema, invoke: (api, r) => api.goals.pause(r) },
|
||||
|
||||
@@ -7,28 +7,24 @@
|
||||
* `ctx.apiProxy`). Transport-agnostic by design: this package registers no
|
||||
* routes — physical carriers wrap `ctx.apiProxy` themselves.
|
||||
*
|
||||
* The gateway also owns the `api-gateway` settings section: the route a
|
||||
* session starts from when its own log names none. The composition entry is
|
||||
* the shipped default and the section layers the user's choice over it, so
|
||||
* switching models in a conversation is what sets the default for the next
|
||||
* one. Sessions that have already logged a route are never retargeted by it.
|
||||
* The gateway consumes `ctx.agentDefaultModel`, the transport-independent default
|
||||
* shared with direct front doors. Switching models persists through that
|
||||
* service; sessions that have already logged a selection remain unchanged.
|
||||
*/
|
||||
|
||||
import { resolve } from 'node:path'
|
||||
import { Context, Service } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import type { AgentLlmTarget } from '@deepseek-ai/dsh-agent'
|
||||
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
||||
import { installSettingsSection } from '@deepseek-ai/dsh-settings'
|
||||
import type {} from '@deepseek-ai/dsh-agent-default-model'
|
||||
import type { ApiProxy } from './api/index.ts'
|
||||
import { API_GATEWAY_SETTINGS_NAMESPACE, createApiProxy } from './api-proxy.ts'
|
||||
import { createApiProxy } from './api-proxy.ts'
|
||||
|
||||
export type * from './api/index.ts'
|
||||
export { RpcId } from './api/rpc.ts'
|
||||
export { toFetchHandler } from './fetch/handler.ts'
|
||||
export { AbstractApiClient, InProcessApiClient } from './fetch/client.ts'
|
||||
export type { IApiClient } from './fetch/client.ts'
|
||||
export { API_GATEWAY_SETTINGS_NAMESPACE, createApiProxy } from './api-proxy.ts'
|
||||
export { createApiProxy } from './api-proxy.ts'
|
||||
export type { ApiProxyDefaults } from './api-proxy.ts'
|
||||
|
||||
declare module 'cordis' {
|
||||
@@ -38,60 +34,18 @@ declare module 'cordis' {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `api-gateway` settings section: the route a session starts from when its
|
||||
* own log names none. `workspaceRoot` is deliberately not part of it — that is
|
||||
* a launcher fact, not a preference.
|
||||
*/
|
||||
export interface DefaultRouteSettings {
|
||||
/** Default provider route for created agents. */
|
||||
provider: string
|
||||
/** Default model id. */
|
||||
model: string
|
||||
/** Default reasoning effort; absence preserves the adapter/provider default. */
|
||||
reasoningEffort?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway plugin config: host-level agent routing and Workspace creation root.
|
||||
*
|
||||
* `reasoningEffort` is deliberately absent, so the section carries one field
|
||||
* the composition cannot. The seam resolves a section by MERGING the user
|
||||
* layer over the composition entry per field, and an absent key cannot
|
||||
* override a present one — so a composition-set effort would survive every
|
||||
* later switch to a model that has none, and strand it for the next session
|
||||
* to fail on. Effort is a per-model fact anyway: a deployment default belongs
|
||||
* on the adapter profile (`llm-pi-ai`'s `reasoning`, `llm-deepseek`'s own),
|
||||
* which resolves per model rather than per gateway.
|
||||
*/
|
||||
/** Gateway plugin config: the Host-only Workspace creation root. */
|
||||
export interface Config {
|
||||
/** Default provider route for created agents. */
|
||||
provider: string
|
||||
/** Default model id. */
|
||||
model: string
|
||||
/** Parent directory for name-created Workspaces; defaults to the Host cwd. */
|
||||
workspaceRoot?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema of the `api-gateway` section, exported because it IS that section's
|
||||
* contract — the shape anything reading or writing `settings.yaml` addresses.
|
||||
*/
|
||||
export const DEFAULT_ROUTE_SCHEMA: z<DefaultRouteSettings> = z.object({
|
||||
provider: z.string().required(),
|
||||
model: z.string().required(),
|
||||
reasoningEffort: z.string(),
|
||||
})
|
||||
|
||||
/** Project the stored/composed section onto the agent-facing target shape. */
|
||||
function routeTarget(settings: DefaultRouteSettings): AgentLlmTarget {
|
||||
return {
|
||||
provider: settings.provider,
|
||||
model: settings.model,
|
||||
...settings.reasoningEffort === undefined
|
||||
? {}
|
||||
: { reasoningEffort: ReasoningEffortId(settings.reasoningEffort) },
|
||||
}
|
||||
/**
|
||||
* Whether this deployment can hand paths to a native desktop opener —
|
||||
* the `hasDocument` capability the agent-preset roster reports. Absent,
|
||||
* the platform is asked (macOS/Windows/WSL yes; Linux only with a display
|
||||
* server); set it explicitly where detection misleads, e.g. `false` in a
|
||||
* container whose DISPLAY points nowhere a user can see.
|
||||
*/
|
||||
nativeOpen?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,14 +55,13 @@ function routeTarget(settings: DefaultRouteSettings): AgentLlmTarget {
|
||||
*/
|
||||
export class ApiProxyService extends Service implements ApiProxy {
|
||||
static inject = [
|
||||
'agents', 'directoryPicker', 'llm', 'sessions', 'subagents', 'sessionQuery',
|
||||
'agentDefaultModel', 'agents', 'directoryPicker', 'llm', 'sessions', 'subagents', 'sessionQuery',
|
||||
'tools', 'userInteraction', 'workspace',
|
||||
]
|
||||
|
||||
static Config: z<Config> = z.object({
|
||||
provider: z.string().required(),
|
||||
model: z.string().required(),
|
||||
workspaceRoot: z.string(),
|
||||
nativeOpen: z.boolean(),
|
||||
})
|
||||
|
||||
readonly sessions: ApiProxy['sessions']
|
||||
@@ -118,6 +71,7 @@ export class ApiProxyService extends Service implements ApiProxy {
|
||||
readonly commands: ApiProxy['commands']
|
||||
readonly goals: ApiProxy['goals']
|
||||
readonly skills: ApiProxy['skills']
|
||||
readonly agentPresets: ApiProxy['agentPresets']
|
||||
readonly settings: ApiProxy['settings']
|
||||
readonly credentials: ApiProxy['credentials']
|
||||
readonly llm: ApiProxy['llm']
|
||||
@@ -127,32 +81,12 @@ export class ApiProxyService extends Service implements ApiProxy {
|
||||
constructor(ctx: Context, config: Config) {
|
||||
super(ctx, 'apiProxy')
|
||||
const cwd = process.cwd()
|
||||
// The composition entry is the shipped default; the settings section
|
||||
// layers the user's own choice over it, and a deployment without a
|
||||
// settings provider simply keeps the entry.
|
||||
const entry: DefaultRouteSettings = { provider: config.provider, model: config.model }
|
||||
let route: () => DefaultRouteSettings = () => entry
|
||||
installSettingsSection(ctx, API_GATEWAY_SETTINGS_NAMESPACE, DEFAULT_ROUTE_SCHEMA, entry, {
|
||||
setSource: (current) => {
|
||||
route = current
|
||||
},
|
||||
// Nothing registration-level derives from the default: every consumer
|
||||
// reads it through the thunk at the moment it needs a route.
|
||||
onChange: () => {},
|
||||
})
|
||||
const api = createApiProxy(ctx, {
|
||||
defaultTarget: () => routeTarget(route()),
|
||||
// Wholesale, never a merge: switching to a model with no reasoning
|
||||
// effort must clear a stored one, and a merged patch would strand it
|
||||
// for the next session to fail on. This clears it because the entry
|
||||
// below the user layer carries no effort to re-inherit — the reason
|
||||
// `Config` deliberately has no such field. The section holds no
|
||||
// secrets, so there is nothing a replace can collaterally drop.
|
||||
persistDefaultTarget: async (target) => {
|
||||
await ctx.get('settings')?.replace(API_GATEWAY_SETTINGS_NAMESPACE, target)
|
||||
},
|
||||
defaultModelSelection: () => ctx.agentDefaultModel.currentSelection(),
|
||||
saveDefaultModelSelection: selection => ctx.agentDefaultModel.saveSelection(selection),
|
||||
cwd,
|
||||
workspaceRoot: resolve(config.workspaceRoot ?? cwd),
|
||||
...config.nativeOpen === undefined ? {} : { canOpenPath: () => config.nativeOpen as boolean },
|
||||
})
|
||||
this.sessions = api.sessions
|
||||
this.subagents = api.subagents
|
||||
@@ -161,12 +95,13 @@ export class ApiProxyService extends Service implements ApiProxy {
|
||||
this.commands = api.commands
|
||||
this.goals = api.goals
|
||||
this.skills = api.skills
|
||||
this.agentPresets = api.agentPresets
|
||||
this.settings = api.settings
|
||||
this.credentials = api.credentials
|
||||
this.llm = api.llm
|
||||
this.events = api.events
|
||||
// createApiProxy returns closures (no `this` capture); bind only satisfies
|
||||
// the unbound-method lint without changing behavior.
|
||||
// createApiProxy returns closures (no `this` capture), so the bind is
|
||||
// behavior-neutral.
|
||||
this.respond = api.respond.bind(api)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,12 +152,31 @@ async function openNativePathWithIntent(
|
||||
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 seam for deterministic tests.
|
||||
* @param internals - Platform, environment, and runner hooks for deterministic tests.
|
||||
*/
|
||||
export function openNativePath(
|
||||
path: string,
|
||||
@@ -172,7 +191,7 @@ export function openNativePath(
|
||||
* 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 seam for deterministic tests.
|
||||
* @param internals - Platform and runner hooks for deterministic tests.
|
||||
*/
|
||||
export function openNativeTextFile(
|
||||
path: string,
|
||||
|
||||
Reference in New Issue
Block a user