mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
refactor(client): consume migrated Remote namespaces
This commit is contained in:
@@ -5,8 +5,8 @@ import agentPresetsRemote from '@deepseek-ai/dsh-agent-presets/remote'
|
||||
import commandsRemote from '@deepseek-ai/dsh-commands/remote'
|
||||
import settingsControllerRemote from '@deepseek-ai/dsh-api-settings-controller/remote'
|
||||
import goalsRemote from '@deepseek-ai/dsh-goal/remote'
|
||||
import llmRemote from '@deepseek-ai/dsh-llm/remote'
|
||||
import dynamicRemote from '@deepseek-ai/dsh-cordis-host-runner/remote'
|
||||
import fileReferencesRemote from '@deepseek-ai/dsh-file-reference/remote'
|
||||
import pluginInventoryRemote from '@deepseek-ai/dsh-host-plugin-inventory/remote'
|
||||
import messageFeedbackRemote from '@deepseek-ai/dsh-message-feedback/remote'
|
||||
import sessionReferencesRemote from '@deepseek-ai/dsh-session-reference/remote'
|
||||
@@ -20,8 +20,8 @@ export type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inven
|
||||
export type {} from '@deepseek-ai/dsh-agent-presets/remote'
|
||||
export type {} from '@deepseek-ai/dsh-commands/remote'
|
||||
export type {} from '@deepseek-ai/dsh-api-settings-controller/remote'
|
||||
export type {} from '@deepseek-ai/dsh-file-reference/remote'
|
||||
export type {} from '@deepseek-ai/dsh-goal/remote'
|
||||
export type {} from '@deepseek-ai/dsh-llm/remote'
|
||||
export type {} from '@deepseek-ai/dsh-host-plugin-inventory/remote'
|
||||
export type {} from '@deepseek-ai/dsh-message-feedback/remote'
|
||||
export type {} from '@deepseek-ai/dsh-session-reference/remote'
|
||||
@@ -54,11 +54,10 @@ export type {} from '@deepseek-ai/dsh-api-session-controller/types'
|
||||
* the carrier's runtime values stay behind their own module edge.
|
||||
*/
|
||||
export type {
|
||||
ConfigurableProviderView, ConnectionHandle, ConnectionSinks, ContentBlock,
|
||||
DiscoveredModelView, IApiClient,
|
||||
MessageId, ModelCatalog, ModelCatalogFailure, ModelProviderGroup, ModelReasoningEffort, ModelSelection,
|
||||
ConnectionHandle, ConnectionSinks, ContentBlock, IApiClient,
|
||||
MessageId, ModelCatalogFailure, ModelProviderGroup, ModelReasoningEffort, ModelSelection,
|
||||
RpcError, RpcId, RpcRequest, RpcResponse, RpcResult, SessionId,
|
||||
SkillEntry, StreamChunk,
|
||||
StreamChunk,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
export type {} from '@deepseek-ai/dsh-api-gateway/client'
|
||||
export type {} from '@deepseek-ai/dsh-cordis-host-runner/remote'
|
||||
@@ -111,6 +110,11 @@ export type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
|
||||
export type {
|
||||
SettingsDescribeValue, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
|
||||
} from '@deepseek-ai/dsh-settings/types'
|
||||
// Provider registry and discovery vocabulary for the llm namespace.
|
||||
export type {
|
||||
LlmConfigurableProvider, LlmDiscoveredModel, LlmModelDiscoveryError,
|
||||
LlmModelDiscoveryRequest, LlmProviderInfo,
|
||||
} from '@deepseek-ai/dsh-llm/types'
|
||||
// Reference-discovery result vocabulary for the fileReferences and
|
||||
// sessionReferenceResolver namespaces.
|
||||
export type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
|
||||
@@ -123,6 +127,7 @@ export type ClientFailure =
|
||||
| import('@deepseek-ai/dsh-api-session-controller/types').SessionError
|
||||
| import('@deepseek-ai/dsh-api-settings-controller/types').CredentialError
|
||||
| import('@deepseek-ai/dsh-api-settings-controller/types').SettingsError
|
||||
| import('@deepseek-ai/dsh-llm/types').LlmModelDiscoveryError
|
||||
| import('@deepseek-ai/dsh-subagent/client').SubagentControlError
|
||||
| import('@deepseek-ai/dsh-api-workspace-controller/types').WorkspaceError
|
||||
|
||||
@@ -150,8 +155,7 @@ export async function apply(ctx: Context): Promise<() => Promise<void>> {
|
||||
const disposers: Array<() => Promise<void>> = []
|
||||
try {
|
||||
for (const contribution of [
|
||||
agentPresetsRemote, commandsRemote, settingsControllerRemote, goalsRemote, dynamicRemote,
|
||||
fileReferencesRemote,
|
||||
agentPresetsRemote, commandsRemote, settingsControllerRemote, goalsRemote, llmRemote, dynamicRemote,
|
||||
pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote,
|
||||
subagentsRemote, sessionRemote, workspaceRemote,
|
||||
]) {
|
||||
|
||||
@@ -8,11 +8,8 @@
|
||||
export type {
|
||||
ApiProxy, HostApi,
|
||||
ResponseValue,
|
||||
SkillsApi, SkillEntry,
|
||||
ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
|
||||
ModelReasoningEffort, ModelSelection,
|
||||
SettingsApi,
|
||||
ConfigurableProviderView, DiscoveredModelView, LlmApi,
|
||||
} from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
export type {
|
||||
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
||||
|
||||
@@ -551,7 +551,7 @@ const OPENAI_REASONING = {
|
||||
defaultEffort: 'medium',
|
||||
}
|
||||
|
||||
/** Catalog served by `llm.models` (fresh copies per call). */
|
||||
/** Catalog served by `session/modelCatalog` (fresh copies per call). */
|
||||
function fixtureModelGroups(): ModelProviderGroup[] {
|
||||
return [
|
||||
{
|
||||
@@ -1834,6 +1834,25 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
},
|
||||
}
|
||||
},
|
||||
openSettingsDocument(): RpcResult<{ opened: true }> {
|
||||
return { ok: true, value: { opened: true } }
|
||||
},
|
||||
openAgentPresetDirectory(agentPreset: string): RpcResult<
|
||||
{ opened: true } | { opened: false; path: string }
|
||||
> {
|
||||
const existing = fixturePresets.get(agentPreset)
|
||||
if (existing === undefined || existing.trust === 'system') {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'agent-preset-read-only',
|
||||
message: `agent preset "${agentPreset}" ships with the deployment`,
|
||||
details: { agentPreset, reason: 'it ships with the deployment' },
|
||||
},
|
||||
}
|
||||
}
|
||||
return { ok: true, value: { opened: true } }
|
||||
},
|
||||
}
|
||||
|
||||
const credentialRemotes = {
|
||||
@@ -1999,10 +2018,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
function ok<P, T>(request: RpcRequest<P>, value: T): Promise<RpcResponse<T>> {
|
||||
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true, value } })
|
||||
}
|
||||
function err<P, T>(request: RpcRequest<P>, error: Extract<RpcResult<T>, { ok: false }>['error']): Promise<RpcResponse<T>> {
|
||||
return Promise.resolve({ rpcId: request.rpcId, result: { ok: false, error } })
|
||||
}
|
||||
|
||||
function sessionOk<T>(value: T): Promise<ConnectionRpcResult<T>> {
|
||||
return Promise.resolve({ ok: true, value })
|
||||
}
|
||||
@@ -2012,16 +2027,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
}
|
||||
|
||||
const summaryOf = (id: SessionId): FixtureSessionSummary | undefined => sessions.find(s => s.sessionId === id)
|
||||
/** Shared session guard for sessionId-addressed catalog routes: the error
|
||||
* response when the session is unknown, undefined when it exists. */
|
||||
const requireSession = (request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<never>> | undefined => {
|
||||
if (summaryOf(request.payload.sessionId) !== undefined) return undefined
|
||||
return err<{ sessionId: SessionId }, never>(request, {
|
||||
code: 'session-not-found',
|
||||
message: `no session ${request.payload.sessionId}`,
|
||||
details: { sessionId: request.payload.sessionId },
|
||||
})
|
||||
}
|
||||
const requireRemoteSession = (
|
||||
request: { readonly sessionId: SessionId },
|
||||
): Promise<ConnectionRpcResult<never>> | undefined => {
|
||||
@@ -3398,65 +3403,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
describe: request => ok(request, {
|
||||
version: '0.0.0-fixture', cwd: '/tmp/fixture', attachedSessions, home: FIXTURE_HOME, canOpenPath: true,
|
||||
}),
|
||||
openPath: request => ok(request, { opened: true as const }),
|
||||
},
|
||||
agentPresets: {
|
||||
// Native opens are deterministic no-op successes in this fixture, so the
|
||||
// open-directory affordance renders and the path-text fallback stays a
|
||||
// component-test concern.
|
||||
openDocument: (request) => {
|
||||
const { agentPreset } = request.payload
|
||||
const existing = fixturePresets.get(agentPreset)
|
||||
if (existing === undefined || existing.trust === 'system') {
|
||||
return err(request, {
|
||||
code: 'agent-preset-read-only',
|
||||
message: `agent preset "${agentPreset}" ships with the deployment`,
|
||||
details: { agentPreset, reason: 'it ships with the deployment' },
|
||||
})
|
||||
}
|
||||
return ok(request, { opened: true as const })
|
||||
},
|
||||
},
|
||||
|
||||
skills: {
|
||||
list: (request) => {
|
||||
const missing = requireSession(request)
|
||||
if (missing !== undefined) return missing
|
||||
return ok(request, {
|
||||
skills: [
|
||||
{ name: 'fixture-demo', description: 'fixture 技能样本', whenToUse: '仅供 UI 目录渲染验收', modelInvocable: true },
|
||||
{ name: 'fixture-user-only', description: 'fixture 仅用户技能样本', modelInvocable: false },
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
// Native opens are deterministic no-op successes in this fixture, as is host.openPath.
|
||||
openDocument: request => ok(request, { opened: true as const }),
|
||||
},
|
||||
llm: {
|
||||
providers: request => ok(request, {
|
||||
providers: [
|
||||
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true },
|
||||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: true, declared: false },
|
||||
{ provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], active: false, declared: false },
|
||||
// One hand-declared route, so a surface reading this fixture meets
|
||||
// the tagged shape rather than only the shipped one.
|
||||
{ provider: 'acme-gateway', displayName: 'Acme Gateway', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'acme-gateway'], active: true, declared: true },
|
||||
],
|
||||
}),
|
||||
models: request => ok(request, {
|
||||
default: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
routableProviders: ['deepseek-official', 'openai', 'acme-gateway'],
|
||||
groups: fixtureModelGroups(),
|
||||
failures: [],
|
||||
}),
|
||||
// The fixture endpoint is imaginary, so the interrogation answers the
|
||||
// catalog it already serves — enough for a surface to exercise adopting
|
||||
// candidates without a reachable provider.
|
||||
discoverModels: request => ok(request, {
|
||||
models: fixtureModelGroups().flatMap(group => group.models.map(model => ({ id: model.id, name: model.name }))),
|
||||
}),
|
||||
},
|
||||
// Satisfies the ApiProxy contract type only: the browser export button
|
||||
// hands GET /api/session.export to the native download manager, so this
|
||||
@@ -3484,6 +3430,7 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
refs?: readonly string[]
|
||||
value?: string
|
||||
ns?: string
|
||||
settingsNs?: string
|
||||
agentPreset?: string
|
||||
from?: string
|
||||
id?: string
|
||||
@@ -3538,6 +3485,58 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
case 'credentials/set': return Promise.resolve(credentialRemotes.set(args.ref as string))
|
||||
case 'credentials/unset': return Promise.resolve(credentialRemotes.unset(args.ref as string))
|
||||
case 'settings/describe': return Promise.resolve(settingsRemotes.describe())
|
||||
case 'settings/openSettingsDocument': return Promise.resolve(settingsRemotes.openSettingsDocument())
|
||||
case 'settings/openAgentPresetDirectory': return Promise.resolve(
|
||||
settingsRemotes.openAgentPresetDirectory(args.agentPreset as string),
|
||||
)
|
||||
case 'skills/list': {
|
||||
const skillRequest = request as { readonly sessionId: SessionId }
|
||||
const missing = requireRemoteSession(skillRequest)
|
||||
if (missing !== undefined) return missing
|
||||
return sessionOk({
|
||||
skills: [
|
||||
{ name: 'fixture-demo', description: 'fixture 技能样本', whenToUse: '仅供 UI 目录渲染验收', modelInvocable: true },
|
||||
{ name: 'fixture-user-only', description: 'fixture 仅用户技能样本', modelInvocable: false },
|
||||
],
|
||||
})
|
||||
}
|
||||
case 'session/openWorkspacePath': {
|
||||
const pathRequest = request as { readonly sessionId: SessionId; readonly path: string }
|
||||
const missing = requireRemoteSession(pathRequest)
|
||||
return missing ?? sessionOk({ opened: true as const })
|
||||
}
|
||||
case 'session/modelCatalog': return Promise.resolve({
|
||||
ok: true,
|
||||
value: {
|
||||
default: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
routableProviders: ['deepseek-official', 'openai', 'acme-gateway'],
|
||||
groups: fixtureModelGroups(),
|
||||
failures: [],
|
||||
},
|
||||
})
|
||||
case 'llm/listProviders': return Promise.resolve({
|
||||
ok: true,
|
||||
value: [
|
||||
{ id: 'deepseek-official', name: 'DeepSeek' },
|
||||
{ id: 'openai', name: 'openai' },
|
||||
{ id: 'acme-gateway', name: 'Acme Gateway' },
|
||||
],
|
||||
})
|
||||
case 'llm/listConfigurableProviders': return Promise.resolve({
|
||||
ok: true,
|
||||
value: [
|
||||
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] },
|
||||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], declared: false },
|
||||
{ provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], declared: false },
|
||||
{ provider: 'acme-gateway', displayName: 'Acme Gateway', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'acme-gateway'], declared: true },
|
||||
],
|
||||
})
|
||||
// The fixture endpoint is imaginary, so interrogation answers the
|
||||
// catalog it already serves without a network request.
|
||||
case 'llm/discoverModels': return Promise.resolve({
|
||||
ok: true,
|
||||
value: fixtureModelGroups().flatMap(group => group.models.map(model => ({ id: model.id, name: model.name }))),
|
||||
})
|
||||
case 'settings/update': return Promise.resolve(settingsRemotes.update(args.ns as string))
|
||||
case 'settings/replace': return Promise.resolve(settingsRemotes.replace(args.ns as string))
|
||||
case 'settings/mutate': return Promise.resolve(settingsRemotes.mutate(args.ns as string))
|
||||
@@ -3643,13 +3642,13 @@ export class FixtureApiClient extends AbstractApiClient {
|
||||
payload: RequestPayload<K>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RpcResponse<ResponseValue<K>>> {
|
||||
void signal
|
||||
const request = rpcRequest(payload)
|
||||
const full: ClientRequest = { type: 'client-request', rpcId: request.rpcId, method, payload }
|
||||
this.onEnvelope(full)
|
||||
const response = await this.dispatch(
|
||||
method,
|
||||
request as RpcRequest<never>,
|
||||
signal ?? new AbortController().signal,
|
||||
) as RpcResponse<ResponseValue<K>>
|
||||
const fullResponse: ServerResponse = { type: 'server-response', rpcId: response.rpcId, result: response.result }
|
||||
this.onEnvelope(fullResponse)
|
||||
@@ -3660,17 +3659,9 @@ export class FixtureApiClient extends AbstractApiClient {
|
||||
private dispatch(
|
||||
method: keyof RpcMethodMap,
|
||||
request: RpcRequest<never>,
|
||||
signal: AbortSignal,
|
||||
): Promise<RpcResponse<unknown>> {
|
||||
switch (method) {
|
||||
case 'host.describe': return this.api.host.describe(request)
|
||||
case 'host.openPath': return this.api.host.openPath(request, new AbortController().signal)
|
||||
case 'skill.list': return this.api.skills.list(request)
|
||||
case 'agentPreset.openDocument': return this.api.agentPresets.openDocument(request, new AbortController().signal)
|
||||
case 'settings.openDocument': return this.api.settings.openDocument(request, signal)
|
||||
case 'llm.providers': return this.api.llm.providers(request)
|
||||
case 'llm.models': return this.api.llm.models(request)
|
||||
case 'llm.discoverModels': return this.api.llm.discoverModels(request, signal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,11 @@ declare module '@deepseek-ai/cordis' {
|
||||
// ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
|
||||
export type {
|
||||
ApiProxy, HostApi,
|
||||
SkillsApi, SkillEntry,
|
||||
ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
|
||||
MessageId, ModelReasoningEffort, ModelSelection,
|
||||
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
||||
ClientRequest, ServerResponse, RpcMessage,
|
||||
HostDescription, IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
|
||||
SettingsApi,
|
||||
ConfigurableProviderView, DiscoveredModelView, LlmApi,
|
||||
} from './api.ts'
|
||||
export {
|
||||
RpcId,
|
||||
|
||||
@@ -65,7 +65,7 @@ export function apply(ctx: ClientContext): void {
|
||||
// One roster, four surfaces. The chip is registered in a later scope, so it
|
||||
// subscribes here rather than being reached from this one.
|
||||
const rosterReaders = new Set<() => void>()
|
||||
const section = new AgentPresetSectionController({ ...api, ...settingsWire }, ctx.remote, () => {
|
||||
const section = new AgentPresetSectionController(api, ctx.remote, () => {
|
||||
void controller.load()
|
||||
for (const read of rosterReaders) read()
|
||||
})
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import type { ClientRemote, IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SettingsWireFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import { beginRosterRead, messageOf, writeDefaultPreset } from './settings-store.ts'
|
||||
|
||||
@@ -134,8 +133,8 @@ export class AgentPresetSectionController {
|
||||
readonly store: SnapshotStore<AgentPresetSectionState> = createSnapshotStore(INITIAL)
|
||||
|
||||
constructor(
|
||||
private readonly api: SettingsWireFace & Pick<IApiClient, 'agentPresets' | 'host'>,
|
||||
private readonly remote: Pick<ClientRemote, 'agentPresets'>,
|
||||
private readonly api: Pick<IApiClient, 'host'>,
|
||||
private readonly remote: Pick<ClientRemote, 'agentPresets' | 'settings'>,
|
||||
/**
|
||||
* Called after this page changes the roster DIRECTORY, so the other
|
||||
* surfaces reading the same roster re-read it. A settings field moving is
|
||||
@@ -296,13 +295,13 @@ export class AgentPresetSectionController {
|
||||
*/
|
||||
async openLocation(id: string): Promise<void> {
|
||||
try {
|
||||
const response = await this.api.agentPresets.openDocument({ agentPreset: id })
|
||||
if (!response.result.ok) {
|
||||
this.set({ error: response.result.error.message })
|
||||
const result = await this.remote.settings.openAgentPresetDirectory(id)
|
||||
if (!result.ok) {
|
||||
this.set({ error: result.error.message })
|
||||
return
|
||||
}
|
||||
if (response.result.value.opened) return
|
||||
const { path } = response.result.value
|
||||
if (result.value.opened) return
|
||||
const { path } = result.value
|
||||
this.set({ revealedPaths: { ...this.store.getSnapshot().revealedPaths, [id]: path } })
|
||||
} catch (error) {
|
||||
this.set({ error: messageOf(error) })
|
||||
@@ -350,7 +349,7 @@ export class AgentPresetSectionController {
|
||||
* @returns once the write settled and the roster was re-read.
|
||||
*/
|
||||
async makeDefault(id: string): Promise<void> {
|
||||
const failure = await writeDefaultPreset(this.api, id)
|
||||
const failure = await writeDefaultPreset(this.remote, id)
|
||||
if (failure !== undefined) {
|
||||
this.set({ error: failure })
|
||||
return
|
||||
|
||||
@@ -74,8 +74,7 @@
|
||||
"@deepseek-ai/dsh-session-stats": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-util-workspace-path": "workspace:^"
|
||||
"@deepseek-ai/dsh-tools": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
@@ -105,7 +104,6 @@
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-util-workspace-path": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** Register the Chat Conversation target, renderers, stats, and details surface. */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SessionBinding } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { BoundActions, ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path'
|
||||
// Type-only service and declaration merges used by the apply world.
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
@@ -46,7 +46,8 @@ const CHAT_NODE_INJECT: ChatNodeTurnDataInjected = {
|
||||
|
||||
/** Services required by the Chat target and its presentation registrations. */
|
||||
export const inject = [
|
||||
'slots', 'sessions', 'uiSession', 'uiConversation', 'uiWorkspace', 'layout', 'locale', 'settingsScope',
|
||||
'slots', 'sessions', 'uiSession', 'uiConversation', 'layout', 'locale',
|
||||
'settingsScope', 'remote', 'remote.session',
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -115,9 +116,9 @@ export function apply(ctx: Context): void {
|
||||
ctx.layout.openDetails()
|
||||
},
|
||||
fileMentions: (owner: TurnTailOwnerProps) => ctx.get('chatFileMentions')?.forClosing(owner),
|
||||
openFile: (path) => {
|
||||
const cwd = ctx.sessions.list.getSnapshot().byId[sessionId]?.cwd
|
||||
return ctx.uiWorkspace.openPath(resolveWorkspacePath(cwd, path))
|
||||
openFile: async (path) => {
|
||||
const result = await ctx.remote.session.openWorkspacePath({ sessionId, path })
|
||||
if (!result.ok) throw new Error(`path open failed: ${result.error.message}`)
|
||||
},
|
||||
loadOlder: () => { void session.loadOlder() },
|
||||
loadImage: Object.assign(
|
||||
|
||||
@@ -50,9 +50,6 @@
|
||||
{
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../util/workspace-path"
|
||||
},
|
||||
{
|
||||
"path": "../../session/session-stats"
|
||||
},
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
"client": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-api-session-controller",
|
||||
"@deepseek-ai/dsh-client-connection",
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-ui-commands",
|
||||
"@deepseek-ai/dsh-api-remotes"
|
||||
@@ -49,7 +48,6 @@
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
@@ -64,7 +62,6 @@
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-commands": "workspace:^",
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
/** One Host-generation model catalog shared by every Session selector. */
|
||||
|
||||
import {
|
||||
type IApiClient,
|
||||
type ModelCatalog,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ClientRemote, ModelCatalog } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
|
||||
/** Observable lifecycle of the shared model catalog. */
|
||||
@@ -25,8 +22,8 @@ export class ModelCatalogDirectory {
|
||||
private generation = 0
|
||||
private inflight: Promise<ModelCatalog> | undefined
|
||||
|
||||
/** @param api - shared connection API client. */
|
||||
constructor(private readonly api: IApiClient) {}
|
||||
/** @param session - Session Remote namespace carrying the Host-generation catalog. */
|
||||
constructor(private readonly session: Pick<ClientRemote['session'], 'modelCatalog'>) {}
|
||||
|
||||
/**
|
||||
* Return the current generation's catalog, sharing its one in-flight load.
|
||||
@@ -41,14 +38,14 @@ export class ModelCatalogDirectory {
|
||||
draft.status = 'loading'
|
||||
draft.error = null
|
||||
})
|
||||
const operation = this.api.llm.models({}).then((response) => {
|
||||
if (!response.result.ok) {
|
||||
throw new Error(`${response.result.error.code}: ${response.result.error.message}`)
|
||||
const operation = this.session.modelCatalog().then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.error.code}: ${response.error.message}`)
|
||||
}
|
||||
if (generation === this.generation) {
|
||||
this.store.set({ value: response.result.value, status: 'ready', error: null })
|
||||
this.store.set({ value: response.value, status: 'ready', error: null })
|
||||
}
|
||||
return response.result.value
|
||||
return response.value
|
||||
}).catch((error: unknown) => {
|
||||
if (generation === this.generation) {
|
||||
this.store.update((draft) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Model selection plugin, browser half — TWO entries over ONE per-session
|
||||
* directory owned by ModelDirectoryResolver (`ctx.modelDirectories`). The /model popupSelect
|
||||
* contribution and the composer's named `conversation.input.model` seat share
|
||||
* one Host-generation `llm.models` catalog, combine it with the Session's
|
||||
* one Host-generation `session/modelCatalog` catalog, combine it with the Session's
|
||||
* durable model-selection projection, and submit through `session.selectModel`.
|
||||
* A switch made in either entry is what the other shows next. Failures
|
||||
* ride each entry's own retry surface (popup shell error/retry; seat menu
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
import { Service } from '@deepseek-ai/cordis'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { ModelCatalogDirectory } from './catalog.ts'
|
||||
import { ModelDirectory } from './directory.ts'
|
||||
@@ -34,7 +33,7 @@ interface LiveState {
|
||||
|
||||
/** The `ctx.modelDirectories` session model-selection service. */
|
||||
export class ModelDirectoryResolver extends Service {
|
||||
static inject = ['sessions', 'remote', 'remote.session', 'connection']
|
||||
static inject = ['sessions', 'remote', 'remote.session']
|
||||
|
||||
private readonly live: LiveState = { directories: new Map() }
|
||||
private readonly catalog: ModelCatalogDirectory
|
||||
@@ -49,9 +48,7 @@ export class ModelDirectoryResolver extends Service {
|
||||
constructor(ctx: Context, config: { blockReason: () => string }) {
|
||||
super(ctx, 'modelDirectories')
|
||||
this.blockReason = config.blockReason
|
||||
const connection = ctx.get('connection') as ConnectionHandle | undefined
|
||||
if (connection === undefined) throw new Error('ui-model-selection: connection service is unavailable')
|
||||
this.catalog = new ModelCatalogDirectory(connection.api)
|
||||
this.catalog = new ModelCatalogDirectory(ctx.remote.session)
|
||||
void this.catalog.load().catch(() => { /* selectors expose the shared error */ })
|
||||
ctx.on('connection/reset', () => {
|
||||
this.catalog.resetGeneration()
|
||||
|
||||
@@ -55,7 +55,7 @@ const NS = 'settings'
|
||||
* ui-settings' apply, whose activation order relative to this one is NOT
|
||||
* constrained; registrations depend on their slots through `slots.inject()`.
|
||||
*/
|
||||
export const inject = ['slots', 'locale', 'connection', 'settingsScope']
|
||||
export const inject = ['slots', 'locale', 'connection', 'remote', 'remote.settings', 'settingsScope']
|
||||
|
||||
/**
|
||||
* Register the `settings` dictionaries, the chrome content, and the General
|
||||
@@ -72,7 +72,7 @@ export function apply(ctx: ClientContext): void {
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
// The shared SettingsScope mirror updates after document commits and reconnects.
|
||||
const documentController = connection.isLoopback
|
||||
? new SettingsDocumentStore(connection.api, ctx.settingsScope.describe())
|
||||
? new SettingsDocumentStore(ctx.remote, ctx.settingsScope.describe())
|
||||
: undefined
|
||||
const documentInjected = documentController === undefined
|
||||
? undefined
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** State owner for the optional local settings-document action. */
|
||||
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type { SettingsDescribeFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
|
||||
@@ -32,7 +32,7 @@ export class SettingsDocumentStore {
|
||||
* @param describeFace - the shared mirror's describe face (`hasDocument` source).
|
||||
*/
|
||||
constructor(
|
||||
private readonly api: Pick<IApiClient, 'settings'>,
|
||||
private readonly remote: Pick<ClientRemote, 'settings'>,
|
||||
private readonly describeFace: SettingsDescribeFace,
|
||||
) {}
|
||||
|
||||
@@ -63,8 +63,8 @@ export class SettingsDocumentStore {
|
||||
state.error = null
|
||||
})
|
||||
try {
|
||||
const response = await this.api.settings.openDocument({})
|
||||
if (!response.result.ok) throw new Error(response.result.error.message)
|
||||
const result = await this.remote.settings.openSettingsDocument()
|
||||
if (!result.ok) throw new Error(result.error.message)
|
||||
} catch (error) {
|
||||
this.store.update((state) => { state.error = messageOf(error) })
|
||||
} finally {
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
@@ -55,7 +54,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-store": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { DiscoveredModelView, IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { LlmDiscoveredModel } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { formatCapacity, parseCapacity } from './DeepSeekModelsEditor.tsx'
|
||||
import type { DeepSeekModelDraft } from './DeepSeekModelsEditor.tsx'
|
||||
import { messageOf } from './store.ts'
|
||||
import { messageOf, type ModelsWire } from './store.ts'
|
||||
import type { en } from './locales.ts'
|
||||
import styles from './ModelsSection.module.css'
|
||||
|
||||
@@ -80,7 +80,7 @@ export interface ModelListEditorProps {
|
||||
*/
|
||||
probeBlocked?: keyof typeof en | undefined
|
||||
/** Wire face the fetch action calls. */
|
||||
api: Pick<IApiClient, 'llm'>
|
||||
api: Pick<ModelsWire, 'llm'>
|
||||
/** Section copy. */
|
||||
t: (key: keyof typeof en) => string
|
||||
/** Disable every control (read-only deployment or a pending write). */
|
||||
@@ -142,7 +142,7 @@ function capacitySpelling(value: number | undefined): string {
|
||||
}
|
||||
|
||||
/** Adopt a candidate, keeping whatever capacities the provider disclosed. */
|
||||
function adopt(candidate: DiscoveredModelView): ModelDraft {
|
||||
function adopt(candidate: LlmDiscoveredModel): ModelDraft {
|
||||
return {
|
||||
id: candidate.id,
|
||||
...candidate.name === undefined ? {} : { name: candidate.name },
|
||||
@@ -160,7 +160,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
|
||||
const { models, onChange, probe, api, t, disabled } = props
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [failure, setFailure] = useState<string | undefined>(undefined)
|
||||
const [candidates, setCandidates] = useState<readonly DiscoveredModelView[] | undefined>(undefined)
|
||||
const [candidates, setCandidates] = useState<readonly LlmDiscoveredModel[] | undefined>(undefined)
|
||||
const [picked, setPicked] = useState<ReadonlySet<string>>(new Set())
|
||||
// Rows carry an id and a name; capacities are the exception, so they stay
|
||||
// folded until asked for rather than crowding every row with four inputs.
|
||||
@@ -229,18 +229,17 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
|
||||
setBusy(true)
|
||||
setFailure(undefined)
|
||||
try {
|
||||
const response = await api.llm.discoverModels({
|
||||
settingsNs: probe.settingsNs,
|
||||
const response = await api.llm.discoverModels(probe.settingsNs, {
|
||||
...probe.provider === undefined ? {} : { provider: probe.provider },
|
||||
...probe.baseURL === undefined || probe.baseURL.length === 0 ? {} : { baseURL: probe.baseURL },
|
||||
...probe.api === undefined ? {} : { api: probe.api },
|
||||
...probe.apiKey === undefined ? {} : { apiKey: probe.apiKey },
|
||||
})
|
||||
if (!response.result.ok) {
|
||||
setFailure(response.result.error.message)
|
||||
if (!response.ok) {
|
||||
setFailure(response.error.message)
|
||||
return
|
||||
}
|
||||
const found = response.result.value.models
|
||||
const found = response.value
|
||||
if (found.length === 0) {
|
||||
setFailure(t('fetchEmpty'))
|
||||
return
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
* packages/client/AGENTS.md.
|
||||
*/
|
||||
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
// Type-only: pulls the shell's SlotMap merge (the 'settings.section' entry).
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
||||
@@ -42,7 +41,9 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
|
||||
/** Dictionary namespace owned by this plugin. */
|
||||
const NS = 'settings.models'
|
||||
export type { ModelsCredentials, ModelsSettingsState, ModelsWire, ProviderRow } from './store.ts'
|
||||
export type {
|
||||
ModelsCredentials, ModelsLlm, ModelsSettingsState, ModelsWire, ProviderDirectoryEntry, ProviderRow,
|
||||
} from './store.ts'
|
||||
|
||||
/**
|
||||
* Refetch the page snapshot only after its first load: an unopened Models
|
||||
@@ -60,7 +61,7 @@ export function refreshIfLoaded(controller: ModelsSettingsStore): void {
|
||||
* constrained; registration depends on each slot through `slots.inject()`.
|
||||
*/
|
||||
export const inject = [
|
||||
'slots', 'locale', 'connection', 'remote', 'remote.credentials', 'remote.settings',
|
||||
'slots', 'locale', 'remote', 'remote.credentials', 'remote.llm', 'remote.settings',
|
||||
'settingsScope', 'settingsSchema',
|
||||
]
|
||||
|
||||
@@ -73,14 +74,11 @@ export const inject = [
|
||||
export function apply(ctx: ClientContext): void {
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-settings-models: copy dictionaries')
|
||||
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
const schema = createSettingsSchemaOperations(ctx.settingsSchema)
|
||||
// The page's two carriers under one face: model discovery and the catalog
|
||||
// still ride the unary API, while settings and credentials are Remote
|
||||
// namespaces.
|
||||
// Every configuration operation rides its owning Remote namespace.
|
||||
const wire: ModelsWire = {
|
||||
...connection.api,
|
||||
credentials: ctx.remote.credentials,
|
||||
llm: ctx.remote.llm,
|
||||
settings: ctx.remote.settings,
|
||||
}
|
||||
const controller = new ModelsSettingsStore(wire, schema, ctx.settingsScope.describe())
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* without editing it.
|
||||
*
|
||||
* `settings.models.provider-card` is keyed by the row's owning settings
|
||||
* namespace (`ConfigurableProviderView.settingsNs`): an adapter family's
|
||||
* namespace (`ProviderDirectoryEntry.settingsNs`): an adapter family's
|
||||
* companion plugin registers one entry under the family's namespace and
|
||||
* receives every card of that family — shipped, added, and hand-declared rows
|
||||
* alike — while the section never learns what the namespace means. Keying on
|
||||
@@ -17,8 +17,8 @@
|
||||
* the declaration. The types therefore live with their declarer.
|
||||
*/
|
||||
|
||||
import type { ConfigurableProviderView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { ProviderDirectoryEntry } from './store.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface SlotMap {
|
||||
@@ -42,7 +42,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
/** Owner share of one provider-card extension occurrence. */
|
||||
export interface ProviderCardExtrasOwnerProps {
|
||||
/** The card's directory row (route id, display name, settings address, live state). */
|
||||
provider: ConfigurableProviderView
|
||||
provider: ProviderDirectoryEntry
|
||||
/** Whether any layer configures this provider (its profile resolves); `false` while the add-provider draft edits a dormant row. */
|
||||
configured: boolean
|
||||
/** Whether the row's referenced api-key credential is confirmed configured (the page's credential join). */
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/**
|
||||
* Models settings page store: one snapshot joining the configurable-provider
|
||||
* directory (`llm.providers`), the settings namespaces (shared settings mirror),
|
||||
* directory (`llm/listProviders` joined with `llm/listConfigurableProviders`),
|
||||
* the settings namespaces (shared settings mirror),
|
||||
* and the referenced credentials (`credentials/describe`). The host stays the
|
||||
* single fact source — every mutation writes through the wire and the page
|
||||
* re-renders from the next describe, pushed or refetched.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ClientRemote, ConfigurableProviderView, CredentialInfo, IApiClient, SettingsNamespaceView,
|
||||
ClientRemote, CredentialInfo, LlmConfigurableProvider, LlmProviderInfo, SettingsNamespaceView,
|
||||
} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
@@ -23,22 +24,71 @@ const PROBE_ROUTE = '\u0000probe'
|
||||
/** The credentials Remote methods the Models page reads and writes through. */
|
||||
export type ModelsCredentials = Pick<ClientRemote['credentials'], 'describe' | 'set' | 'unset'>
|
||||
|
||||
/** LLM Remote methods used by the Models page. */
|
||||
export type ModelsLlm = Pick<
|
||||
ClientRemote['llm'],
|
||||
'discoverModels' | 'listConfigurableProviders' | 'listProviders'
|
||||
>
|
||||
|
||||
/** One provider row after joining the configurable directory with live routes. */
|
||||
export interface ProviderDirectoryEntry {
|
||||
readonly provider: string
|
||||
readonly displayName: string
|
||||
readonly settingsNs: string
|
||||
readonly settingsPath: readonly string[]
|
||||
readonly active: boolean
|
||||
readonly declared?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Every wire face the Models page reaches: the settings and llm unary domains,
|
||||
* plus the credentials Remote namespace, which is addressed by reference name
|
||||
* and never answers with a value.
|
||||
* Join declared configurable providers with the currently registered routes.
|
||||
* @param registered - live provider routes in registration order.
|
||||
* @param directory - declared configurable providers in declaration order.
|
||||
* @returns declared rows followed by live routes with no declaration.
|
||||
*/
|
||||
export interface ModelsWire extends Pick<IApiClient, 'llm'> {
|
||||
export function joinProviderDirectory(
|
||||
registered: readonly LlmProviderInfo[],
|
||||
directory: readonly LlmConfigurableProvider[],
|
||||
): ProviderDirectoryEntry[] {
|
||||
const active = new Set(registered.map(provider => provider.id))
|
||||
const declared = new Set(directory.map(entry => entry.provider))
|
||||
const rows: ProviderDirectoryEntry[] = directory.map(entry => ({
|
||||
provider: entry.provider,
|
||||
displayName: entry.displayName,
|
||||
settingsNs: entry.settingsNs,
|
||||
settingsPath: [...entry.settingsPath],
|
||||
active: active.has(entry.provider),
|
||||
...entry.declared === undefined ? {} : { declared: entry.declared },
|
||||
}))
|
||||
for (const provider of registered) {
|
||||
if (declared.has(provider.id)) continue
|
||||
rows.push({
|
||||
provider: provider.id,
|
||||
displayName: provider.name,
|
||||
settingsNs: '',
|
||||
settingsPath: [],
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* Every Remote wire face the Models page reaches.
|
||||
*/
|
||||
export interface ModelsWire {
|
||||
/** The settings Remote namespace: the redacted read and the profile writes. */
|
||||
settings: SettingsRemote
|
||||
/** Credential state and writes for the references provider profiles name. */
|
||||
credentials: ModelsCredentials
|
||||
/** Provider directory reads and draft endpoint discovery. */
|
||||
llm: ModelsLlm
|
||||
}
|
||||
|
||||
/** One provider row the page renders. */
|
||||
export interface ProviderRow {
|
||||
/** The directory entry (route id, display name, settings address, live state). */
|
||||
entry: ConfigurableProviderView
|
||||
entry: ProviderDirectoryEntry
|
||||
/** Whether any layer configures this provider (its profile resolves). */
|
||||
configured: boolean
|
||||
/** Whether the user layer alone carries the profile (removal restores the base). */
|
||||
@@ -157,20 +207,22 @@ export class ModelsSettingsStore {
|
||||
async load(): Promise<void> {
|
||||
const generation = ++this.generation
|
||||
this.store.update((s) => { s.status = 'loading'; s.error = null })
|
||||
let providers: ConfigurableProviderView[]
|
||||
let providers: ProviderDirectoryEntry[]
|
||||
let writable: boolean
|
||||
let views: readonly SettingsNamespaceView[]
|
||||
try {
|
||||
const [providersResponse] = await Promise.all([
|
||||
this.api.llm.providers({}),
|
||||
const [registered, declared] = await Promise.all([
|
||||
this.api.llm.listProviders(),
|
||||
this.api.llm.listConfigurableProviders(),
|
||||
this.describeFace.ensure(),
|
||||
])
|
||||
if (!providersResponse.result.ok) throw new Error(providersResponse.result.error.message)
|
||||
if (!registered.ok) throw new Error(registered.error.message)
|
||||
if (!declared.ok) throw new Error(declared.error.message)
|
||||
const mirrored = this.describeFace.getSnapshot()
|
||||
if (mirrored.view === undefined) {
|
||||
throw new Error(mirrored.error ?? 'settings are unavailable in this browser')
|
||||
}
|
||||
providers = providersResponse.result.value.providers
|
||||
providers = joinProviderDirectory(registered.value, declared.value)
|
||||
writable = mirrored.view.writable
|
||||
views = mirrored.view.namespaces
|
||||
} catch (error) {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
* settings scope, which keeps them unaware of one another and of other tabs.
|
||||
*/
|
||||
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
// Type-only: the settings shell's SlotMap merge (the 'settings.section' entry)
|
||||
@@ -54,14 +53,15 @@ export type { WebSearchCardFace, WebSearchCardState } from './web-search-card-co
|
||||
const NS = 'settings.plugins'
|
||||
|
||||
/** Required services (cordis fiber inject). */
|
||||
export const inject = ['slots', 'locale', 'connection', 'remote', 'remote.credentials', 'settingsScope']
|
||||
export const inject = [
|
||||
'slots', 'locale', 'connection', 'remote', 'remote.credentials', 'remote.session', 'settingsScope',
|
||||
]
|
||||
|
||||
/**
|
||||
* Mount the plugin configuration section and the cards this package ships.
|
||||
* @param ctx - the browser plugin context.
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
const { api } = ctx.get('connection') as ConnectionHandle
|
||||
const t = ctx.locale.bind(NS)
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-settings-plugins: section dictionaries')
|
||||
|
||||
@@ -71,7 +71,7 @@ export function apply(ctx: ClientContext): void {
|
||||
ctx.settingsScope.bind({ namespace: WEB_SEARCH_NS }), ctx.remote.credentials)
|
||||
const subagentModelSelection = new SubagentModelSelectionCardController(
|
||||
ctx.settingsScope.bind({ namespace: SUBAGENT_MODEL_SELECTION_NS }),
|
||||
api,
|
||||
ctx.remote.session,
|
||||
)
|
||||
|
||||
// The credential a card reports is not part of any settings section, so its
|
||||
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
/** Staged editor for the Host-owned subagent model allowlist. */
|
||||
|
||||
import type {
|
||||
IApiClient,
|
||||
ClientRemote,
|
||||
ModelProviderGroup,
|
||||
} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
@@ -145,11 +145,11 @@ export class SubagentModelSelectionCardController {
|
||||
|
||||
/**
|
||||
* @param scope - bound `subagent-model-selection` settings scope.
|
||||
* @param api - Host LLM directory face.
|
||||
* @param session - Host Session model-catalog face.
|
||||
*/
|
||||
constructor(
|
||||
private readonly scope: SettingsScope<SubagentModelSelectionSettings>,
|
||||
private readonly api: Pick<IApiClient, 'llm'>,
|
||||
private readonly session: Pick<ClientRemote['session'], 'modelCatalog'>,
|
||||
) {
|
||||
this.store = createSnapshotStore(this.projection())
|
||||
this.unsubscribe = scope.subscribe(() => {
|
||||
@@ -321,11 +321,11 @@ export class SubagentModelSelectionCardController {
|
||||
this.catalogPartial = false
|
||||
this.publish()
|
||||
try {
|
||||
const response = await this.api.llm.models({})
|
||||
const response = await this.session.modelCatalog()
|
||||
if (generation !== this.catalogGeneration) return
|
||||
if (!response.result.ok) throw new Error(response.result.error.message)
|
||||
this.catalogGroups = response.result.value.groups
|
||||
this.catalogPartial = response.result.value.failures.length > 0
|
||||
if (!response.ok) throw new Error(response.error.message)
|
||||
this.catalogGroups = response.value.groups
|
||||
this.catalogPartial = response.value.failures.length > 0
|
||||
this.catalogStatus = 'ready'
|
||||
} catch {
|
||||
if (generation !== this.catalogGeneration) return
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Skill reference plugin, browser half: registers the '/' skill source —
|
||||
* candidates from the skill.list RPC addressed by the per-call session
|
||||
* candidates from the `skills/list` Remote addressed by the per-call session
|
||||
* projection's sessionId (sessions are always agent-backed; the host
|
||||
* resolves cwd from the session header). A pick lands the literal `/name `
|
||||
* text and the prompt ships the same literal (plain-text-reference decision;
|
||||
@@ -10,7 +10,7 @@
|
||||
* leading `/name` naming a user-invocable skill and injects the rendered
|
||||
* body for every entry point, including `disable-model-invocation` skills the
|
||||
* model-side catalog never lists (issue #1470). The RPC rides the plugin's
|
||||
* root-context connection captured at registration — the source never reads
|
||||
* root-context Remote captured at registration — the source never reads
|
||||
* services off a per-call argument. Draft chip visuals derive from
|
||||
* the lexicon scan; this source implements no reference codec.
|
||||
*
|
||||
@@ -31,7 +31,7 @@
|
||||
*/
|
||||
// Type-only: the carrier types, the forwarded Host-event face and the ctx.remote merge.
|
||||
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
||||
import type { ConnectionHandle, SkillEntry } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SkillEntry } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type {} from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { InputTriggerServiceContract, InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
||||
@@ -71,7 +71,7 @@ export function apply(ctx: ClientContext): void {
|
||||
SkillRow,
|
||||
))
|
||||
|
||||
const skills = (ctx.get('connection') as ConnectionHandle).api.skills
|
||||
const skills = ctx.remote.skills
|
||||
const sessions = ctx.sessions
|
||||
// Session-keyed catalog cache; single-flight per key. Plugin-closure state:
|
||||
// the fiber effect below is its teardown boundary.
|
||||
@@ -98,8 +98,8 @@ export function apply(ctx: ClientContext): void {
|
||||
if (existing !== undefined) return existing.promise
|
||||
const abort = new AbortController()
|
||||
const promise = (async () => {
|
||||
const { result } = await skills.list({ sessionId }, abort.signal)
|
||||
if (!result.ok) throw new Error(`skill.list failed: ${result.error.code}: ${result.error.message}`)
|
||||
const result = await skills.list({ sessionId }, abort.signal)
|
||||
if (!result.ok) throw new Error(`skills/list failed: ${result.error.code}: ${result.error.message}`)
|
||||
return result.value.skills
|
||||
})()
|
||||
const entry: CatalogFetch = { promise, abort }
|
||||
|
||||
@@ -75,7 +75,7 @@ export function apply(ctx: Context): void {
|
||||
const workspaces = ctx.get('workspaces') as IWorkspaces
|
||||
const hostDescription = connection.hostDescription
|
||||
const uiWorkspace = new UiWorkspaceService(
|
||||
ctx, connection.api, ctx.remote.directoryPicker, workspaces, sessions)
|
||||
ctx, ctx.remote.directoryPicker, workspaces, sessions)
|
||||
ctx.slots.provideRoot({ hooks: { workspaces: workspaces.list } })
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-workspace: dictionaries')
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/** Workspace archive and directory UI capability. */
|
||||
|
||||
import { Service, type Context } from '@deepseek-ai/cordis'
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ClientRemote, DirectoryListing } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { RemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type {
|
||||
@@ -50,11 +49,6 @@ export interface UiWorkspace {
|
||||
* @returns created absolute path.
|
||||
*/
|
||||
createDirectory(path: string, name: string): Promise<string>
|
||||
/**
|
||||
* Open a path with the Host operating system.
|
||||
* @param path - absolute or Host-resolvable path.
|
||||
*/
|
||||
openPath(path: string): Promise<void>
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
@@ -80,14 +74,12 @@ class UiWorkspaceService extends Service implements UiWorkspace {
|
||||
|
||||
/**
|
||||
* @param ctx - Client root Context.
|
||||
* @param api - shared Host API carrier.
|
||||
* @param directoryPicker - the directory-picking Remote namespace.
|
||||
* @param workspaces - pure Workspace Controller.
|
||||
* @param sessions - pure Session Controller.
|
||||
*/
|
||||
constructor(
|
||||
ctx: Context,
|
||||
private readonly api: IApiClient,
|
||||
private readonly directoryPicker: ClientRemote['directoryPicker'],
|
||||
private readonly workspaces: IWorkspaces,
|
||||
private readonly sessions: ISessions,
|
||||
@@ -163,13 +155,6 @@ class UiWorkspaceService extends Service implements UiWorkspace {
|
||||
return result.value
|
||||
}
|
||||
|
||||
async openPath(path: string): Promise<void> {
|
||||
const response = await this.api.host.openPath({ path })
|
||||
if (!response.result.ok) {
|
||||
throw new Error(`path open failed: ${response.result.error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
private watchNavigation(): () => void {
|
||||
let initial: 'waiting' | 'connecting' | 'done' = 'waiting'
|
||||
let disposed = false
|
||||
|
||||
Reference in New Issue
Block a user