refactor(apiproxy): retire migrated unary routes

This commit is contained in:
imccyu
2026-08-27 21:57:56 +08:00
parent 5b2f679e4a
commit ce3391e280
22 changed files with 14 additions and 1049 deletions
+5
View File
@@ -415,6 +415,11 @@
"tests/**/*.ts"
]
},
"packages/llm/llm": {
"ignoreDependencies": [
"zod"
]
},
"packages/llm/llm-deepseek": {
"entry": [
"tests/**/*.spec.ts",
+1 -8
View File
@@ -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:^"
}
+5 -330
View File
@@ -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<T>(request: RpcRequest<unknown>, value: T): RpcResponse<T> {
return { rpcId: request.rpcId, result: { ok: true, value } }
}
/** Wrap an error result echoing the request's rpcId. */
function err<T>(request: RpcRequest<unknown>, error: RpcError): RpcResponse<T> {
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<void>
/** Native text-editor handoff; injectable for settings-document tests. */
openTextFile?: (path: string, signal: AbortSignal) => Promise<void>
/** 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<ScopeKey | undefined> {
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<unknown>, path: string, signal: AbortSignal,
open: (path: string, signal: AbortSignal) => Promise<void>,
): Promise<RpcResponse<{ opened: true }>> {
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<unknown>, path: string, signal: AbortSignal,
): Promise<RpcResponse<{ opened: true }>> {
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<unknown>, path: string, signal: AbortSignal,
): Promise<RpcResponse<{ opened: true }>> {
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: {
@@ -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<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'>>>
@@ -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<RpcResponse<{ opened: true } | { opened: false; path: string }>>
}
@@ -19,13 +19,3 @@ export const hostDescribeValueSchema = z.object({
home: z.string(),
canOpenPath: z.boolean(),
}) satisfies z.ZodType<Wire<ResponseValue<'host.describe'>>>
/** host.openPath request payload. */
export const hostOpenPathRequestSchema = z.object({
path: z.string().min(1),
}) satisfies z.ZodType<Wire<RequestPayload<'host.openPath'>>>
/** host.openPath response value. */
export const hostOpenPathValueSchema = z.object({
opened: z.literal(true),
}) satisfies z.ZodType<Wire<ResponseValue<'host.openPath'>>>
-10
View File
@@ -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<RpcResponse<{ opened: true }>>
}
-12
View File
@@ -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) ----
@@ -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<Wire<ModelReasoningEffort>>
/** Exact-model reasoning metadata. */
const modelReasoningSchema = z.object({
efforts: z.array(modelReasoningEffortSchema).min(1),
defaultEffort: z.string().min(1).optional(),
}) satisfies z.ZodType<Wire<ModelReasoning>>
/** 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<Wire<ModelCatalogModel>>
/** 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<Wire<ModelProviderGroup>>
/** 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<Wire<ModelCatalogFailure>>
/** 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<Wire<ModelSelection>>
/** 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<Wire<ConfigurableProviderView>>
/** llm.providers request payload. */
export const llmProvidersRequestSchema = z.object({}) satisfies z.ZodType<Wire<RequestPayload<'llm.providers'>>>
/** llm.providers response value. */
export const llmProvidersValueSchema = z.object({
providers: z.array(configurableProviderViewSchema),
}) satisfies z.ZodType<Wire<ResponseValue<'llm.providers'>>>
/** llm.models request payload. */
export const llmModelsRequestSchema = z.object({}) satisfies z.ZodType<Wire<RequestPayload<'llm.models'>>>
/** 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<Wire<ResponseValue<'llm.models'>>>
/** 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<Wire<DiscoveredModelView>>
/** 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<Wire<RequestPayload<'llm.discoverModels'>>>
/** llm.discoverModels response value. */
export const llmDiscoverModelsValueSchema = z.object({
models: z.array(discoveredModelViewSchema),
}) satisfies z.ZodType<Wire<ResponseValue<'llm.discoverModels'>>>
-90
View File
@@ -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<RpcResponse<{ providers: ConfigurableProviderView[] }>>
/**
* 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<RpcResponse<ModelCatalog>>
/**
* 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<RpcResponse<{ models: DiscoveredModelView[] }>>
}
/** 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
}
-11
View File
@@ -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). */
@@ -41,7 +41,6 @@ export const rpcErrorSchema: z.ZodType<RpcError> = 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<RpcError>
-9
View File
@@ -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': {}
}
@@ -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<Wire<RequestPayload<'settings.openDocument'>>>
/** settings.openDocument response value. */
export const settingsOpenDocumentValueSchema = z.object({
opened: z.literal(true),
}) satisfies z.ZodType<Wire<ResponseValue<'settings.openDocument'>>>
@@ -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<RpcResponse<{ opened: true }>>
}
@@ -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<Wire<SkillEntry>>
/** skill.list request payload. */
export const skillListRequestSchema = z.object({
sessionId: sessionIdSchema,
}) satisfies z.ZodType<Wire<RequestPayload<'skill.list'>>>
/** skill.list response value. */
export const skillListValueSchema = z.object({
skills: z.array(skillEntrySchema),
}) satisfies z.ZodType<Wire<ResponseValue<'skill.list'>>>
-33
View File
@@ -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<RpcResponse<{ skills: readonly SkillEntry[] }>>
}
+1 -57
View File
@@ -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<RpcResponse<ResponseValue<'host.describe'>>>
openPath(payload: RequestPayload<'host.openPath'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'host.openPath'>>>
}
skills: {
list(payload: RequestPayload<'skill.list'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'skill.list'>>>
}
agentPresets: {
openDocument(payload: RequestPayload<'agentPreset.openDocument'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'agentPreset.openDocument'>>>
}
settings: {
openDocument(payload: RequestPayload<'settings.openDocument'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'settings.openDocument'>>>
}
llm: {
providers(payload: RequestPayload<'llm.providers'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'llm.providers'>>>
models(payload: RequestPayload<'llm.models'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'llm.models'>>>
discoverModels(payload: RequestPayload<'llm.discoverModels'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'llm.discoverModels'>>>
}
}
@@ -63,13 +38,6 @@ export interface IApiClient {
*/
const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseValue<K>>> } = {
'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),
}
}
+1 -18
View File
@@ -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). */
+1 -12
View File
@@ -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<Config> = 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
}
}
@@ -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<boolean> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
return openNativePathWithIntent(path, signal, 'text-editor', internals)
}
-21
View File
@@ -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"
},