mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge pull request #3085 from deepseek-harness/worktree-apire-c
refactor(apiproxy): credentials and settings to Remote
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
|
||||
2026-07-30-web-config-plane.md: d5bd9c05e8352536c5c6f8b265db7dbd56a4fb84
|
||||
2026-07-30-web-config-plane.zh.md: 3c7f801766b1a6c197cc208a3c4a030b8aaac771
|
||||
2026-07-30-web-config-plane.md: c817071ed17554d06249aa6893ed759bea5d72d0
|
||||
2026-07-30-web-config-plane.zh.md: 0b15e329340051d0f63e8a5b1f8c70a29a2138d2
|
||||
|
||||
@@ -12,7 +12,7 @@ The request-level configuration seam made LLM adapter configuration restart-free
|
||||
|
||||
## Decision
|
||||
|
||||
**Wire domains on the compiled RPC map, rejections as codes, owner events forwarded verbatim.** `settings.describe/openDocument/update/replace/mutate`, `credentials.describe/set/unset`, `llm.providers`, and `llm.models` join `RpcMethodMap`, so the compiler-locked wiring sites keep schema, handler, and client in lockstep. Seam rejections fold into `settings-rejected {ns}` / `credential-rejected {ref}` business errors, while clients subscribe to forwarded settings, credentials, and LLM owner events and converge without polling ([forwarded Remote events](2026-08-10-remote-event-delivery.md)). Connection authenticates settings reads, native actions, writes, `pickDirectory`, `openPath`, and every other Host operation with one browser session; Host/Origin failures still return 403 before identity is checked.
|
||||
**Configuration calls use their owning wire implementation, rejections as codes, and owner events forwarded verbatim.** `@deepseek-ai/dsh-api-settings-controller` owns generated Remote methods for `settings/describe`, `settings/update`, `settings/replace`, `settings/mutate`, and `credentials/describe|set|unset`; `settings.openDocument` and the `llm.*` methods remain in `RpcMethodMap`. Provider absence retains the configuration API's actionable `internal` diagnostic, while seam rejections retain `settings-rejected {ns}` / `settings-conflict {ns, expected, actual}` / `credential-rejected {ref}`. Clients subscribe to forwarded settings, credentials, and LLM owner events and converge without polling ([forwarded Remote events](2026-08-10-remote-event-delivery.md)). Connection authenticates generated Remote methods and API Proxy fallbacks with the same browser session; Host/Origin failures still return 403 before identity is checked.
|
||||
|
||||
**`describe()` grows layers and structural secret redaction.** `SettingsDescriptor` carries `base`/`user` beside the effective value, so the form marks "overridden" by presence in the user layer, not value inequality (an override *equal* to the base is still an override). `describe({ redactSecrets: true })` — mandatory at every wire face — strips `role('secret')` subtrees from all three layers via a pure structural walk of the schema (object/dict/array containers; a secret-role subtree is one opaque leaf) and enumerates the stripped slots as `{path, set}`, so a page can render write-only inputs without ever receiving a value.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
**wire 领域挂上编译期 RPC 映射,拒绝落为错误码,owner 事件原样转发。**`settings.describe/openDocument/update/replace/mutate`、`credentials.describe/set/unset`、`llm.providers` 与 `llm.models` 一同加入 `RpcMethodMap`,由编译器锁定的接线位点让 schema、处理器与客户端保持步调一致。seam 侧拒绝折叠为业务错误,客户端则订阅转发的 settings、credentials 与 LLM owner 事件,无需轮询即可收敛(见[转发的 Remote 事件](2026-08-10-remote-event-delivery.zh.md))。Connection 用一个浏览器会话认证 settings 读取、原生操作、写入、`pickDirectory`、`openPath` 与其他所有 Host 操作;Host/Origin 失败仍会在身份校验前返回 403。
|
||||
**配置调用使用其所属的 wire 实现,拒绝落为错误码,owner 事件原样转发。**`@deepseek-ai/dsh-api-settings-controller` 持有 `settings/describe`、`settings/update`、`settings/replace`、`settings/mutate` 与 `credentials/describe|set|unset` 的生成 Remote 方法;`settings.openDocument` 和 `llm.*` 方法仍位于 `RpcMethodMap`。provider 缺失时保留配置 API 可操作的 `internal` 诊断,seam 拒绝则保留 `settings-rejected {ns}`/`settings-conflict {ns, expected, actual}`/`credential-rejected {ref}`。Client 订阅转发的 settings、credentials 与 LLM owner 事件,无需轮询即可收敛(见[转发的 Remote 事件](2026-08-10-remote-event-delivery.zh.md))。Connection 使用同一个浏览器会话认证生成的 Remote 方法与 API Proxy 回退;Host/Origin 失败仍会在身份校验前返回 403。
|
||||
|
||||
**`describe()` 增加分层与结构化 secret 脱敏。**`SettingsDescriptor` 在生效值之外携带 `base`/`user`,表单据此按「字段是否出现在用户层」来标记「已覆盖」,而非按值是否不等(与 base *相等*的覆盖仍然是覆盖)。`describe({ redactSecrets: true })`——在每个 wire 面都强制启用——经由对 schema 的纯结构遍历(object/dict/array 容器;secret 角色子树整体是一个不透明叶节点)从全部三层剥除 `role('secret')` 子树,并把剥除的槽位枚举为 `{path, set}`,页面因此不必收到任何值就能渲染只写输入框。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md
|
||||
2026-08-10-unary-apiproxy-remote-migration.md: cd019ef10c6d584a98b185ac50856a3fe63b8bd0
|
||||
2026-08-10-unary-apiproxy-remote-migration.zh.md: e6842cc82cfd7f850bb58e9d640ec23394287427
|
||||
2026-08-10-unary-apiproxy-remote-migration.md: b63946b581d3a2afcd159e3b1c0ef44f824aa35c
|
||||
2026-08-10-unary-apiproxy-remote-migration.zh.md: 92f40fc79f2be44855620b6b6915798834284747
|
||||
|
||||
@@ -29,7 +29,8 @@ Large BFF methods remain in `dsh-host-apiproxy`. A method leaves this migration
|
||||
| `session.rename` | `ctx.remote.sessionTitle` in `@deepseek-ai/dsh-session-title` | `SessionTitleService.rename(Session, title)` | Direct `@Remote`; Client maps `eventSeq` to its title projection sequence. |
|
||||
| `command.list`, `command.execute` | `ctx.remote.commands` in `@deepseek-ai/dsh-commands` | `CommandRuntime.list(Agent)`, `execute(Agent, line, signal)` | Direct `@Remote`; Client maps `undefined` to unmatched and preserves caller cancellation. |
|
||||
| `llm.providers` | `ctx.remote.llm` in `@deepseek-ai/dsh-llm` | `LlmRuntime.listProviders()`, `listConfigurableProviders()` | Direct `@Remote` on both reads; the Client joins registration and configuration-directory rows. |
|
||||
| `credentials.describe`, `credentials.set`, `credentials.unset` | `ctx.remote.credentials` in `@deepseek-ai/dsh-credentials-local` | `LocalCredentialProvider.describe(ref)`, `set(ref, value)`, `unset(ref)` | Direct `@Remote`; Client batches `describe` calls when its UI requests several refs. |
|
||||
| `credentials.describe`, `credentials.set`, `credentials.unset` | `ctx.remote.credentials` in `@deepseek-ai/dsh-api-settings-controller` | `CredentialsController.describe(refs)`, `set(ref, value)`, `unset(ref)` | The controller preserves batch size, reference validation, field projection, provider-absence diagnostics, and provider refusal mapping without adding wire behavior to the abstract Definition. |
|
||||
| `settings.describe`, `settings.update`, `settings.replace`, `settings.mutate` | `ctx.remote.settings` in `@deepseek-ai/dsh-api-settings-controller` | `SettingsController.describe()`, `update(ns, patch, expectedRevision)`, `replace(ns, section, expectedRevision)`, `mutate(ns, ops, expectedRevision)` | The controller preserves redaction, all three write operations, optimistic revision checks, provider-absence diagnostics, and failure details. |
|
||||
| `agentPreset.read`, `agentPreset.copy`, `agentPreset.remove` | `ctx.remote.agentPresets` in `@deepseek-ai/dsh-agent-presets` | `readDocument(id)`, `copy(from, id, name?)`, `remove(id)` | `copy` and `remove` are direct; `readDocument` combines stored content with metadata from one live discovery. |
|
||||
| `subagent.interrupt` | `ctx.remote.subagents` in `@deepseek-ai/dsh-subagent` | `interruptByParent(targetSessionId, parentSessionId)` | Adapter constructs the internal user-authority variant without resolving or resuming either Agent. |
|
||||
| `workspace.list`, `workspace.insertSessionBefore`, `workspace.archiveSession` | `ctx.remote.workspace` in `@deepseek-ai/dsh-workspace` | `snapshot()`, `insertSessionBefore(workspaceId, sessionId, before?)`, `archiveSession(sessionId)` | Registry adapters detach mutable entities and return the settled workspace or archive snapshot. |
|
||||
@@ -44,7 +45,7 @@ The Remote API deliberately follows Service names rather than preserving dotted
|
||||
| Session transcript | `session.history`, `attachment`, `subagent.history` | Cold/live logs, pagination, projections, presenters, and attachment authorization. |
|
||||
| Agent model selection | `session.models`, `selectModel` | Per-Agent state, model validation, and default persistence are BFF policy. |
|
||||
| Agent input and control | `session.prompt`, `updateQueue`, `cancel` | Image admission, Inbox mutation, and endpoint-specific live-only semantics. |
|
||||
| Configuration Remote | `settings.describe`, `openDocument`, `update`, `replace`, `mutate` | Namespace exposure, redaction, revision checks, and native opening are product policy. |
|
||||
| Native settings document | `settings.openDocument` | Host path resolution, document preparation, and native opening remain product policy in API Proxy. |
|
||||
| Session skill catalog | `skill.list` | Cold Sessions must not resume; preset standing scope and presenter filtering are BFF joins. |
|
||||
| Host runtime information | `host.describe` | Version, cwd, default model, and attached count combine several Host owners. |
|
||||
| Host path opening | `host.openPath`, `agentPreset.openDocument` | Native desktop authority and cancellation belong to the Host composition. |
|
||||
@@ -72,7 +73,7 @@ Methods whose signatures contain only branded ids do not invoke Typert object lo
|
||||
|
||||
## Client and error behavior
|
||||
|
||||
Generated Remote methods return business values and throw an Error whose `cause` contains the existing RPC failure. Client business services own adaptation to their current result/store interfaces. They must settle successful results immediately exactly as the existing services do so event frames remain idempotent replays rather than the only update path.
|
||||
Generated Remote methods return `RemoteResult` values. Client business services adapt them to their current stores and settle successful results immediately exactly as the existing services do, so event frames remain idempotent replays rather than the only update path. The migration preserves domain validation, provider-absence diagnostics, business error codes, structured details, and successful values; only endpoint addressing, the Remote result envelope, and the separately accepted timeout behavior differ from API Proxy transport.
|
||||
|
||||
Resolver-owned `session-not-found` and `agent-busy` errors remain stable because the shared resolver raises `TypertLookupFailure`. Ordinary business exceptions become the Gateway's existing `internal` RPC failure. A selected Client consumer may migrate only if it does not branch on a more specific legacy business error code; if implementation finds such a branch, that RPC leaves this set unless the business package gains a transport-independent typed failure.
|
||||
|
||||
|
||||
+4
-3
@@ -29,7 +29,8 @@ API Proxy 还包含一些不以业务方法为约定的 BFF 操作:Session 生
|
||||
| `session.rename` | `ctx.remote.sessionTitle`,位于 `@deepseek-ai/dsh-session-title` | `SessionTitleService.rename(Session, title)` | 直接使用 `@Remote`;Client 将 `eventSeq` 映射到自身的标题投影序列。 |
|
||||
| `command.list`、`command.execute` | `ctx.remote.commands`,位于 `@deepseek-ai/dsh-commands` | `CommandRuntime.list(Agent)`、`execute(Agent, line, signal)` | 直接使用 `@Remote`;Client 将 `undefined` 映射为未匹配结果,并保留调用方的取消行为。 |
|
||||
| `llm.providers` | `ctx.remote.llm`,位于 `@deepseek-ai/dsh-llm` | `LlmRuntime.listProviders()`、`listConfigurableProviders()` | 两项读取都直接使用 `@Remote`;Client 关联注册行与配置目录行。 |
|
||||
| `credentials.describe`、`credentials.set`、`credentials.unset` | `ctx.remote.credentials`,位于 `@deepseek-ai/dsh-credentials-local` | `LocalCredentialProvider.describe(ref)`、`set(ref, value)`、`unset(ref)` | 直接使用 `@Remote`;当 UI 请求多个 ref 时,Client 批量发起 `describe` 调用。 |
|
||||
| `credentials.describe`、`credentials.set`、`credentials.unset` | `ctx.remote.credentials`,位于 `@deepseek-ai/dsh-api-settings-controller` | `CredentialsController.describe(refs)`、`set(ref, value)`、`unset(ref)` | controller 保留批量上限、引用校验、字段投影、provider 缺失诊断与 provider 拒绝映射,不给抽象 Definition 增加 wire 行为。 |
|
||||
| `settings.describe`、`settings.update`、`settings.replace`、`settings.mutate` | `ctx.remote.settings`,位于 `@deepseek-ai/dsh-api-settings-controller` | `SettingsController.describe()`、`update(ns, patch, expectedRevision)`、`replace(ns, section, expectedRevision)`、`mutate(ns, ops, expectedRevision)` | controller 保留脱敏、三种写入操作、乐观 revision 校验、provider 缺失诊断与失败 details。 |
|
||||
| `agentPreset.read`、`agentPreset.copy`、`agentPreset.remove` | `ctx.remote.agentPresets`,位于 `@deepseek-ai/dsh-agent-presets` | `readDocument(id)`、`copy(from, id, name?)`、`remove(id)` | `copy` 和 `remove` 直接暴露现有方法;`readDocument` 将存储的内容与一次实时发现取得的元数据组合。 |
|
||||
| `subagent.interrupt` | `ctx.remote.subagents`,位于 `@deepseek-ai/dsh-subagent` | `interruptByParent(targetSessionId, parentSessionId)` | 适配器构造内部的用户权限变体,不解析也不恢复任一 Agent。 |
|
||||
| `workspace.list`、`workspace.insertSessionBefore`、`workspace.archiveSession` | `ctx.remote.workspace`,位于 `@deepseek-ai/dsh-workspace` | `snapshot()`、`insertSessionBefore(workspaceId, sessionId, before?)`、`archiveSession(sessionId)` | 注册表适配器分离可变实体,并返回已完成更新的 workspace 或归档快照。 |
|
||||
@@ -44,7 +45,7 @@ Remote API 有意采用服务名称,而不保留旧 RPC 的点分名称。例
|
||||
| Session transcript | `session.history`、`attachment`、`subagent.history` | cold/live 日志、分页、投影、呈现器和附件授权。 |
|
||||
| Agent 模型选择 | `session.models`、`selectModel` | 各 Agent 的状态、模型校验和默认值持久化属于 BFF 策略。 |
|
||||
| Agent 输入与控制 | `session.prompt`、`updateQueue`、`cancel` | 图片准入、Inbox 变更和端点特有的仅限 live 语义。 |
|
||||
| 配置 Remote | `settings.describe`、`openDocument`、`update`、`replace`、`mutate` | namespace 暴露、脱敏、修订检查和原生打开操作属于产品策略。 |
|
||||
| 原生 settings 文档 | `settings.openDocument` | Host 路径解析、文档准备和原生打开仍属于 API Proxy 中的产品策略。 |
|
||||
| Session skill 目录 | `skill.list` | 不得恢复冷 Session;preset 的常驻 scope 和呈现器过滤属于 BFF 关联操作。 |
|
||||
| Host 运行时信息 | `host.describe` | 版本、cwd、默认模型和当前已附加的 Session 数量来自多个 Host 所有者。 |
|
||||
| Host 路径打开 | `host.openPath`、`agentPreset.openDocument` | 原生桌面权限和取消属于 Host 组合。 |
|
||||
@@ -72,7 +73,7 @@ Lookup 策略作用于整个 key,而非特定端点。提示词输入、队列
|
||||
|
||||
## Client 与错误行为
|
||||
|
||||
生成的 Remote 方法返回业务值,并抛出一个 Error,其 `cause` 包含现有的 RPC 失败。Client 业务服务负责适配到当前的结果/store 接口。它们必须与现有服务一样让成功结果立即生效,使事件帧仍是幂等回放,而非唯一的更新路径。
|
||||
生成的 Remote 方法返回 `RemoteResult` 值。Client 业务服务负责把它们适配到现有 store,并与既有服务一样让成功结果立即生效,使事件帧仍是幂等回放,而非唯一的更新路径。迁移保留领域校验、provider 缺失诊断、业务错误码、结构化 details 与成功值;只有 endpoint 寻址、Remote 结果信封和另行接受的超时行为不同于 API Proxy 传输。
|
||||
|
||||
Resolver 拥有的 `session-not-found` 和 `agent-busy` 错误保持稳定,因为共享 resolver 会抛出 `TypertLookupFailure`。普通业务异常会变成 Gateway 现有的 `internal` RPC 失败。只有在选定的 Client 消费方不根据更具体的旧版业务错误码进行分支时,才能迁移该调用;如果实现过程中发现这种分支,除非业务包新增与传输无关的类型化失败,否则该 RPC 将退出此集合。
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
// Regression pin for the reload flash: both steps are satisfied, yet each
|
||||
// must load private facts before deciding not to show. Dialog chrome lives
|
||||
// inside each visible branch, so the deciding window paints and blocks
|
||||
// nothing. Holding settings.describe widens that window from loopback
|
||||
// nothing. Holding settings/describe widens that window from loopback
|
||||
// RTT scale to a deterministic hundreds of milliseconds, removing all
|
||||
// timing dependence from the sampler assertions below.
|
||||
//
|
||||
@@ -162,7 +162,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
if (document.getElementById('root')?.inert === true) sightings.push('inert')
|
||||
}, 8)
|
||||
})
|
||||
// EVERY settings.describe issued before the release is held — not just
|
||||
// EVERY settings/describe issued before the release is held — not just
|
||||
// the first — so the pin cannot silently collapse back to loopback
|
||||
// timing if a second boot-time consumer of the join ever appears.
|
||||
let released = false
|
||||
@@ -171,7 +171,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
released = true
|
||||
for (const resolve of heldRoutes.splice(0)) resolve()
|
||||
}
|
||||
await page.route('**/api/settings.describe', async (route) => {
|
||||
await page.route('**/api/settings/describe', async (route) => {
|
||||
if (!released) await new Promise<void>((resolve) => { heldRoutes.push(resolve) })
|
||||
await route.continue()
|
||||
})
|
||||
@@ -182,7 +182,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
await page.waitForTimeout(600)
|
||||
releaseDescribe()
|
||||
await page.waitForTimeout(400)
|
||||
await page.unroute('**/api/settings.describe')
|
||||
await page.unroute('**/api/settings/describe')
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
|
||||
expect(await page.evaluate(() =>
|
||||
(window as unknown as { __takeoverSightings: string[] }).__takeoverSightings)).toEqual([])
|
||||
|
||||
@@ -317,17 +317,6 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
|
||||
type Result<T> = { result: { ok: true; value: T } | { ok: false; error: { code: string; message: string } } }
|
||||
interface PreviewApi {
|
||||
skills: { list(payload: { sessionId: string }): Promise<Result<{ skills: unknown[] }>> }
|
||||
settings: {
|
||||
describe(payload: object): Promise<Result<{ namespaces: Array<{ ns: string; revision: number }> }>>
|
||||
update(payload: { ns: string; patch: object; expectedRevision: number }): Promise<Result<unknown>>
|
||||
}
|
||||
credentials: {
|
||||
set(payload: { ref: string; value: string }): Promise<Result<unknown>>
|
||||
unset(payload: { ref: string }): Promise<Result<unknown>>
|
||||
describe(payload: { refs: string[] }): Promise<Result<{
|
||||
credentials: Record<string, { configured: boolean }>
|
||||
}>>
|
||||
}
|
||||
}
|
||||
interface PreviewTransport {
|
||||
fetch(input: string, init: RequestInit): Promise<Response>
|
||||
@@ -373,22 +362,29 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
|
||||
if (!refreshed.result.ok) throw new Error(`skill.list refresh failed: ${refreshed.result.error.message}`)
|
||||
}
|
||||
await createDirectory('/dsh/workspace/.agents/skills', 'runtime-created')
|
||||
const settings = await api.settings.describe({})
|
||||
if (!settings.result.ok) throw new Error(`settings.describe failed: ${settings.result.error.message}`)
|
||||
const shell = settings.result.value.namespaces.find(namespace => namespace.ns === 'shell')
|
||||
if (shell === undefined) throw new Error('settings.describe omitted the shell namespace')
|
||||
const updated = await api.settings.update({ ns: 'shell', patch: { timeoutMs: 61_000 }, expectedRevision: shell.revision })
|
||||
if (!updated.result.ok) throw new Error(`settings.update failed: ${updated.result.error.message}`)
|
||||
const stored = await api.credentials.set({ ref: 'PREVIEW_TEST_SECRET', value: 'worker-only' })
|
||||
if (!stored.result.ok) throw new Error(`credentials.set failed: ${stored.result.error.message}`)
|
||||
const credentials = await api.credentials.describe({ refs: ['PREVIEW_TEST_SECRET'] })
|
||||
if (!credentials.result.ok) throw new Error(`credentials.describe failed: ${credentials.result.error.message}`)
|
||||
const removed = await api.credentials.unset({ ref: 'PREVIEW_TEST_SECRET' })
|
||||
if (!removed.result.ok) throw new Error(`credentials.unset failed: ${removed.result.error.message}`)
|
||||
// Settings and credentials both answer over the Remote carrier, so this
|
||||
// half of the sweep posts the generated endpoints directly like the
|
||||
// session read above.
|
||||
const settings = await remote<{ namespaces: { ns: string; revision: number }[] }>(
|
||||
'settings/describe', {},
|
||||
)
|
||||
const shell = settings.namespaces.find(namespace => namespace.ns === 'shell')
|
||||
if (shell === undefined) throw new Error('settings/describe omitted the shell namespace')
|
||||
await remote('settings/update', {
|
||||
ns: 'shell',
|
||||
patch: { timeoutMs: 61_000 },
|
||||
expectedRevision: shell.revision,
|
||||
})
|
||||
await remote('credentials/set', { ref: 'PREVIEW_TEST_SECRET', value: 'worker-only' })
|
||||
const credentials = await remote<Record<string, { configured: boolean }>>(
|
||||
'credentials/describe',
|
||||
{ refs: ['PREVIEW_TEST_SECRET'] },
|
||||
)
|
||||
await remote('credentials/unset', { ref: 'PREVIEW_TEST_SECRET' })
|
||||
await new Promise((resolve) => { setTimeout(resolve, 250) })
|
||||
return {
|
||||
skillCount: skills.result.value.skills.length,
|
||||
credentialConfigured: credentials.result.value.credentials.PREVIEW_TEST_SECRET?.configured,
|
||||
credentialConfigured: credentials.PREVIEW_TEST_SECRET?.configured,
|
||||
}
|
||||
})
|
||||
expect(exercised.skillCount).toBeGreaterThan(0)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Cold boot may issue at most two settings.describe calls regardless of client
|
||||
// Cold boot may issue at most two settings/describe calls regardless of client
|
||||
// plugin count. No model call or replay fixture is involved.
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
@@ -38,7 +38,7 @@ describe('startup RPC budget', () => {
|
||||
// absorbs the first-connection reset wave the budget must include.
|
||||
await page.getByRole('textbox', { name: 'Choose workspace' }).waitFor({ timeout: 30_000 })
|
||||
await page.waitForTimeout(3000)
|
||||
const describeCount = calls.filter(method => method === 'settings.describe').length
|
||||
const describeCount = calls.filter(method => method === 'settings/describe').length
|
||||
expect(describeCount, `startup /api calls:\n${calls.join('\n')}`).toBe(DESCRIBE_BUDGET)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/capability-seams.md
|
||||
capability-seams.md: 7cf93600cbf0d2deef586135f299058629732685
|
||||
capability-seams.zh.md: 3d79717d811fc3cd4e51c19c34c02a4e51d5da21
|
||||
capability-seams.md: 1e7e6e39d307a9e72b5d57420bde99f51063f64d
|
||||
capability-seams.zh.md: e33a1e6da7f71838c48b961f93389ba1a089f57f
|
||||
|
||||
@@ -38,6 +38,9 @@ flowchart LR
|
||||
pkg_invariants["invariants"]
|
||||
pkg_message_feedback["message-feedback"]
|
||||
svc_sessionController["ctx.sessionController<br/>Host Session Remote controller"]
|
||||
pkg_api_settings_controller["api-settings-controller"]
|
||||
svc_credentialsController["ctx.credentialsController<br/>Host credential-surface Remote controller"]
|
||||
svc_settingsController["ctx.settingsController<br/>Host settings-surface Remote controller"]
|
||||
pkg_api_workspace_controller["api-workspace-controller"]
|
||||
svc_workspaceController["ctx.workspaceController<br/>Host Workspace Remote controller"]
|
||||
svc_directoryPickerController["ctx.directoryPickerController<br/>Host directory-picking Remote controller"]
|
||||
@@ -218,6 +221,8 @@ flowchart LR
|
||||
pkg_agent_presets --> svc_agentPresets
|
||||
pkg_api_gateway --> svc_typertGateway
|
||||
pkg_api_session_controller --> svc_sessionController
|
||||
pkg_api_settings_controller --> svc_credentialsController
|
||||
pkg_api_settings_controller --> svc_settingsController
|
||||
pkg_api_workspace_controller --> svc_directoryPickerController
|
||||
pkg_api_workspace_controller --> svc_workspaceController
|
||||
pkg_attachment --> svc_attachments
|
||||
@@ -460,6 +465,8 @@ flowchart LR
|
||||
| `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
|
||||
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. |
|
||||
| `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Owns Session commands, cold reads, durable-event following, live control state, and Agent activation policy; apiProxy reuses its inspection and Agent-resolution operations for Session-aware domains. |
|
||||
| `ctx.credentialsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | Projects the credential-reference seam onto the generated Remote namespace: batch fan-out, view projection, and refusal mapping live here, not on the seam Definition. |
|
||||
| `ctx.settingsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | Projects the user-settings seam onto the generated Remote namespace: the read is always redacted and every refusal is classified here, not on the seam Definition. |
|
||||
| `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Owns Workspace commands and reconnect-safe Workspace state delivery through the generated Remote namespace. |
|
||||
| `ctx.directoryPickerController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Carries the picking seam onto the wire: capability gating, cancellation, and the seam-coded failures a browser directory flow discriminates on. |
|
||||
| `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
|
||||
|
||||
@@ -40,6 +40,9 @@ flowchart LR
|
||||
pkg_invariants["invariants"]
|
||||
pkg_message_feedback["message-feedback"]
|
||||
svc_sessionController["ctx.sessionController<br/>Host Session Remote controller"]
|
||||
pkg_api_settings_controller["api-settings-controller"]
|
||||
svc_credentialsController["ctx.credentialsController<br/>Host credential-surface Remote controller"]
|
||||
svc_settingsController["ctx.settingsController<br/>Host settings-surface Remote controller"]
|
||||
pkg_api_workspace_controller["api-workspace-controller"]
|
||||
svc_workspaceController["ctx.workspaceController<br/>Host Workspace Remote controller"]
|
||||
svc_directoryPickerController["ctx.directoryPickerController<br/>Host directory-picking Remote controller"]
|
||||
@@ -220,6 +223,8 @@ flowchart LR
|
||||
pkg_agent_presets --> svc_agentPresets
|
||||
pkg_api_gateway --> svc_typertGateway
|
||||
pkg_api_session_controller --> svc_sessionController
|
||||
pkg_api_settings_controller --> svc_credentialsController
|
||||
pkg_api_settings_controller --> svc_settingsController
|
||||
pkg_api_workspace_controller --> svc_directoryPickerController
|
||||
pkg_api_workspace_controller --> svc_workspaceController
|
||||
pkg_attachment --> svc_attachments
|
||||
@@ -462,6 +467,8 @@ flowchart LR
|
||||
| `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
|
||||
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
|
||||
| `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | 负责 Session 命令、冷读取、持久事件跟随、实时控制状态与 Agent 激活策略;apiProxy 在需要 Session 上下文的领域中复用其检查和 Agent 解析操作。 |
|
||||
| `ctx.credentialsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | 把凭据引用 seam 投影到生成的 Remote namespace:批量扇出、视图投影与拒绝映射都在这里,而不在 seam Definition 上。 |
|
||||
| `ctx.settingsController` | `core` | [`api-settings-controller`](../packages/api/settings-controller) | - | - | - | 把用户设置 seam 投影到生成的 Remote namespace:读取一律脱敏,所有拒绝在这里分类,而不在 seam Definition 上。 |
|
||||
| `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 通过生成的 Remote namespace 负责 Workspace 命令和可在重连后收敛的 Workspace 状态投递。 |
|
||||
| `ctx.directoryPickerController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 把选目录 seam 送上线:能力门禁、取消传播,以及浏览器目录流程用于分支判断的 seam 错误码。 |
|
||||
| `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | 配套子路径注册所属包本地的检查;该服务负责选择、唯一性、子 fiber,以及标明所属包的失败。 |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||
config-catalog.md: 6e001376cfba7c04a07acd56ac733f089283f0bf
|
||||
config-catalog.zh.md: 8b1ab084668ef8e1568801855f9aebc19e76a5e3
|
||||
config-catalog.md: 7f85b870bd9604ed983b0d8a251a3ee4511a52b7
|
||||
config-catalog.zh.md: 1ab6838e4cea77a7d98a2227aca6e8ac47d84fcd
|
||||
|
||||
@@ -3319,6 +3319,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
- `@deepseek-ai/dsh-agent` ([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-gateway` — requires `typert` ([`packages/api/gateway/src/index.ts`](../packages/api/gateway/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-remotes` — requires `typertGateway` ([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-settings-controller` ([`packages/api/settings-controller/src/index.ts`](../packages/api/settings-controller/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-workspace-controller` — requires `typert` · `workspaceRegistry` ([`packages/api/workspace-controller/src/index.ts`](../packages/api/workspace-controller/src/index.ts))
|
||||
- `@deepseek-ai/dsh-authorization` — requires `credentials` ([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-locale` ([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))
|
||||
|
||||
@@ -3321,6 +3321,7 @@ export interface Config {
|
||||
- `@deepseek-ai/dsh-agent`([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-gateway` — 需要 `typert`([`packages/api/gateway/src/index.ts`](../packages/api/gateway/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-remotes` — 需要 `typertGateway`([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-settings-controller`([`packages/api/settings-controller/src/index.ts`](../packages/api/settings-controller/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-workspace-controller` — 需要 `typert` · `workspaceRegistry`([`packages/api/workspace-controller/src/index.ts`](../packages/api/workspace-controller/src/index.ts))
|
||||
- `@deepseek-ai/dsh-authorization` — 需要 `credentials`([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-locale`([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
|
||||
event-producer-consumer.md: bcf7ec4f3418d6b598b7edecfb2e4386f1a42e97
|
||||
event-producer-consumer.zh.md: beba6bee877989ca2fc3d33d26a989be30069c33
|
||||
event-producer-consumer.md: 91bfb814a4370fdafb6d62bc79259495b0069dda
|
||||
event-producer-consumer.zh.md: e7dcd089f28888bdb235ee0a6df97dc4189f0723
|
||||
|
||||
@@ -35,8 +35,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:397`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:367`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:373`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:87`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:75`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
|
||||
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
|
||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
|
||||
@@ -49,8 +49,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:178`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:152`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
| `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:397`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:367`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:373`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:87`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:75`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
|
||||
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
|
||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
|
||||
@@ -51,8 +51,8 @@
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:178`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
|
||||
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:152`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/module-graph.md
|
||||
module-graph.md: 1668d509cea592acc6a6bfd15bb4ba0e5d55d5c8
|
||||
module-graph.zh.md: 03edacc1abfe4bfdc0037dd4712e0ce67a014103
|
||||
module-graph.md: 93170bd4d76d768d1cf5f1bc5efdf94d1ce2453a
|
||||
module-graph.zh.md: ac7f7e70d2364469ce5de5bf0734457e8e4dfb3e
|
||||
|
||||
+22
-14
@@ -108,6 +108,7 @@ flowchart TD
|
||||
pkg_api_gateway["api-gateway"]
|
||||
pkg_api_remotes["api-remotes"]
|
||||
pkg_api_session_controller["api-session-controller"]
|
||||
pkg_api_settings_controller["api-settings-controller"]
|
||||
pkg_api_workspace_controller["api-workspace-controller"]
|
||||
end
|
||||
subgraph group_attachment["packages/attachment"]
|
||||
@@ -410,8 +411,6 @@ flowchart TD
|
||||
pkg_anonymous_user_id --> pkg_brand
|
||||
pkg_anonymous_user_id --> pkg_home_paths
|
||||
pkg_anonymous_user_id --> pkg_invariants
|
||||
pkg_settings --> pkg_brand
|
||||
pkg_settings --> pkg_invariants
|
||||
pkg_storage_domain --> pkg_invariants
|
||||
pkg_storage_domain --> pkg_storage
|
||||
pkg_storage_json --> pkg_invariants
|
||||
@@ -438,10 +437,6 @@ flowchart TD
|
||||
pkg_credentials_local --> pkg_home_paths
|
||||
pkg_credentials_local --> pkg_invariants
|
||||
pkg_credentials_local --> pkg_launch_environment
|
||||
pkg_settings_file --> pkg_atomic_write
|
||||
pkg_settings_file --> pkg_home_paths
|
||||
pkg_settings_file --> pkg_invariants
|
||||
pkg_settings_file --> pkg_settings
|
||||
pkg_session --> pkg_brand
|
||||
pkg_session --> pkg_invariants
|
||||
pkg_session --> pkg_llm
|
||||
@@ -504,6 +499,9 @@ flowchart TD
|
||||
pkg_session_persistence --> pkg_timeout
|
||||
pkg_session_projection --> pkg_invariants
|
||||
pkg_session_projection --> pkg_session
|
||||
pkg_settings --> pkg_brand
|
||||
pkg_settings --> pkg_invariants
|
||||
pkg_settings --> pkg_session
|
||||
pkg_session_snapshot --> pkg_invariants
|
||||
pkg_session_snapshot --> pkg_session
|
||||
pkg_llm_retry --> pkg_agent
|
||||
@@ -537,6 +535,11 @@ flowchart TD
|
||||
pkg_web_search_deepseek --> pkg_web
|
||||
pkg_spill_local --> pkg_invariants
|
||||
pkg_spill_local --> pkg_spill
|
||||
pkg_api_settings_controller --> pkg_credentials
|
||||
pkg_api_settings_controller --> pkg_invariants
|
||||
pkg_api_settings_controller --> pkg_session
|
||||
pkg_api_settings_controller --> pkg_settings
|
||||
pkg_api_settings_controller --> pkg_typert_protocol
|
||||
pkg_file_reference --> pkg_agent
|
||||
pkg_file_reference --> pkg_invariants
|
||||
pkg_file_reference --> pkg_typert_protocol
|
||||
@@ -605,6 +608,10 @@ flowchart TD
|
||||
pkg_session_title --> pkg_llm
|
||||
pkg_session_title --> pkg_session
|
||||
pkg_session_title --> pkg_session_projection
|
||||
pkg_settings_file --> pkg_atomic_write
|
||||
pkg_settings_file --> pkg_home_paths
|
||||
pkg_settings_file --> pkg_invariants
|
||||
pkg_settings_file --> pkg_settings
|
||||
pkg_shell --> pkg_invariants
|
||||
pkg_shell --> pkg_sandbox
|
||||
pkg_shell --> pkg_settings
|
||||
@@ -1088,6 +1095,7 @@ flowchart TD
|
||||
pkg_client_connection --> pkg_invariants
|
||||
pkg_client_connection --> pkg_llm
|
||||
pkg_client_connection --> pkg_session
|
||||
pkg_client_connection --> pkg_settings
|
||||
pkg_client_connection --> pkg_tool_todo
|
||||
pkg_compaction_basic --> pkg_agent
|
||||
pkg_compaction_basic --> pkg_commands
|
||||
@@ -1292,6 +1300,7 @@ flowchart TD
|
||||
pkg_api_remotes --> pkg_agent_presets
|
||||
pkg_api_remotes --> pkg_api_gateway
|
||||
pkg_api_remotes --> pkg_api_session_controller
|
||||
pkg_api_remotes --> pkg_api_settings_controller
|
||||
pkg_api_remotes --> pkg_api_workspace_controller
|
||||
pkg_api_remotes --> pkg_commands
|
||||
pkg_api_remotes --> pkg_cordis_host_runner
|
||||
@@ -1334,7 +1343,6 @@ flowchart TD
|
||||
pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_settings
|
||||
pkg_client_ui_settings_plugin_inventory --> pkg_invariants
|
||||
pkg_client_ui_settings_plugins --> pkg_api_remotes
|
||||
pkg_client_ui_settings_plugins --> pkg_client_connection
|
||||
pkg_client_ui_settings_plugins --> pkg_client_locale
|
||||
pkg_client_ui_settings_plugins --> pkg_client_ui_renderer
|
||||
pkg_client_ui_settings_plugins --> pkg_client_ui_settings
|
||||
@@ -1616,7 +1624,6 @@ flowchart TD
|
||||
pkg_client_ui_model_selection --> pkg_typert_protocol
|
||||
pkg_client_ui_permission_presets --> pkg_api_remotes
|
||||
pkg_client_ui_permission_presets --> pkg_api_session_controller
|
||||
pkg_client_ui_permission_presets --> pkg_client_connection
|
||||
pkg_client_ui_permission_presets --> pkg_client_locale
|
||||
pkg_client_ui_permission_presets --> pkg_client_ui_commands
|
||||
pkg_client_ui_permission_presets --> pkg_client_ui_input_trigger
|
||||
@@ -1728,7 +1735,6 @@ flowchart TD
|
||||
| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/runtime-diagnostics/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`anonymous-user-id`](../packages/identity/anonymous-user-id) | `identity` | [`brand`](../packages/util/brand), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
|
||||
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
|
||||
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
|
||||
@@ -1738,7 +1744,6 @@ flowchart TD
|
||||
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment) |
|
||||
| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||
| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||
@@ -1758,6 +1763,7 @@ flowchart TD
|
||||
| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
||||
| [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
||||
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |
|
||||
@@ -1765,6 +1771,7 @@ flowchart TD
|
||||
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
|
||||
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
|
||||
| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
|
||||
@@ -1780,6 +1787,7 @@ flowchart TD
|
||||
| [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
@@ -1858,7 +1866,7 @@ flowchart TD
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-directory-picker`](../packages/host/directory-picker), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) |
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-directory-picker`](../packages/host/directory-picker), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`tool-todo`](../packages/todo/tool-todo) |
|
||||
| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
|
||||
@@ -1886,13 +1894,13 @@ flowchart TD
|
||||
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
|
||||
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-settings-controller`](../packages/api/settings-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`client-ui-session`](../packages/client/ui-session) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
@@ -1922,7 +1930,7 @@ flowchart TD
|
||||
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
|
||||
| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
|
||||
| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`api-remotes`](../packages/api/remotes), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
| [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
|
||||
+22
-14
@@ -110,6 +110,7 @@ flowchart TD
|
||||
pkg_api_gateway["api-gateway"]
|
||||
pkg_api_remotes["api-remotes"]
|
||||
pkg_api_session_controller["api-session-controller"]
|
||||
pkg_api_settings_controller["api-settings-controller"]
|
||||
pkg_api_workspace_controller["api-workspace-controller"]
|
||||
end
|
||||
subgraph group_attachment["packages/attachment"]
|
||||
@@ -412,8 +413,6 @@ flowchart TD
|
||||
pkg_anonymous_user_id --> pkg_brand
|
||||
pkg_anonymous_user_id --> pkg_home_paths
|
||||
pkg_anonymous_user_id --> pkg_invariants
|
||||
pkg_settings --> pkg_brand
|
||||
pkg_settings --> pkg_invariants
|
||||
pkg_storage_domain --> pkg_invariants
|
||||
pkg_storage_domain --> pkg_storage
|
||||
pkg_storage_json --> pkg_invariants
|
||||
@@ -440,10 +439,6 @@ flowchart TD
|
||||
pkg_credentials_local --> pkg_home_paths
|
||||
pkg_credentials_local --> pkg_invariants
|
||||
pkg_credentials_local --> pkg_launch_environment
|
||||
pkg_settings_file --> pkg_atomic_write
|
||||
pkg_settings_file --> pkg_home_paths
|
||||
pkg_settings_file --> pkg_invariants
|
||||
pkg_settings_file --> pkg_settings
|
||||
pkg_session --> pkg_brand
|
||||
pkg_session --> pkg_invariants
|
||||
pkg_session --> pkg_llm
|
||||
@@ -506,6 +501,9 @@ flowchart TD
|
||||
pkg_session_persistence --> pkg_timeout
|
||||
pkg_session_projection --> pkg_invariants
|
||||
pkg_session_projection --> pkg_session
|
||||
pkg_settings --> pkg_brand
|
||||
pkg_settings --> pkg_invariants
|
||||
pkg_settings --> pkg_session
|
||||
pkg_session_snapshot --> pkg_invariants
|
||||
pkg_session_snapshot --> pkg_session
|
||||
pkg_llm_retry --> pkg_agent
|
||||
@@ -539,6 +537,11 @@ flowchart TD
|
||||
pkg_web_search_deepseek --> pkg_web
|
||||
pkg_spill_local --> pkg_invariants
|
||||
pkg_spill_local --> pkg_spill
|
||||
pkg_api_settings_controller --> pkg_credentials
|
||||
pkg_api_settings_controller --> pkg_invariants
|
||||
pkg_api_settings_controller --> pkg_session
|
||||
pkg_api_settings_controller --> pkg_settings
|
||||
pkg_api_settings_controller --> pkg_typert_protocol
|
||||
pkg_file_reference --> pkg_agent
|
||||
pkg_file_reference --> pkg_invariants
|
||||
pkg_file_reference --> pkg_typert_protocol
|
||||
@@ -607,6 +610,10 @@ flowchart TD
|
||||
pkg_session_title --> pkg_llm
|
||||
pkg_session_title --> pkg_session
|
||||
pkg_session_title --> pkg_session_projection
|
||||
pkg_settings_file --> pkg_atomic_write
|
||||
pkg_settings_file --> pkg_home_paths
|
||||
pkg_settings_file --> pkg_invariants
|
||||
pkg_settings_file --> pkg_settings
|
||||
pkg_shell --> pkg_invariants
|
||||
pkg_shell --> pkg_sandbox
|
||||
pkg_shell --> pkg_settings
|
||||
@@ -1090,6 +1097,7 @@ flowchart TD
|
||||
pkg_client_connection --> pkg_invariants
|
||||
pkg_client_connection --> pkg_llm
|
||||
pkg_client_connection --> pkg_session
|
||||
pkg_client_connection --> pkg_settings
|
||||
pkg_client_connection --> pkg_tool_todo
|
||||
pkg_compaction_basic --> pkg_agent
|
||||
pkg_compaction_basic --> pkg_commands
|
||||
@@ -1294,6 +1302,7 @@ flowchart TD
|
||||
pkg_api_remotes --> pkg_agent_presets
|
||||
pkg_api_remotes --> pkg_api_gateway
|
||||
pkg_api_remotes --> pkg_api_session_controller
|
||||
pkg_api_remotes --> pkg_api_settings_controller
|
||||
pkg_api_remotes --> pkg_api_workspace_controller
|
||||
pkg_api_remotes --> pkg_commands
|
||||
pkg_api_remotes --> pkg_cordis_host_runner
|
||||
@@ -1336,7 +1345,6 @@ flowchart TD
|
||||
pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_settings
|
||||
pkg_client_ui_settings_plugin_inventory --> pkg_invariants
|
||||
pkg_client_ui_settings_plugins --> pkg_api_remotes
|
||||
pkg_client_ui_settings_plugins --> pkg_client_connection
|
||||
pkg_client_ui_settings_plugins --> pkg_client_locale
|
||||
pkg_client_ui_settings_plugins --> pkg_client_ui_renderer
|
||||
pkg_client_ui_settings_plugins --> pkg_client_ui_settings
|
||||
@@ -1618,7 +1626,6 @@ flowchart TD
|
||||
pkg_client_ui_model_selection --> pkg_typert_protocol
|
||||
pkg_client_ui_permission_presets --> pkg_api_remotes
|
||||
pkg_client_ui_permission_presets --> pkg_api_session_controller
|
||||
pkg_client_ui_permission_presets --> pkg_client_connection
|
||||
pkg_client_ui_permission_presets --> pkg_client_locale
|
||||
pkg_client_ui_permission_presets --> pkg_client_ui_commands
|
||||
pkg_client_ui_permission_presets --> pkg_client_ui_input_trigger
|
||||
@@ -1730,7 +1737,6 @@ flowchart TD
|
||||
| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/runtime-diagnostics/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`anonymous-user-id`](../packages/identity/anonymous-user-id) | `identity` | [`brand`](../packages/util/brand), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
|
||||
| [`storage-json`](../packages/storage/storage-json) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
|
||||
| [`storage-sqlite`](../packages/storage/storage-sqlite) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants), [`storage`](../packages/storage/storage) |
|
||||
@@ -1740,7 +1746,6 @@ flowchart TD
|
||||
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment) |
|
||||
| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||
| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
|
||||
@@ -1760,6 +1765,7 @@ flowchart TD
|
||||
| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
||||
| [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
|
||||
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |
|
||||
@@ -1767,6 +1773,7 @@ flowchart TD
|
||||
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
|
||||
| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
|
||||
| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
|
||||
@@ -1782,6 +1789,7 @@ flowchart TD
|
||||
| [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
|
||||
| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`shell`](../packages/shell/shell) | `shell` | [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
@@ -1860,7 +1868,7 @@ flowchart TD
|
||||
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-directory-picker`](../packages/host/directory-picker), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tool-todo`](../packages/todo/tool-todo) |
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`host-apiproxy`](../packages/host/apiproxy), [`host-directory-picker`](../packages/host/directory-picker), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`tool-todo`](../packages/todo/tool-todo) |
|
||||
| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
|
||||
@@ -1888,13 +1896,13 @@ flowchart TD
|
||||
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
|
||||
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-settings-controller`](../packages/api/settings-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-reference`](../packages/context/session-reference), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`client-ui-session`](../packages/client/ui-session) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-ui-renderer`](../packages/client/ui-renderer), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
|
||||
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
|
||||
| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-locale`](../packages/client/locale), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
@@ -1924,7 +1932,7 @@ flowchart TD
|
||||
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
|
||||
| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
|
||||
| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`api-remotes`](../packages/api/remotes), [`api-workspace-controller`](../packages/api/workspace-controller), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`util-workspace-path`](../packages/util/workspace-path) |
|
||||
| [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/credentials.md
|
||||
credentials.md: 9955f3ae6c6991ba5abbbec593ca5ccd12ef8773
|
||||
credentials.zh.md: 5565fe89431da42cd50fabca48bcc53e0810c800
|
||||
credentials.md: 60cd388fc18fe239b9ee5207d77d3b5561b7a3b3
|
||||
credentials.zh.md: 5ca47fac6c36c14eb1ee0aa8a6cb6a8b5e3eb9c4
|
||||
|
||||
@@ -34,13 +34,17 @@ interface ResolvedCredential {
|
||||
`describe(ref)` answers configuration surfaces without ever exposing a value: whether the reference resolves, from which layer, and whether `set` would currently succeed. The local provider reports a reference supplied by the live process environment as `writable: false` — a write would appear to succeed while resolution kept returning the shadowing value, so the seam rejects it and the UI can render the reference read-only up front.
|
||||
|
||||
```ts type-equiv
|
||||
/** Source and writability facts for one reference, safe for configuration UIs — never the value. */
|
||||
/**
|
||||
* Source and writability facts for one reference, safe for configuration UIs —
|
||||
* never the value. The view has no slot a value could ride in, which is what
|
||||
* lets the whole read half cross the Remote wire.
|
||||
*/
|
||||
interface CredentialInfo {
|
||||
/** Whether {@link CredentialProvider.resolve} would currently return a value. */
|
||||
/** Whether resolving the reference would currently return a value. */
|
||||
configured: boolean
|
||||
/** Source layer currently supplying the value; absent while unconfigured. */
|
||||
source?: string
|
||||
/** Whether {@link CredentialProvider.set} would currently succeed for this reference. */
|
||||
/** Whether the active provider can write this reference. */
|
||||
writable: boolean
|
||||
}
|
||||
```
|
||||
@@ -212,6 +216,42 @@ abstract deleteRecord(key: CredentialKey): Promise<void>
|
||||
|
||||
Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts)
|
||||
|
||||
<a id="ctxcredentialscontroller--credentialscontroller"></a>
|
||||
|
||||
### `ctx.credentialsController` — `CredentialsController`
|
||||
|
||||
Host service backing the generated `ctx.remote.credentials` namespace. It carries every wire obligation the credential seam itself does not: the batch fan-out bound, the field-by-field view projection, the reference-grammar guard, and the refusal mapping. Secret values cross in one direction only — no method here returns one.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Describe several references for one configuration surface. Batched because
|
||||
* a settings page describes every reference its rows name at once, and one
|
||||
* round trip keeps those rows from settling separately.
|
||||
* @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar rejects the whole call as `bad-request`.
|
||||
* @returns one view per requested name, keyed by that name.
|
||||
* @throws TypertRemoteFailure when the request is invalid or no credential provider is mounted.
|
||||
*/
|
||||
@Remote async describe(refs: string[]): Promise<Record<string, CredentialInfo>>
|
||||
|
||||
/**
|
||||
* Store one value from a configuration surface. The value crosses the wire in
|
||||
* this direction only: no read path returns it.
|
||||
* @param ref - reference name to store under.
|
||||
* @param value - the non-empty secret value.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async set(ref: string, value: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Remove one reference from a configuration surface.
|
||||
* @param ref - reference name to remove.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async unset(ref: string): Promise<void>
|
||||
```
|
||||
|
||||
Source: [`packages/api/settings-controller/src/credentials.ts`](../../packages/api/settings-controller/src/credentials.ts)
|
||||
|
||||
<a id="authorization-events"></a>
|
||||
|
||||
### `authorization/*` events
|
||||
|
||||
@@ -34,13 +34,17 @@ interface ResolvedCredential {
|
||||
`describe(ref)` 在绝不暴露值的前提下回应配置界面:引用当前是否可解析、来自哪一层、`set` 当前能否成功。本地提供方把由当前进程环境供值的引用报告为 `writable: false`——那样的写入会表面成功而解析持续返回遮蔽值,因此 seam 直接拒绝,界面也得以提前把该引用渲染为只读。
|
||||
|
||||
```ts type-equiv
|
||||
/** Source and writability facts for one reference, safe for configuration UIs — never the value. */
|
||||
/**
|
||||
* Source and writability facts for one reference, safe for configuration UIs —
|
||||
* never the value. The view has no slot a value could ride in, which is what
|
||||
* lets the whole read half cross the Remote wire.
|
||||
*/
|
||||
interface CredentialInfo {
|
||||
/** Whether {@link CredentialProvider.resolve} would currently return a value. */
|
||||
/** Whether resolving the reference would currently return a value. */
|
||||
configured: boolean
|
||||
/** Source layer currently supplying the value; absent while unconfigured. */
|
||||
source?: string
|
||||
/** Whether {@link CredentialProvider.set} would currently succeed for this reference. */
|
||||
/** Whether the active provider can write this reference. */
|
||||
writable: boolean
|
||||
}
|
||||
```
|
||||
@@ -212,6 +216,42 @@ abstract deleteRecord(key: CredentialKey): Promise<void>
|
||||
|
||||
Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts)
|
||||
|
||||
<a id="ctxcredentialscontroller--credentialscontroller"></a>
|
||||
|
||||
### `ctx.credentialsController` — `CredentialsController`
|
||||
|
||||
Host service backing the generated `ctx.remote.credentials` namespace. It carries every wire obligation the credential seam itself does not: the batch fan-out bound, the field-by-field view projection, the reference-grammar guard, and the refusal mapping. Secret values cross in one direction only — no method here returns one.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Describe several references for one configuration surface. Batched because
|
||||
* a settings page describes every reference its rows name at once, and one
|
||||
* round trip keeps those rows from settling separately.
|
||||
* @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar rejects the whole call as `bad-request`.
|
||||
* @returns one view per requested name, keyed by that name.
|
||||
* @throws TypertRemoteFailure when the request is invalid or no credential provider is mounted.
|
||||
*/
|
||||
@Remote async describe(refs: string[]): Promise<Record<string, CredentialInfo>>
|
||||
|
||||
/**
|
||||
* Store one value from a configuration surface. The value crosses the wire in
|
||||
* this direction only: no read path returns it.
|
||||
* @param ref - reference name to store under.
|
||||
* @param value - the non-empty secret value.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async set(ref: string, value: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Remove one reference from a configuration surface.
|
||||
* @param ref - reference name to remove.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async unset(ref: string): Promise<void>
|
||||
```
|
||||
|
||||
Source: [`packages/api/settings-controller/src/credentials.ts`](../../packages/api/settings-controller/src/credentials.ts)
|
||||
|
||||
<a id="authorization-events"></a>
|
||||
|
||||
### `authorization/*` events
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/settings.md
|
||||
settings.md: bfda7b897031337090f78e692a881a3d8e4cb625
|
||||
settings.zh.md: 06e82d6d25ea95ac82072ac4fa0e93d90cc4d948
|
||||
settings.md: 59bd06a6e7a8659097c726b85db13caa46dd461f
|
||||
settings.zh.md: a6bbdbce633a8b51167615f7838e55b698d48829
|
||||
|
||||
@@ -254,6 +254,56 @@ async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevi
|
||||
|
||||
Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
|
||||
|
||||
<a id="ctxsettingscontroller--settingscontroller"></a>
|
||||
|
||||
### `ctx.settingsController` — `SettingsController`
|
||||
|
||||
Host service backing the generated `ctx.remote.settings` namespace. Every remote read uses `redactSecrets: true`, so a `role('secret')` field cannot ride a response. Writes expose the settings service's merge, replacement, and path-addressed operations, and classify every provider refusal as `settings-conflict` or `settings-rejected` with the service's message.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Describe every registered namespace for a configuration page: redacted
|
||||
* layered values plus the serialized schema the page renders its form from.
|
||||
* @returns provider writability, local-document presence, and one view per namespace.
|
||||
* @throws TypertRemoteFailure when no settings provider is mounted.
|
||||
*/
|
||||
@Remote describe(): SettingsDescribeValue
|
||||
|
||||
/**
|
||||
* Merge a patch into one namespace's stored user section.
|
||||
* @param ns - namespace key to write.
|
||||
* @param patch - fields to merge into the user section.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote update( ns: string, patch: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
|
||||
/**
|
||||
* Replace one namespace's stored user section wholesale.
|
||||
* @param ns - namespace key to write.
|
||||
* @param section - complete replacement user section.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote replace( ns: string, section: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
|
||||
/**
|
||||
* Apply path-addressed edits to one namespace's user section, resolved against
|
||||
* the section as stored rather than against whatever the caller last read,
|
||||
* then answer with that namespace's new redacted view.
|
||||
* @param ns - namespace key to write.
|
||||
* @param ops - the edits to apply, in order.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
```
|
||||
|
||||
Source: [`packages/api/settings-controller/src/index.ts`](../../packages/api/settings-controller/src/index.ts)
|
||||
|
||||
<a id="settings-events"></a>
|
||||
|
||||
### `settings/*` events
|
||||
|
||||
@@ -254,6 +254,56 @@ async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevi
|
||||
|
||||
Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
|
||||
|
||||
<a id="ctxsettingscontroller--settingscontroller"></a>
|
||||
|
||||
### `ctx.settingsController` — `SettingsController`
|
||||
|
||||
Host service backing the generated `ctx.remote.settings` namespace. Every remote read uses `redactSecrets: true`, so a `role('secret')` field cannot ride a response. Writes expose the settings service's merge, replacement, and path-addressed operations, and classify every provider refusal as `settings-conflict` or `settings-rejected` with the service's message.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Describe every registered namespace for a configuration page: redacted
|
||||
* layered values plus the serialized schema the page renders its form from.
|
||||
* @returns provider writability, local-document presence, and one view per namespace.
|
||||
* @throws TypertRemoteFailure when no settings provider is mounted.
|
||||
*/
|
||||
@Remote describe(): SettingsDescribeValue
|
||||
|
||||
/**
|
||||
* Merge a patch into one namespace's stored user section.
|
||||
* @param ns - namespace key to write.
|
||||
* @param patch - fields to merge into the user section.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote update( ns: string, patch: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
|
||||
/**
|
||||
* Replace one namespace's stored user section wholesale.
|
||||
* @param ns - namespace key to write.
|
||||
* @param section - complete replacement user section.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote replace( ns: string, section: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
|
||||
/**
|
||||
* Apply path-addressed edits to one namespace's user section, resolved against
|
||||
* the section as stored rather than against whatever the caller last read,
|
||||
* then answer with that namespace's new redacted view.
|
||||
* @param ns - namespace key to write.
|
||||
* @param ops - the edits to apply, in order.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
|
||||
```
|
||||
|
||||
Source: [`packages/api/settings-controller/src/index.ts`](../../packages/api/settings-controller/src/index.ts)
|
||||
|
||||
<a id="settings-events"></a>
|
||||
|
||||
### `settings/*` events
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/api/README.md
|
||||
README.md: 1d9bcc49684ee557bc62b8ba2bbe79918c5de5ca
|
||||
README.zh.md: 2aae832f8dc9ece0f15283efded7c9c05b5bff88
|
||||
README.md: aaea6ef47aabdc8baa63ef7a875de065bcae94b9
|
||||
README.zh.md: 8e87f97c413620ede4ee160384d00f18382cfdf7
|
||||
|
||||
@@ -29,9 +29,10 @@ The packages below provide the Remote layer; the package READMEs own the exhaust
|
||||
| [`remotes/`](remotes/README.md) | Chooses which Host capabilities and events the Client can consume. | — |
|
||||
| [`gateway/`](gateway/README.md) | Carries typed unary calls, multiplexed streams, and forwarded Host events. | `ctx.typertGateway` / `ctx.remote` |
|
||||
| [`session-controller/`](session-controller/README.md) | Owns Session commands, history streams, live control state, and Agent/Session identity policy. | `ctx.sessionController` / `ctx.remote.session` |
|
||||
| [`settings-controller/`](settings-controller/README.md) | Owns the configuration-surface reads and writes over the settings-domain seams. | `ctx.settingsController`, `ctx.credentialsController` / `ctx.remote.settings`, `ctx.remote.credentials` |
|
||||
| [`workspace-controller/`](workspace-controller/README.md) | Owns Workspace mutations and the complete Client Workspace projection. | `ctx.workspaceController` / `ctx.remote.workspace` |
|
||||
|
||||
Remote calls run Client → Host over the application's shared Connection. API Gateway owns Remote transport, while the two controller packages own Session and Workspace behavior. Endpoints without a Remote definition fall through to the application's API Proxy.
|
||||
Remote calls run Client → Host over the application's shared Connection. API Gateway owns Remote transport, while the controller packages own Session, configuration-surface, and Workspace behavior. Endpoints without a Remote definition fall through to the application's API Proxy.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -22,16 +22,17 @@ kind: "package-group"
|
||||
<a id="packages"></a>
|
||||
## 包
|
||||
|
||||
下面两个包共同提供 Remote 层;穷尽式约定以各包 README 为准。
|
||||
下面这些包共同提供 Remote 层;穷尽式约定以各包 README 为准。
|
||||
|
||||
| 包 | 职责 | ctx key |
|
||||
|---|---|---|
|
||||
| [`remotes/`](remotes/README.zh.md) | 决定 Client 可以消费哪些 Host 能力与事件。 | — |
|
||||
| [`gateway/`](gateway/README.zh.md) | 承载带类型的单次调用、多路复用 stream 与转发的 Host 事件。 | `ctx.typertGateway` / `ctx.remote` |
|
||||
| [`session-controller/`](session-controller/README.zh.md) | 拥有 Session 命令、历史 stream、实时控制状态与 Agent/Session 身份策略。 | `ctx.sessionController` / `ctx.remote.session` |
|
||||
| [`settings-controller/`](settings-controller/README.zh.md) | 拥有 settings 域各 seam 之上的配置界面读写。 | `ctx.settingsController`、`ctx.credentialsController` / `ctx.remote.settings`、`ctx.remote.credentials` |
|
||||
| [`workspace-controller/`](workspace-controller/README.zh.md) | 拥有 Workspace 变更与完整 Client Workspace 投影。 | `ctx.workspaceController` / `ctx.remote.workspace` |
|
||||
|
||||
Remote 调用沿 Client → Host 方向运行在应用共享的 Connection 之上。API Gateway 拥有 Remote 传输,两个 controller 包分别拥有 Session 与 Workspace 行为。没有 Remote 定义的 endpoint 会回退到应用的 API Proxy。
|
||||
Remote 调用沿 Client → Host 方向运行在应用共享的 Connection 之上。API Gateway 拥有 Remote 传输,各 controller 包分别拥有 Session、配置界面与 Workspace 行为。没有 Remote 定义的 endpoint 会回退到应用的 API Proxy。
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/api/remotes/README.md
|
||||
README.md: 7817f30c531abffeb9156e1afaf8c6fe0697431e
|
||||
README.zh.md: 4529ee60289fc43978f9526d4abb3e2cbf073687
|
||||
README.md: 50361d1a6ae2f0b3b53a5bd928429414ab12bbce
|
||||
README.zh.md: 5545198cce2e43513af73adfc9d4277831adae92
|
||||
|
||||
@@ -27,7 +27,7 @@ Two-sided BFF for Host Remote capabilities selected by this application. The Hos
|
||||
|
||||
[`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.md) owns Agent and Session identity policy, including the Typert lookup resolvers used by other namespaces. This package only selects and mounts that generated Session contribution; it does not duplicate activation policy.
|
||||
|
||||
The Client assembly mounts Commands, Goal, dynamic Cordis, file and Session references, read-only Host plugin inventory, message feedback, Session Controller, and Workspace Controller contributions. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
|
||||
The Client assembly mounts Commands, credentials, settings, Goal, dynamic Cordis, file and Session references, read-only Host plugin inventory, message feedback, Session Controller, and Workspace Controller contributions. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
|
||||
|
||||
This package owns no physical transport or Host service discovery. It projects the application selection into generated Remote contributions and an independent Host event source per Client; API Gateway owns endpoints, carriers, cancellation, and reconnection. Its Client face can be reused by Web or a future TUI that provides the same React-free `ctx.remote` contract.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ kind: "package-reference"
|
||||
|
||||
[`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.zh.md) 拥有 Agent 与 Session 身份策略,包括供其他 namespace 使用的 Typert lookup resolver。本包只选择并挂载生成的 Session contribution,不复制激活策略。
|
||||
|
||||
Client 组合挂载 Commands、Goal、动态 Cordis、文件与 Session 引用、只读 Host 插件清单、消息反馈、Session Controller 和 Workspace Controller contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
|
||||
Client 组合挂载 Commands、凭据、settings、Goal、动态 Cordis、文件与 Session 引用、只读 Host 插件清单、消息反馈、Session Controller 和 Workspace Controller contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
|
||||
|
||||
本包不拥有物理传输或 Host 服务发现。它只把应用选择投影为生成的 Remote contribution 和唯一的 Host Cordis event source;API Gateway 负责 endpoint、carrier、取消与重连。Web 或未来的 TUI 只要提供同一份不依赖 React 的 `ctx.remote` 约定,均可复用其 Client face。
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"@deepseek-ai/dsh-agent-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-gateway": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-settings-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
|
||||
@@ -85,6 +86,7 @@
|
||||
"@deepseek-ai/dsh-agent-presets": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-gateway": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-settings-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
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 dynamicRemote from '@deepseek-ai/dsh-cordis-host-runner/remote'
|
||||
import fileReferencesRemote from '@deepseek-ai/dsh-file-reference/remote'
|
||||
@@ -18,6 +19,7 @@ export type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
|
||||
export type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inventory/types'
|
||||
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-host-plugin-inventory/remote'
|
||||
@@ -53,10 +55,10 @@ export type {} from '@deepseek-ai/dsh-api-session-controller/types'
|
||||
*/
|
||||
export type {
|
||||
ConfigurableProviderView, ConnectionHandle, ConnectionSinks, ContentBlock,
|
||||
CredentialView, DiscoveredModelView, IApiClient,
|
||||
DiscoveredModelView, IApiClient,
|
||||
MessageId, ModelCatalog, ModelCatalogFailure, ModelProviderGroup, ModelReasoningEffort, ModelSelection,
|
||||
RpcError, RpcId, RpcRequest, RpcResponse, RpcResult, SessionId,
|
||||
SettingsNamespaceView, SettingsPathOpView, SkillEntry, StreamChunk,
|
||||
SkillEntry, 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'
|
||||
@@ -102,6 +104,13 @@ export type {
|
||||
// reason: a Client contribution names what it sends without importing a Host
|
||||
// package, and this assembly is where both planes legitimately meet.
|
||||
export type { JsonValue } from '@deepseek-ai/dsh-session/types'
|
||||
// Credential state vocabulary for the credentials namespace (values never ride it).
|
||||
export type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
|
||||
// Redacted namespace vocabulary for the settings namespace (secrets never ride
|
||||
// it). It travels with its seam, whose `./types` the Client face already reads.
|
||||
export type {
|
||||
SettingsDescribeValue, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
|
||||
} from '@deepseek-ai/dsh-settings/types'
|
||||
// Reference-discovery result vocabulary for the fileReferences and
|
||||
// sessionReferenceResolver namespaces.
|
||||
export type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
|
||||
@@ -112,6 +121,8 @@ export type ClientFailure =
|
||||
| import('@deepseek-ai/dsh-client-connection/client').RpcError
|
||||
| import('@deepseek-ai/dsh-agent-presets/types').AgentPresetError
|
||||
| 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-subagent/client').SubagentControlError
|
||||
| import('@deepseek-ai/dsh-api-workspace-controller/types').WorkspaceError
|
||||
|
||||
@@ -139,7 +150,8 @@ export async function apply(ctx: Context): Promise<() => Promise<void>> {
|
||||
const disposers: Array<() => Promise<void>> = []
|
||||
try {
|
||||
for (const contribution of [
|
||||
agentPresetsRemote, commandsRemote, goalsRemote, dynamicRemote, fileReferencesRemote,
|
||||
agentPresetsRemote, commandsRemote, settingsControllerRemote, goalsRemote, dynamicRemote,
|
||||
fileReferencesRemote,
|
||||
pluginInventoryRemote, messageFeedbackRemote, sessionReferencesRemote,
|
||||
subagentsRemote, sessionRemote, workspaceRemote,
|
||||
]) {
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
{
|
||||
"path": "../session-controller/tsconfig.client.json"
|
||||
},
|
||||
{
|
||||
"path": "../settings-controller"
|
||||
},
|
||||
{
|
||||
"path": "../workspace-controller/tsconfig.client.json"
|
||||
},
|
||||
|
||||
@@ -234,17 +234,7 @@ export class FakeApiClient implements IApiClient {
|
||||
}
|
||||
|
||||
readonly settings: IApiClient['settings'] = {
|
||||
describe: payload => this.record('settings.describe', payload, Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [] }))),
|
||||
openDocument: payload => this.record('settings.openDocument', payload, Promise.resolve(ok({ opened: true as const }))),
|
||||
update: payload => this.record('settings.update', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
|
||||
replace: payload => this.record('settings.replace', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
|
||||
mutate: payload => this.record('settings.mutate', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
|
||||
}
|
||||
|
||||
readonly credentials: IApiClient['credentials'] = {
|
||||
describe: payload => this.record('credentials.describe', payload, Promise.resolve(ok({ credentials: {} }))),
|
||||
set: payload => this.record('credentials.set', payload, Promise.resolve(ok({}))),
|
||||
unset: payload => this.record('credentials.unset', payload, Promise.resolve(ok({}))),
|
||||
}
|
||||
|
||||
readonly llm: IApiClient['llm'] = {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/api/settings-controller/README.md
|
||||
README.md: f57bab1cf68ff05d807102a831f4ad9ce65dba76
|
||||
README.zh.md: 41062db004b8c98f544f70c42137a71aee997ec8
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
description: "Host Remote owner for settings and credential configuration surfaces, including redacted reads, path-addressed settings writes, and credential reference management."
|
||||
kind: "package-reference"
|
||||
---
|
||||
# Settings Controller
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
## Summary
|
||||
|
||||
`@deepseek-ai/dsh-api-settings-controller` exposes generated `ctx.remote.settings` and `ctx.remote.credentials` namespaces for browser configuration surfaces. It returns redacted settings and credential metadata, supports merge, replacement, and path-addressed settings writes, and stores or removes credential references without returning secret values. When either provider is absent, its namespace remains registered and returns an actionable configuration error.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Use this package](#use-this-package)
|
||||
- [Model Experience](#model-experience)
|
||||
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
||||
- [Dev Note](#dev-note)
|
||||
|
||||
-----
|
||||
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
Mount this package as a Loader entry in a profile that serves browser configuration. The entry registers both namespaces independently of their providers so a missing provider produces a named configuration error at invocation. Its generated descriptors enter the strict Typert registry, while the settings and credential Definitions remain plain Cordis Services with no wire obligations of their own.
|
||||
|
||||
`describe(refs)` answers one map keyed by the requested names, so a settings page describing every reference its rows carry settles those rows together. It accepts at most 64 names per call, reports an invalid name or empty write value as `bad-request`, and copies each answer field by field — a provider returning more than `CredentialInfo` declares cannot widen what crosses. Valid `set(ref, value)` and `unset(ref)` calls report a provider refusal as `credential-rejected`, carrying the provider's message with only the reference in its details. Secret values cross in this direction only: no method here returns one.
|
||||
|
||||
`settings.describe()` returns deployment facts and every namespace under `redactSecrets: true`. `settings.update`, `settings.replace`, and `settings.mutate` expose the settings service's three write operations and return the namespace's new redacted view; stale writes use `settings-conflict` and other provider refusals use `settings-rejected`.
|
||||
|
||||
-----
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
None, as settings and credential configuration are browser and Host state and register no prompt, tool, or session event.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No direct effect; reading or writing these configuration values does not alter model requests already in flight.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- Settings document opening uses API Proxy rather than this Remote namespace.
|
||||
- The batch bound is fixed at 64 references and is not a deployment-configurable field.
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### Dev Note
|
||||
|
||||
<details>
|
||||
<summary>Working context for maintainers — click to expand</summary>
|
||||
|
||||
None.
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
description: "settings 与凭据配置界面的 Host Remote owner,涵盖脱敏读取、按路径写入 settings 和管理凭据引用。"
|
||||
kind: "package-reference"
|
||||
---
|
||||
# Settings Controller
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
## 概述
|
||||
|
||||
`@deepseek-ai/dsh-api-settings-controller` 为浏览器配置界面提供生成的 `ctx.remote.settings` 与 `ctx.remote.credentials` namespace。它返回脱敏的 settings 与凭据元数据,支持合并、替换和按路径表达的 settings 写入,并在不返回密钥值的前提下写入或移除凭据引用。任一 provider 缺失时,对应 namespace 仍会注册,并返回可操作的配置错误。
|
||||
|
||||
## 目录
|
||||
|
||||
- [使用本包](#use-this-package)
|
||||
- [模型体验](#model-experience)
|
||||
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
||||
- [开发备注](#dev-note)
|
||||
|
||||
-----
|
||||
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
请把本包作为 Loader entry 挂载到提供浏览器配置的 profile 中。本 entry 不依赖 provider 是否存在而注册两个 namespace,因此缺少 provider 会在调用时产生具名配置错误。它生成的 descriptor 进入严格 Typert registry,而 settings 与凭据 Definition 仍是普通 Cordis Service,自身不承担任何 wire 义务。
|
||||
|
||||
`describe(refs)` 以请求的名字为键返回一份 map,因此设置页描述其各行携带的全部引用时,这些行会一起落定。单次调用最多接受 64 个名字,无效名字或空写入值报告为 `bad-request`,并逐字段复制每个答案——provider 返回超出 `CredentialInfo` 声明的内容也无法扩大跨越 wire 的字段。有效的 `set(ref, value)` 与 `unset(ref)` 调用把 provider 拒绝报告为 `credential-rejected`,携带 provider 的消息,details 中只有该引用。密钥值只在这个方向跨越 wire:这里没有任何方法会返回它。
|
||||
|
||||
`settings.describe()` 返回部署信息,以及在 `redactSecrets: true` 下读取的所有 namespace。`settings.update`、`settings.replace` 与 `settings.mutate` 暴露 settings service 的三种写入操作,并返回该 namespace 的新脱敏视图;过期写入使用 `settings-conflict`,其他 provider 拒绝使用 `settings-rejected`。
|
||||
|
||||
-----
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
无,因为 settings 与凭据配置属于浏览器和 Host 状态,并且不注册提示词、工具或会话事件。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
无直接影响;读取或写入这些配置值不会改变已经在途的模型请求。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
- settings 文档打开使用 API Proxy,而不经过本 Remote namespace。
|
||||
- 批量上限固定为 64 个引用,不是可按部署配置的字段。
|
||||
|
||||
<a id="dev-note"></a>
|
||||
### 开发备注
|
||||
|
||||
<details>
|
||||
<summary>维护者工作上下文——点击展开</summary>
|
||||
|
||||
无。
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-settings-controller",
|
||||
"description": "Remote owner for the configuration surfaces over the settings-domain seams",
|
||||
"version": "0.1.1-rc.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/api/settings-controller"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./lib/types/types.d.ts",
|
||||
"default": "./lib/types/types.js"
|
||||
},
|
||||
"./typert": {
|
||||
"types": "./lib/typert.host.d.ts",
|
||||
"default": "./lib/typert.host.js"
|
||||
},
|
||||
"./remote": {
|
||||
"types": "./lib/typert.remote-client.d.ts",
|
||||
"default": "./lib/typert.remote-client.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/typert.host.js",
|
||||
"lib/typert.host.d.ts",
|
||||
"lib/typert.remote-client.js",
|
||||
"lib/typert.remote-client.d.ts"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:^"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Host owner of the `credentials` Remote namespace: the reference half of
|
||||
* `ctx.credentials` as a browser configuration page reads and writes it.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-api-settings-controller/src/credentials.ts
|
||||
*/
|
||||
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import type { CredentialProvider } from '@deepseek-ai/dsh-credentials'
|
||||
import type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
|
||||
import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* Fan-out bound on one remote `describe` batch. A settings page asks about the
|
||||
* references its own rows name, so this is far above any real page and still
|
||||
* keeps one authenticated request from starting unbounded provider work.
|
||||
*/
|
||||
const MAX_DESCRIBE_REFS = 64
|
||||
|
||||
const credentialRefSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)
|
||||
const describeRequestSchema = z.object({
|
||||
refs: z.array(credentialRefSchema).max(MAX_DESCRIBE_REFS),
|
||||
})
|
||||
const setRequestSchema = z.object({ ref: credentialRefSchema, value: z.string().min(1) })
|
||||
const unsetRequestSchema = z.object({ ref: credentialRefSchema })
|
||||
|
||||
/** Parse the domain constraints that are more specific than generated TypeScript codecs. */
|
||||
function parseRequest<T>(method: string, schema: z.ZodType<T>, value: unknown): T {
|
||||
const parsed = schema.safeParse(value)
|
||||
if (!parsed.success) {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'bad-request',
|
||||
message: `invalid payload for ${method}`,
|
||||
details: { issues: parsed.error.issues },
|
||||
})
|
||||
}
|
||||
return parsed.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy exactly the fields {@link CredentialInfo} declares. The Gateway returns
|
||||
* a business result without decoding it, so a provider whose `describe` carried
|
||||
* extra enumerable properties would otherwise serialize them to the caller.
|
||||
* @param info - the provider's answer for one reference.
|
||||
* @returns the same facts with nothing else attached.
|
||||
*/
|
||||
function projectCredentialInfo(info: CredentialInfo): CredentialInfo {
|
||||
return {
|
||||
configured: info.configured,
|
||||
...info.source === undefined ? {} : { source: info.source },
|
||||
writable: info.writable,
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
/** Host owner of the `credentials` Remote namespace. */
|
||||
credentialsController: CredentialsController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Host service backing the generated `ctx.remote.credentials` namespace. It
|
||||
* carries every wire obligation the credential seam itself does not: the batch
|
||||
* fan-out bound, the field-by-field view projection, the reference-grammar
|
||||
* guard, and the refusal mapping. Secret values cross in one direction only —
|
||||
* no method here returns one.
|
||||
*/
|
||||
export class CredentialsController extends TypertRemoteService {
|
||||
/** @param ctx - Host context where a credential provider may be mounted. */
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'credentialsController', { namespace: 'credentials' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe several references for one configuration surface. Batched because
|
||||
* a settings page describes every reference its rows name at once, and one
|
||||
* round trip keeps those rows from settling separately.
|
||||
* @param refs - reference names, at most {@link MAX_DESCRIBE_REFS}; a name outside the grammar rejects the whole call as `bad-request`.
|
||||
* @returns one view per requested name, keyed by that name.
|
||||
* @throws TypertRemoteFailure when the request is invalid or no credential provider is mounted.
|
||||
*/
|
||||
@Remote
|
||||
async describe(refs: string[]): Promise<Record<string, CredentialInfo>> {
|
||||
const request = parseRequest('credentials.describe', describeRequestSchema, { refs })
|
||||
const branded = request.refs.map(ref => [ref, credentialRef(ref)] as const)
|
||||
const credentials = this.provider()
|
||||
const entries = await Promise.all(branded.map(async ([ref, key]) =>
|
||||
[ref, projectCredentialInfo(await credentials.describe(key))] as const))
|
||||
return Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store one value from a configuration surface. The value crosses the wire in
|
||||
* this direction only: no read path returns it.
|
||||
* @param ref - reference name to store under.
|
||||
* @param value - the non-empty secret value.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote
|
||||
async set(ref: string, value: string): Promise<void> {
|
||||
const request = parseRequest('credentials.set', setRequestSchema, { ref, value })
|
||||
const branded = credentialRef(request.ref)
|
||||
const credentials = this.provider()
|
||||
await this.write(request.ref, () => credentials.set(branded, request.value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove one reference from a configuration surface.
|
||||
* @param ref - reference name to remove.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote
|
||||
async unset(ref: string): Promise<void> {
|
||||
const request = parseRequest('credentials.unset', unsetRequestSchema, { ref })
|
||||
const branded = credentialRef(request.ref)
|
||||
const credentials = this.provider()
|
||||
await this.write(request.ref, () => credentials.unset(branded))
|
||||
}
|
||||
|
||||
/** Resolve the optional provider or report how to supply it. */
|
||||
private provider(): CredentialProvider {
|
||||
const credentials = this.ctx.get('credentials')
|
||||
if (credentials === undefined) {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'internal',
|
||||
message: 'credentials service is absent: this deployment does not mount a credential provider (e.g. @deepseek-ai/dsh-credentials-local) in its composition',
|
||||
details: {},
|
||||
})
|
||||
}
|
||||
return credentials
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one remote write and report every refusal as `credential-rejected`
|
||||
* carrying the seam's own message: a read-only source shadowing the reference
|
||||
* is what a configuration surface must show verbatim. Callers brand the
|
||||
* reference before entering, so a name outside the grammar never reaches this
|
||||
* path and fails the same way it does on the read side. The details name only
|
||||
* the reference, so no failure path can carry the value back out.
|
||||
*/
|
||||
private async write(ref: string, write: () => Promise<void>): Promise<void> {
|
||||
try {
|
||||
await write()
|
||||
} catch (error: unknown) {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'credential-rejected',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
details: { ref },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CredentialsController
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Host Remote owner for the configuration surfaces over the settings-domain
|
||||
* seams. Two namespaces: `settings`, the redacted reads and writes of
|
||||
* `ctx.settings`, owned by the class below; and `credentials`, mounted from
|
||||
* here as its own plugin.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-api-settings-controller
|
||||
*/
|
||||
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { SettingsConflictError, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import type { SettingsDescriptor, SettingsPathOp, SettingsProvider } from '@deepseek-ai/dsh-settings'
|
||||
import type {
|
||||
SettingsDescribeValue, SettingsNamespaceView, SettingsPathOpView,
|
||||
} from '@deepseek-ai/dsh-settings/types'
|
||||
import type { JsonValue } from '@deepseek-ai/dsh-session/types'
|
||||
import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { z } from 'zod'
|
||||
import { CredentialsController } from './credentials.ts'
|
||||
|
||||
export { CredentialsController } from './credentials.ts'
|
||||
export type * from './types.ts'
|
||||
|
||||
const settingsNamespaceRequestSchema = z.object({ ns: z.string().min(1) })
|
||||
|
||||
/**
|
||||
* Project one redacted descriptor onto its wire view, field by field. The
|
||||
* Gateway returns a business result without decoding it, so a provider whose
|
||||
* descriptor carried extra enumerable properties would otherwise serialize them
|
||||
* to the caller.
|
||||
* @param descriptor - one descriptor read under `redactSecrets`.
|
||||
* @returns the same facts with nothing else attached.
|
||||
*/
|
||||
function namespaceView(descriptor: SettingsDescriptor): SettingsNamespaceView {
|
||||
return {
|
||||
ns: String(descriptor.ns),
|
||||
schema: descriptor.schema as JsonValue,
|
||||
value: descriptor.value as JsonValue,
|
||||
...descriptor.base === undefined ? {} : { base: descriptor.base as JsonValue },
|
||||
...descriptor.user === undefined ? {} : { user: descriptor.user as JsonValue },
|
||||
applies: descriptor.applies,
|
||||
secrets: (descriptor.secrets ?? []).map(secret => ({ path: [...secret.path], set: secret.set })),
|
||||
revision: descriptor.revision,
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
/** Host owner of the `settings` Remote namespace. */
|
||||
settingsController: SettingsController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Host service backing the generated `ctx.remote.settings` namespace. Every
|
||||
* remote read uses `redactSecrets: true`, so a `role('secret')` field cannot
|
||||
* ride a response. Writes expose the settings service's merge, replacement,
|
||||
* and path-addressed operations, and classify every provider refusal as
|
||||
* `settings-conflict` or `settings-rejected` with the service's message.
|
||||
*/
|
||||
export class SettingsController extends TypertRemoteService {
|
||||
/**
|
||||
* Register the settings namespace and mount the credentials namespace beside
|
||||
* it. Both namespaces stay registered when a provider is absent so calls can
|
||||
* return the configuration API's actionable missing-provider diagnostic.
|
||||
* @param ctx - Host context where settings and credential providers may be mounted.
|
||||
*/
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'settingsController', { namespace: 'settings' })
|
||||
ctx.plugin(CredentialsController)
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe every registered namespace for a configuration page: redacted
|
||||
* layered values plus the serialized schema the page renders its form from.
|
||||
* @returns provider writability, local-document presence, and one view per namespace.
|
||||
* @throws TypertRemoteFailure when no settings provider is mounted.
|
||||
*/
|
||||
@Remote
|
||||
describe(): SettingsDescribeValue {
|
||||
const settings = this.provider()
|
||||
return {
|
||||
writable: settings.writable,
|
||||
hasDocument: settings.documentPath !== undefined,
|
||||
namespaces: settings.describe({ redactSecrets: true }).map(namespaceView),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a patch into one namespace's stored user section.
|
||||
* @param ns - namespace key to write.
|
||||
* @param patch - fields to merge into the user section.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote
|
||||
update(
|
||||
ns: string,
|
||||
patch: Record<string, JsonValue>,
|
||||
expectedRevision: number | undefined,
|
||||
): Promise<SettingsNamespaceView> {
|
||||
return this.write(ns, 'update', patch, expectedRevision)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace one namespace's stored user section wholesale.
|
||||
* @param ns - namespace key to write.
|
||||
* @param section - complete replacement user section.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote
|
||||
replace(
|
||||
ns: string,
|
||||
section: Record<string, JsonValue>,
|
||||
expectedRevision: number | undefined,
|
||||
): Promise<SettingsNamespaceView> {
|
||||
return this.write(ns, 'replace', section, expectedRevision)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply path-addressed edits to one namespace's user section, resolved against
|
||||
* the section as stored rather than against whatever the caller last read,
|
||||
* then answer with that namespace's new redacted view.
|
||||
* @param ns - namespace key to write.
|
||||
* @param ops - the edits to apply, in order.
|
||||
* @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
|
||||
* @returns the namespace's redacted view after the write.
|
||||
* @throws TypertRemoteFailure when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
*/
|
||||
@Remote
|
||||
async mutate(
|
||||
ns: string,
|
||||
ops: SettingsPathOpView[],
|
||||
expectedRevision: number | undefined,
|
||||
): Promise<SettingsNamespaceView> {
|
||||
return this.write(ns, 'mutate', ops, expectedRevision)
|
||||
}
|
||||
|
||||
private async write(
|
||||
ns: string,
|
||||
mode: 'update' | 'replace' | 'mutate',
|
||||
input: Record<string, JsonValue> | SettingsPathOpView[],
|
||||
expectedRevision: number | undefined,
|
||||
): Promise<SettingsNamespaceView> {
|
||||
const parsed = settingsNamespaceRequestSchema.safeParse({ ns })
|
||||
if (!parsed.success) {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'bad-request',
|
||||
message: `invalid payload for settings.${mode}`,
|
||||
details: { issues: parsed.error.issues },
|
||||
})
|
||||
}
|
||||
const settings = this.provider()
|
||||
let branded
|
||||
try {
|
||||
// A malformed name can address no registration, so it fails exactly as an
|
||||
// unregistered one does.
|
||||
branded = settingsNamespace(parsed.data.ns)
|
||||
} catch (error: unknown) {
|
||||
throw rejected(ns, error)
|
||||
}
|
||||
try {
|
||||
if (mode === 'update') await settings.update(branded, input, expectedRevision)
|
||||
else if (mode === 'replace') await settings.replace(branded, input, expectedRevision)
|
||||
else await settings.mutate(branded, input as SettingsPathOp[], expectedRevision)
|
||||
} catch (error: unknown) {
|
||||
throw rejected(ns, error)
|
||||
}
|
||||
const descriptor = settings.describe({ redactSecrets: true }).find(candidate => candidate.ns === branded)
|
||||
if (descriptor === undefined) {
|
||||
// The write committed but the namespace vanished before this read: only a
|
||||
// concurrent registrant disposal can produce it.
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'internal',
|
||||
message: `settings namespace "${ns}" was disposed after the ${mode}`,
|
||||
details: {},
|
||||
})
|
||||
}
|
||||
return namespaceView(descriptor)
|
||||
}
|
||||
|
||||
/** Resolve the optional provider or report how to supply it. */
|
||||
private provider(): SettingsProvider {
|
||||
const settings = this.ctx.get('settings')
|
||||
if (settings === undefined) {
|
||||
throw new TypertRemoteFailure({
|
||||
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: {},
|
||||
})
|
||||
}
|
||||
return settings
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify one seam refusal. A stale writer is its own outcome, not a malformed
|
||||
* request: the client must re-read and re-apply rather than treat the write as
|
||||
* invalid.
|
||||
* @param ns - the namespace the write addressed.
|
||||
* @param error - whatever the seam threw.
|
||||
* @returns the failure to raise for that refusal.
|
||||
*/
|
||||
function rejected(ns: string, error: unknown): TypertRemoteFailure {
|
||||
if (error instanceof SettingsConflictError) {
|
||||
return new TypertRemoteFailure({
|
||||
code: 'settings-conflict',
|
||||
message: error.message,
|
||||
details: { ns, expected: error.expected, actual: error.actual },
|
||||
})
|
||||
}
|
||||
return new TypertRemoteFailure({
|
||||
code: 'settings-rejected',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
details: { ns },
|
||||
})
|
||||
}
|
||||
|
||||
export default SettingsController
|
||||
@@ -0,0 +1,23 @@
|
||||
/** Package-owned invariant companion. @module @deepseek-ai/dsh-api-settings-controller/invariant */
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-api-settings-controller'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'api-settings-controller-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: the settings and credential seams own storage and
|
||||
* update events, while this package only projects their methods onto the wire.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/** Register this package's invariant companion. */
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Browser-safe failure vocabulary of the configuration surfaces this package
|
||||
* serves. The redacted views themselves live with their seam in
|
||||
* `@deepseek-ai/dsh-settings/types`, whose Cordis event declarations already
|
||||
* register that file for the Client compilation face.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-api-settings-controller/types
|
||||
*/
|
||||
|
||||
/** Stable settings failure details returned by the `settings` namespace. */
|
||||
export interface SettingsErrorDetailsMap {
|
||||
/**
|
||||
* Every seam refusal that is not a stale write: an unregistered or malformed
|
||||
* namespace, a read-only provider, schema validation, storage.
|
||||
*/
|
||||
'settings-rejected': { readonly ns: string }
|
||||
/**
|
||||
* The stored revision moved after the caller read it. Its own outcome rather
|
||||
* than an invalid request: the caller must re-read and re-apply.
|
||||
*/
|
||||
'settings-conflict': { readonly ns: string; readonly expected: number; readonly actual: number }
|
||||
}
|
||||
|
||||
/** Settings business failure carried by a rejected Remote call. */
|
||||
export type SettingsError = {
|
||||
[Code in keyof SettingsErrorDetailsMap]: {
|
||||
readonly code: Code
|
||||
readonly message: string
|
||||
readonly details: SettingsErrorDetailsMap[Code]
|
||||
}
|
||||
}[keyof SettingsErrorDetailsMap]
|
||||
|
||||
/** Stable credential failure details returned by the `credentials` namespace. */
|
||||
export interface CredentialErrorDetailsMap {
|
||||
/**
|
||||
* The provider refused a valid write, for example because a read-only source
|
||||
* shadows the reference. The details name only the reference, never the value.
|
||||
*/
|
||||
'credential-rejected': { readonly ref: string }
|
||||
}
|
||||
|
||||
/** Credential business failure carried by a rejected Remote call. */
|
||||
export type CredentialError = {
|
||||
[Code in keyof CredentialErrorDetailsMap]: {
|
||||
readonly code: Code
|
||||
readonly message: string
|
||||
readonly details: CredentialErrorDetailsMap[Code]
|
||||
}
|
||||
}[keyof CredentialErrorDetailsMap]
|
||||
@@ -0,0 +1,141 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
|
||||
import { TypertRemoteFailure, remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import CredentialsController from '../src/credentials.ts'
|
||||
import { MemoryCredentials } from '../../../credentials/credentials/tests/memory.ts'
|
||||
|
||||
/** A store whose `describe` carries more than the view declares, as a foreign provider might. */
|
||||
class LeakyCredentials extends MemoryCredentials {
|
||||
override describe(): Promise<CredentialInfo> {
|
||||
return Promise.resolve(
|
||||
{ configured: true, source: 'memory', writable: true, value: 'sk-leaked' } as CredentialInfo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A store whose write rejects with a bare string, the way some client libraries do. */
|
||||
class LiteralRejectingCredentials extends MemoryCredentials {
|
||||
override async set(): Promise<void> {
|
||||
throw 'the store refused'
|
||||
}
|
||||
}
|
||||
|
||||
/** A store whose provider-owned policy rejects an otherwise valid write. */
|
||||
class RejectingCredentials extends MemoryCredentials {
|
||||
override set(): Promise<void> {
|
||||
return Promise.reject(new Error('a read-only source shadows this reference'))
|
||||
}
|
||||
}
|
||||
|
||||
async function boot(
|
||||
seed: Record<string, string> = {},
|
||||
provider: typeof MemoryCredentials = MemoryCredentials,
|
||||
): Promise<CredentialsController> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(provider, seed)
|
||||
await ctx.plugin(CredentialsController)
|
||||
return ctx.credentialsController
|
||||
}
|
||||
|
||||
describe('the credentials Remote namespace a configuration surface calls', () => {
|
||||
it('publishes the credentials namespace from its own service key', async () => {
|
||||
const controller = await boot()
|
||||
const binding = controller.typertRemote
|
||||
expect(binding.serviceKey).toBe('credentialsController')
|
||||
expect(binding.namespace).toBe('credentials')
|
||||
expect(remoteMethods(controller)).toEqual([
|
||||
{ method: 'describe', invocation: { kind: 'direct' } },
|
||||
{ method: 'set', invocation: { kind: 'direct' } },
|
||||
{ method: 'unset', invocation: { kind: 'direct' } },
|
||||
])
|
||||
})
|
||||
|
||||
it('reports the actionable configuration error while no credential provider is mounted', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(CredentialsController)
|
||||
for (const call of [
|
||||
() => ctx.credentialsController.describe(['DEEPSEEK_API_KEY']),
|
||||
() => ctx.credentialsController.set('DEEPSEEK_API_KEY', 'sk-live'),
|
||||
() => ctx.credentialsController.unset('DEEPSEEK_API_KEY'),
|
||||
]) {
|
||||
const failure = await call().catch((error: unknown) => error)
|
||||
expect(failure).toBeInstanceOf(TypertRemoteFailure)
|
||||
expect((failure as TypertRemoteFailure).failure).toEqual({
|
||||
code: 'internal',
|
||||
message: 'credentials service is absent: this deployment does not mount a credential provider (e.g. @deepseek-ai/dsh-credentials-local) in its composition',
|
||||
details: {},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('describes a batch of references as one map, values excluded', async () => {
|
||||
const controller = await boot({ DEEPSEEK_API_KEY: 'sk-seeded' })
|
||||
const described = await controller.describe(['DEEPSEEK_API_KEY', 'OPENAI_API_KEY'])
|
||||
expect(described).toEqual({
|
||||
DEEPSEEK_API_KEY: { configured: true, source: 'memory', writable: true },
|
||||
OPENAI_API_KEY: { configured: false, writable: true },
|
||||
})
|
||||
expect(JSON.stringify(described)).not.toContain('sk-seeded')
|
||||
})
|
||||
|
||||
it('reports an invalid reference as bad-request', async () => {
|
||||
const controller = await boot()
|
||||
for (const call of [
|
||||
() => controller.describe(['DEEPSEEK_API_KEY', 'not a var']),
|
||||
() => controller.set('not a var', 'sk-live'),
|
||||
() => controller.unset('not a var'),
|
||||
]) {
|
||||
const failure = await call().catch((error: unknown) => error)
|
||||
expect(failure).toBeInstanceOf(TypertRemoteFailure)
|
||||
expect((failure as TypertRemoteFailure).failure).toMatchObject({ code: 'bad-request' })
|
||||
}
|
||||
})
|
||||
|
||||
it('answers the largest batch it accepts and reports one reference more as bad-request', async () => {
|
||||
const controller = await boot()
|
||||
const accepted = Array.from({ length: 64 }, (_unused, index) => `REF_${String(index)}`)
|
||||
expect(Object.keys(await controller.describe(accepted))).toHaveLength(64)
|
||||
const failure = await controller.describe([...accepted, 'REF_64']).catch((error: unknown) => error)
|
||||
expect((failure as TypertRemoteFailure).failure).toMatchObject({ code: 'bad-request' })
|
||||
})
|
||||
|
||||
it('answers only the fields the view declares, whatever a provider returns', async () => {
|
||||
const controller = await boot({}, LeakyCredentials)
|
||||
const described = await controller.describe(['DEEPSEEK_API_KEY'])
|
||||
expect(described.DEEPSEEK_API_KEY).toEqual({ configured: true, source: 'memory', writable: true })
|
||||
expect(JSON.stringify(described)).not.toContain('sk-leaked')
|
||||
})
|
||||
|
||||
it('stores and removes through the same references the batch describes', async () => {
|
||||
const controller = await boot()
|
||||
await controller.set('DEEPSEEK_API_KEY', 'sk-live')
|
||||
expect(await controller.describe(['DEEPSEEK_API_KEY']))
|
||||
.toEqual({ DEEPSEEK_API_KEY: { configured: true, source: 'memory', writable: true } })
|
||||
await controller.unset('DEEPSEEK_API_KEY')
|
||||
expect(await controller.describe(['DEEPSEEK_API_KEY']))
|
||||
.toEqual({ DEEPSEEK_API_KEY: { configured: false, writable: true } })
|
||||
})
|
||||
|
||||
it('reports a refused write as credential-rejected naming only the reference', async () => {
|
||||
const controller = await boot({}, RejectingCredentials)
|
||||
const failure = await controller.set('DEEPSEEK_API_KEY', 'sk-live').catch((error: unknown) => error)
|
||||
expect(failure).toBeInstanceOf(TypertRemoteFailure)
|
||||
const { code, message, details } = (failure as TypertRemoteFailure).failure
|
||||
expect(code).toBe('credential-rejected')
|
||||
expect(message).toContain('read-only source')
|
||||
expect(details).toEqual({ ref: 'DEEPSEEK_API_KEY' })
|
||||
})
|
||||
|
||||
it('reports an empty value as bad-request', async () => {
|
||||
const controller = await boot()
|
||||
const failure = await controller.set('DEEPSEEK_API_KEY', '').catch((error: unknown) => error)
|
||||
expect((failure as TypertRemoteFailure).failure).toMatchObject({ code: 'bad-request' })
|
||||
})
|
||||
|
||||
it('stringifies a refusal that is not an Error', async () => {
|
||||
const controller = await boot({}, LiteralRejectingCredentials)
|
||||
const failure = await controller.set('DEEPSEEK_API_KEY', 'sk-live').catch((error: unknown) => error)
|
||||
expect((failure as TypertRemoteFailure).failure.message).toBe('the store refused')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
|
||||
import * as SettingsControllerInvariant from '../src/invariant.ts'
|
||||
|
||||
describe('api-settings-controller invariant companion', () => {
|
||||
it('reserves the package name against duplicate registration', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(InvariantRegistry)
|
||||
await ctx.plugin(SettingsControllerInvariant)
|
||||
|
||||
expect(() => {
|
||||
ctx.invariants.register('@deepseek-ai/dsh-api-settings-controller', () => {})
|
||||
}).toThrow(/already registered/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,242 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import type { SettingsDescriptor, SettingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { TypertRemoteFailure, remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import SettingsController from '../src/index.ts'
|
||||
import { MemorySettings } from '../../../settings/settings/tests/memory.ts'
|
||||
|
||||
const NS = settingsNamespace('ui-test')
|
||||
|
||||
const Profile = z.object({
|
||||
preference: z.union(['light', 'dark']).default('light'),
|
||||
apiKey: z.string().role('secret'),
|
||||
})
|
||||
|
||||
/** A provider that reports a local document, for the `hasDocument` fact. */
|
||||
class DocumentSettings extends MemorySettings {
|
||||
override get documentPath(): string | undefined {
|
||||
return '/deployment/settings.yaml'
|
||||
}
|
||||
}
|
||||
|
||||
/** A provider whose read forgets the namespace its write just committed. */
|
||||
class VanishingSettings extends MemorySettings {
|
||||
override describe(): SettingsDescriptor[] {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider whose descriptor omits the secret-slot list. `secrets` is optional
|
||||
* on the descriptor, so a foreign provider may leave it out even under
|
||||
* `redactSecrets`, and the view still has to declare an empty list.
|
||||
*/
|
||||
class SlotlessSettings extends MemorySettings {
|
||||
override describe(): SettingsDescriptor[] {
|
||||
return [{
|
||||
ns: NS,
|
||||
schema: Profile.toJSON(),
|
||||
value: { preference: 'light' },
|
||||
applies: 'live',
|
||||
revision: 0,
|
||||
} as unknown as SettingsDescriptor]
|
||||
}
|
||||
}
|
||||
|
||||
/** A provider that refuses every write the way a read-only backing store would. */
|
||||
class RefusingSettings extends MemorySettings {
|
||||
override mutate(ns: SettingsNamespace): Promise<void> {
|
||||
return Promise.reject(new Error(`settings "${ns}" is read-only in this deployment`))
|
||||
}
|
||||
}
|
||||
|
||||
/** A provider that refuses with a bare string, the way some storage clients do. */
|
||||
class LiteralRefusingSettings extends MemorySettings {
|
||||
override async mutate(): Promise<void> {
|
||||
throw 'the document is locked'
|
||||
}
|
||||
}
|
||||
|
||||
async function boot(
|
||||
provider: typeof MemorySettings = MemorySettings,
|
||||
options: { doc?: Record<string, unknown>; base?: { preference: 'light' | 'dark' } } = {},
|
||||
): Promise<{ controller: SettingsController; ctx: Context }> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(provider, options.doc === undefined ? {} : { doc: options.doc })
|
||||
ctx.settings.register(NS, Profile, options.base === undefined ? {} : { base: options.base })
|
||||
await ctx.plugin(SettingsController)
|
||||
return { controller: ctx.settingsController, ctx }
|
||||
}
|
||||
|
||||
describe('the settings Remote namespace a configuration page calls', () => {
|
||||
it('publishes the settings namespace from its own service key', async () => {
|
||||
const { controller } = await boot()
|
||||
expect(controller.typertRemote.serviceKey).toBe('settingsController')
|
||||
expect(controller.typertRemote.namespace).toBe('settings')
|
||||
expect(remoteMethods(controller)).toEqual([
|
||||
{ method: 'describe', invocation: { kind: 'direct' } },
|
||||
{ method: 'update', invocation: { kind: 'direct' } },
|
||||
{ method: 'replace', invocation: { kind: 'direct' } },
|
||||
{ method: 'mutate', invocation: { kind: 'direct' } },
|
||||
])
|
||||
})
|
||||
|
||||
it('reports the actionable configuration error while no settings provider is mounted', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SettingsController)
|
||||
const calls: Array<() => unknown> = [
|
||||
() => ctx.settingsController.describe(),
|
||||
() => ctx.settingsController.update('ui-test', {}, undefined),
|
||||
() => ctx.settingsController.replace('ui-test', {}, undefined),
|
||||
() => ctx.settingsController.mutate('ui-test', [], undefined),
|
||||
]
|
||||
for (const call of calls) {
|
||||
const failure = await Promise.resolve().then(call).catch((error: unknown) => error)
|
||||
expect(failure).toBeInstanceOf(TypertRemoteFailure)
|
||||
expect((failure as TypertRemoteFailure).failure).toEqual({
|
||||
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: {},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('mounts the credentials namespace beside its own', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(MemorySettings)
|
||||
ctx.settings.register(NS, Profile)
|
||||
const fiber = ctx.plugin(SettingsController)
|
||||
await fiber.await()
|
||||
expect(ctx.get('credentialsController')).toBeDefined()
|
||||
await fiber.dispose()
|
||||
expect(ctx.get('settingsController')).toBeUndefined()
|
||||
expect(ctx.get('credentialsController')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('describes every namespace redacted, with the deployment facts around them', async () => {
|
||||
const { controller } = await boot(DocumentSettings, { doc: { 'ui-test': { apiKey: 'sk-stored' } } })
|
||||
const value = controller.describe()
|
||||
expect(value).toMatchObject({ writable: true, hasDocument: true })
|
||||
const [view] = value.namespaces
|
||||
expect(view?.ns).toBe('ui-test')
|
||||
// The secret never rides; its slot reports only that one is stored.
|
||||
expect(JSON.stringify(value)).not.toContain('sk-stored')
|
||||
expect(view?.secrets).toEqual([{ path: ['apiKey'], set: true }])
|
||||
// Redaction removes the field rather than replacing it, so the layer that
|
||||
// stored a secret comes back empty instead of carrying a placeholder.
|
||||
expect(view?.user).toEqual({})
|
||||
})
|
||||
|
||||
it('reports a read-only provider and omits the layers it has none of', async () => {
|
||||
const { controller } = await boot(class extends MemorySettings {
|
||||
override get writable(): boolean {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const value = controller.describe()
|
||||
expect(value).toMatchObject({ writable: false, hasDocument: false })
|
||||
const [view] = value.namespaces
|
||||
// No composition base was declared and no user section is stored, so
|
||||
// neither optional layer appears at all.
|
||||
expect(view && 'base' in view).toBe(false)
|
||||
expect(view && 'user' in view).toBe(false)
|
||||
})
|
||||
|
||||
it('declares an empty slot list when the provider names no secrets', async () => {
|
||||
const { controller } = await boot(SlotlessSettings)
|
||||
const [view] = controller.describe().namespaces
|
||||
expect(view?.secrets).toEqual([])
|
||||
})
|
||||
|
||||
it('carries the composition base layer when the registrant declared one', async () => {
|
||||
const { controller } = await boot(MemorySettings, { base: { preference: 'dark' } })
|
||||
const [view] = controller.describe().namespaces
|
||||
expect(view?.base).toEqual({ preference: 'dark' })
|
||||
})
|
||||
|
||||
it('applies path-addressed edits and answers with the namespace it just wrote', async () => {
|
||||
const { controller } = await boot()
|
||||
const view = await controller.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'dark' }], undefined)
|
||||
expect(view).toMatchObject({ ns: 'ui-test', user: { preference: 'dark' } })
|
||||
expect(view.revision).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('supports merge updates and wholesale replacement on the Remote namespace', async () => {
|
||||
const { controller } = await boot(MemorySettings, {
|
||||
doc: { 'ui-test': { preference: 'dark', apiKey: 'sk-stored' } },
|
||||
})
|
||||
const updated = await controller.update('ui-test', { preference: 'light' }, undefined)
|
||||
expect(updated.user).toEqual({ preference: 'light' })
|
||||
expect(updated.secrets).toEqual([{ path: ['apiKey'], set: true }])
|
||||
|
||||
const replaced = await controller.replace('ui-test', {}, updated.revision)
|
||||
expect(replaced.value).toEqual({ preference: 'light' })
|
||||
expect(replaced.user).toEqual({})
|
||||
expect(replaced.secrets).toEqual([{ path: ['apiKey'], set: false }])
|
||||
})
|
||||
|
||||
it('refuses a stale write as settings-conflict carrying both revisions', async () => {
|
||||
const { controller } = await boot()
|
||||
const held = controller.describe().namespaces[0]!.revision
|
||||
await controller.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'dark' }], held)
|
||||
const failure = await controller
|
||||
.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'light' }], held)
|
||||
.catch((error: unknown) => error)
|
||||
expect(failure).toBeInstanceOf(TypertRemoteFailure)
|
||||
const { code, details } = (failure as TypertRemoteFailure).failure
|
||||
expect(code).toBe('settings-conflict')
|
||||
expect(details).toMatchObject({ ns: 'ui-test', expected: held })
|
||||
})
|
||||
|
||||
it('answers a malformed namespace exactly as an unregistered one', async () => {
|
||||
const { controller } = await boot()
|
||||
for (const ns of ['Not A Namespace', 'unregistered']) {
|
||||
const failure = await controller.mutate(ns, [{ op: 'unset', path: ['preference'] }], undefined)
|
||||
.catch((error: unknown) => error)
|
||||
expect((failure as TypertRemoteFailure).failure).toMatchObject({
|
||||
code: 'settings-rejected',
|
||||
details: { ns },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('reports an empty namespace as bad-request', async () => {
|
||||
const { controller } = await boot()
|
||||
for (const call of [
|
||||
() => controller.update('', {}, undefined),
|
||||
() => controller.replace('', {}, undefined),
|
||||
() => controller.mutate('', [], undefined),
|
||||
]) {
|
||||
const failure = await call().catch((error: unknown) => error)
|
||||
expect(failure).toBeInstanceOf(TypertRemoteFailure)
|
||||
expect((failure as TypertRemoteFailure).failure).toMatchObject({ code: 'bad-request' })
|
||||
}
|
||||
})
|
||||
|
||||
it('reports a refused write as settings-rejected carrying the seam message', async () => {
|
||||
const { controller } = await boot(RefusingSettings)
|
||||
const failure = await controller.mutate('ui-test', [{ op: 'unset', path: ['preference'] }], undefined)
|
||||
.catch((error: unknown) => error)
|
||||
const { code, message } = (failure as TypertRemoteFailure).failure
|
||||
expect(code).toBe('settings-rejected')
|
||||
expect(message).toContain('read-only in this deployment')
|
||||
})
|
||||
|
||||
it('stringifies a refusal that is not an Error', async () => {
|
||||
const { controller } = await boot(LiteralRefusingSettings)
|
||||
const failure = await controller.mutate('ui-test', [{ op: 'unset', path: ['preference'] }], undefined)
|
||||
.catch((error: unknown) => error)
|
||||
expect((failure as TypertRemoteFailure).failure.message).toBe('the document is locked')
|
||||
})
|
||||
|
||||
it('reports a namespace disposed between the write and its read-back', async () => {
|
||||
const { controller } = await boot(VanishingSettings)
|
||||
const failure = await controller.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'dark' }], undefined)
|
||||
.catch((error: unknown) => error)
|
||||
const { code, message } = (failure as TypertRemoteFailure).failure
|
||||
expect(code).toBe('internal')
|
||||
expect(message).toContain('was disposed after the mutate')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../credentials/credentials"
|
||||
},
|
||||
{
|
||||
"path": "../../core/session"
|
||||
},
|
||||
{
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../settings/settings"
|
||||
},
|
||||
{
|
||||
"path": "../../typert/protocol"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -86,6 +86,11 @@
|
||||
- id: session-controller
|
||||
name: '@deepseek-ai/dsh-api-session-controller'
|
||||
|
||||
# Configuration-surface reads and writes over Typert Remote. Each method
|
||||
# reports an actionable error when its settings-domain provider is absent.
|
||||
- id: settings-controller
|
||||
name: '@deepseek-ai/dsh-api-settings-controller'
|
||||
|
||||
# Workspace commands and reconnect-safe projection over Typert Remote.
|
||||
- id: workspace-controller
|
||||
name: '@deepseek-ai/dsh-api-workspace-controller'
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"@deepseek-ai/dsh-session-log-export": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-stats": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-settings-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-workspace-controller": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^",
|
||||
|
||||
@@ -47,29 +47,31 @@
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-directory-picker": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-todo": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-directory-picker": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-todo": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ export type {
|
||||
SkillsApi, SkillEntry,
|
||||
ModelCatalog, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
|
||||
ModelReasoningEffort, ModelSelection,
|
||||
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
|
||||
CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi,
|
||||
SettingsApi,
|
||||
ConfigurableProviderView, DiscoveredModelView, LlmApi,
|
||||
} from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
export type {
|
||||
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
||||
|
||||
@@ -29,7 +29,9 @@ import type { TodoItem } from '@deepseek-ai/dsh-tool-todo/client'
|
||||
// wire-fabrication boundary (the schema layer's one-cast-point posture).
|
||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
import type { CommandDescriptor, CommandExecution, CommandResult } from '@deepseek-ai/dsh-commands/types'
|
||||
import type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types'
|
||||
import type { DirectoryListing as FixtureDirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types'
|
||||
import type { SettingsDescribeValue, SettingsNamespaceView } from '@deepseek-ai/dsh-settings/types'
|
||||
import { deriveEventMessage, foldSurface } from '@deepseek-ai/dsh-session/surface'
|
||||
import type {
|
||||
ApiProxy, ClientRequest,
|
||||
@@ -1778,6 +1780,83 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
// DeepSeek route so unrelated GUI journeys do not enter first-run setup.
|
||||
['DEEPSEEK_API_KEY', true],
|
||||
])
|
||||
|
||||
/** Canonical fixture implementation of the generated Settings Remote contract. */
|
||||
const settingsRemotes = {
|
||||
// Only the resolved DeepSeek address needed by first-run readiness is
|
||||
// represented here. Fixture-backed journeys do not open its Models editor;
|
||||
// real schema-driven forms ride the HTTP transport.
|
||||
describe(): RpcResult<SettingsDescribeValue> {
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: true,
|
||||
namespaces: [{
|
||||
ns: 'llm-deepseek',
|
||||
schema: {},
|
||||
value: { apiKeyEnv: 'DEEPSEEK_API_KEY' },
|
||||
applies: 'live',
|
||||
secrets: [{ path: ['apiKey'], set: false }],
|
||||
revision: 0,
|
||||
}],
|
||||
},
|
||||
}
|
||||
},
|
||||
update(ns: string): ConnectionRpcResult<SettingsNamespaceView> {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'settings-rejected',
|
||||
message: 'fixture: the minimal readiness settings descriptor is read-only',
|
||||
details: { ns },
|
||||
},
|
||||
}
|
||||
},
|
||||
replace(ns: string): ConnectionRpcResult<SettingsNamespaceView> {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'settings-rejected',
|
||||
message: 'fixture: the minimal readiness settings descriptor is read-only',
|
||||
details: { ns },
|
||||
},
|
||||
}
|
||||
},
|
||||
mutate(ns: string): ConnectionRpcResult<SettingsNamespaceView> {
|
||||
// A Remote failure code is free-form, unlike the unary error vocabulary.
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'settings-rejected',
|
||||
message: 'fixture: no settings namespaces are registered',
|
||||
details: { ns },
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const credentialRemotes = {
|
||||
describe(refs: readonly string[]): RpcResult<Record<string, CredentialInfo>> {
|
||||
return {
|
||||
ok: true,
|
||||
value: Object.fromEntries(refs.map(ref => [ref, {
|
||||
configured: fixtureCredentials.has(ref),
|
||||
...fixtureCredentials.has(ref) ? { source: 'file' } : {},
|
||||
writable: true,
|
||||
}])),
|
||||
}
|
||||
},
|
||||
set(ref: string): RpcResult<void> {
|
||||
fixtureCredentials.set(ref, true)
|
||||
return { ok: true, value: undefined }
|
||||
},
|
||||
unset(ref: string): RpcResult<void> {
|
||||
fixtureCredentials.delete(ref)
|
||||
return { ok: true, value: undefined }
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Preset compositions the fixture serves. Held as state rather than
|
||||
* constants so the settings editor's save and delete are exercisable: the
|
||||
@@ -3347,55 +3426,8 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
// Only the resolved DeepSeek address needed by first-run readiness is
|
||||
// represented here. Fixture-backed journeys do not open its Models
|
||||
// editor; real schema-driven forms ride the HTTP transport.
|
||||
describe: request => ok(request, {
|
||||
writable: true,
|
||||
hasDocument: true,
|
||||
namespaces: [{
|
||||
ns: 'llm-deepseek',
|
||||
schema: {},
|
||||
value: { apiKeyEnv: 'DEEPSEEK_API_KEY' },
|
||||
applies: 'live',
|
||||
secrets: [{ path: ['apiKey'], set: false }],
|
||||
revision: 0,
|
||||
}],
|
||||
}),
|
||||
// Native opens are deterministic no-op successes in this fixture, as is host.openPath.
|
||||
openDocument: request => ok(request, { opened: true as const }),
|
||||
update: request => err(request, {
|
||||
code: 'settings-rejected',
|
||||
message: 'fixture: the minimal readiness settings descriptor is read-only',
|
||||
details: { ns: request.payload.ns },
|
||||
}),
|
||||
replace: request => err(request, {
|
||||
code: 'settings-rejected',
|
||||
message: 'fixture: the minimal readiness settings descriptor is read-only',
|
||||
details: { ns: request.payload.ns },
|
||||
}),
|
||||
mutate: request => err(request, {
|
||||
code: 'settings-rejected',
|
||||
message: 'fixture: no settings namespaces are registered',
|
||||
details: { ns: request.payload.ns },
|
||||
}),
|
||||
},
|
||||
credentials: {
|
||||
describe: request => ok(request, {
|
||||
credentials: Object.fromEntries(request.payload.refs.map(ref => [ref, {
|
||||
configured: fixtureCredentials.has(ref),
|
||||
...fixtureCredentials.has(ref) ? { source: 'file' } : {},
|
||||
writable: true,
|
||||
}])),
|
||||
}),
|
||||
set: (request) => {
|
||||
fixtureCredentials.set(request.payload.ref, true)
|
||||
return ok(request, {})
|
||||
},
|
||||
unset: (request) => {
|
||||
fixtureCredentials.delete(request.payload.ref)
|
||||
return ok(request, {})
|
||||
},
|
||||
},
|
||||
llm: {
|
||||
providers: request => ok(request, {
|
||||
@@ -3442,7 +3474,11 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
path?: string
|
||||
name?: string
|
||||
images?: readonly unknown[]
|
||||
ref?: { id: string; revision: number }
|
||||
// A goal ref and a credential reference name share this wire field name.
|
||||
ref?: string | { id: string; revision: number }
|
||||
refs?: readonly string[]
|
||||
value?: string
|
||||
ns?: string
|
||||
agentPreset?: string
|
||||
from?: string
|
||||
id?: string
|
||||
@@ -3493,6 +3529,13 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
},
|
||||
})
|
||||
case 'subagents/interruptByParent': return Promise.resolve({ ok: true, value: { accepted: true } })
|
||||
case 'credentials/describe': return Promise.resolve(credentialRemotes.describe(args.refs ?? []))
|
||||
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/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))
|
||||
case 'session/list': return sessionApi.list(
|
||||
args._request as Parameters<FixtureSessionApi['list']>[0],
|
||||
)
|
||||
@@ -3619,14 +3662,7 @@ export class FixtureApiClient extends AbstractApiClient {
|
||||
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.describe': return this.api.settings.describe(request)
|
||||
case 'settings.openDocument': return this.api.settings.openDocument(request, signal)
|
||||
case 'settings.update': return this.api.settings.update(request)
|
||||
case 'settings.replace': return this.api.settings.replace(request)
|
||||
case 'settings.mutate': return this.api.settings.mutate(request)
|
||||
case 'credentials.describe': return this.api.credentials.describe(request)
|
||||
case 'credentials.set': return this.api.credentials.set(request)
|
||||
case 'credentials.unset': return this.api.credentials.unset(request)
|
||||
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)
|
||||
|
||||
@@ -37,8 +37,8 @@ export type {
|
||||
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
||||
ClientRequest, ServerResponse, RpcMessage,
|
||||
HostDescription, IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
|
||||
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
|
||||
CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi,
|
||||
SettingsApi,
|
||||
ConfigurableProviderView, DiscoveredModelView, LlmApi,
|
||||
} from './api.ts'
|
||||
export {
|
||||
RpcId,
|
||||
|
||||
@@ -76,17 +76,7 @@ export class FakeApiClient implements IApiClient {
|
||||
}
|
||||
|
||||
readonly settings: IApiClient['settings'] = {
|
||||
describe: payload => this.record('settings.describe', payload, Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [] }))),
|
||||
openDocument: payload => this.record('settings.openDocument', payload, Promise.resolve(ok({ opened: true as const }))),
|
||||
update: payload => this.record('settings.update', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
|
||||
replace: payload => this.record('settings.replace', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
|
||||
mutate: payload => this.record('settings.mutate', payload, Promise.resolve(ok({ ns: 'fake', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0 }))),
|
||||
}
|
||||
|
||||
readonly credentials: IApiClient['credentials'] = {
|
||||
describe: payload => this.record('credentials.describe', payload, Promise.resolve(ok({ credentials: {} }))),
|
||||
set: payload => this.record('credentials.set', payload, Promise.resolve(ok({}))),
|
||||
unset: payload => this.record('credentials.unset', payload, Promise.resolve(ok({}))),
|
||||
}
|
||||
|
||||
readonly llm: IApiClient['llm'] = {
|
||||
|
||||
@@ -297,6 +297,8 @@ type FixtureTestApi = ReturnType<typeof createFixtureFaces>['api'] & {
|
||||
readonly sessionRemote: FixtureSessionRemote
|
||||
readonly workspace: FixtureWorkspaceApi
|
||||
readonly workspaceRemote: FixtureWorkspaceRemote
|
||||
readonly credentialRemote: FixtureCredentialRemote
|
||||
readonly settingsRemote: FixtureSettingsRemote
|
||||
readonly remoteEvents: (signal: AbortSignal) => FixtureRemoteEventStream
|
||||
readonly answerRemoteEvent: (result: FixtureRemoteEventResult) => Promise<unknown>
|
||||
}
|
||||
@@ -318,12 +320,48 @@ function createFixtureApi(options: FixtureOptions = {}): FixtureTestApi {
|
||||
sessionRemote: createSessionRemote(rpc),
|
||||
workspace: createWorkspaceApi(rpc),
|
||||
workspaceRemote: createWorkspaceRemote(rpc),
|
||||
credentialRemote: createCredentialRemote(rpc),
|
||||
settingsRemote: createSettingsRemote(rpc),
|
||||
remoteEvents: (signal: AbortSignal) => openFixtureRemoteEvents(rpc, signal),
|
||||
answerRemoteEvent: (result: FixtureRemoteEventResult) =>
|
||||
rpc.call('/api', '$events/result', { args: result }),
|
||||
})
|
||||
}
|
||||
|
||||
/** The fixture's Credentials Remote endpoints over the shared RPC carrier. */
|
||||
interface FixtureCredentialRemote {
|
||||
describe(refs: readonly string[]): Promise<ConnectionRpcResult<unknown>>
|
||||
set(ref: string, value: string): Promise<ConnectionRpcResult<unknown>>
|
||||
unset(ref: string): Promise<ConnectionRpcResult<unknown>>
|
||||
}
|
||||
|
||||
/** The settings Remote reads the fixture serves, addressed like the credential half. */
|
||||
interface FixtureSettingsRemote {
|
||||
describe(): Promise<ConnectionRpcResult<unknown>>
|
||||
update(ns: string, patch: unknown, expectedRevision?: number): Promise<ConnectionRpcResult<unknown>>
|
||||
replace(ns: string, section: unknown, expectedRevision?: number): Promise<ConnectionRpcResult<unknown>>
|
||||
}
|
||||
|
||||
function createSettingsRemote(rpc: ClientConnectionRpc): FixtureSettingsRemote {
|
||||
return {
|
||||
describe: () => rpc.call('/api', 'settings/describe', { args: {} }),
|
||||
update: (ns, patch, expectedRevision) => rpc.call('/api', 'settings/update', {
|
||||
args: { ns, patch, expectedRevision },
|
||||
}),
|
||||
replace: (ns, section, expectedRevision) => rpc.call('/api', 'settings/replace', {
|
||||
args: { ns, section, expectedRevision },
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function createCredentialRemote(rpc: ClientConnectionRpc): FixtureCredentialRemote {
|
||||
return {
|
||||
describe: refs => rpc.call('/api', 'credentials/describe', { args: { refs } }),
|
||||
set: (ref, value) => rpc.call('/api', 'credentials/set', { args: { ref, value } }),
|
||||
unset: ref => rpc.call('/api', 'credentials/unset', { args: { ref } }),
|
||||
}
|
||||
}
|
||||
|
||||
function openFixtureRemoteEvents(
|
||||
rpc: ClientConnectionRpc,
|
||||
signal: AbortSignal,
|
||||
@@ -724,32 +762,40 @@ describe('createFixtureApi', () => {
|
||||
|
||||
it('serves configured DeepSeek readiness and keeps credential values write-only', async () => {
|
||||
const api = createFixtureApi()
|
||||
const settings = await api.settings.describe(req({}))
|
||||
if (!settings.result.ok) throw new Error('settings describe failed')
|
||||
expect(settings.result.value.namespaces).toMatchObject([{
|
||||
const settings = await api.settingsRemote.describe()
|
||||
if (!settings.ok) throw new Error('settings describe failed')
|
||||
expect((settings.value as { namespaces: unknown[] }).namespaces).toMatchObject([{
|
||||
ns: 'llm-deepseek',
|
||||
value: { apiKeyEnv: 'DEEPSEEK_API_KEY' },
|
||||
secrets: [{ path: ['apiKey'], set: false }],
|
||||
}])
|
||||
for (const result of [
|
||||
await api.settingsRemote.update('llm-deepseek', {}, undefined),
|
||||
await api.settingsRemote.replace('llm-deepseek', {}, undefined),
|
||||
]) {
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'settings-rejected', message: 'fixture: the minimal readiness settings descriptor is read-only' },
|
||||
})
|
||||
}
|
||||
|
||||
const initial = await api.credentials.describe(req({ refs: ['DEEPSEEK_API_KEY', 'TEST_API_KEY'] }))
|
||||
if (!initial.result.ok) throw new Error('credential describe failed')
|
||||
expect(initial.result.value.credentials).toEqual({
|
||||
const describe = async (refs: readonly string[]): Promise<Record<string, unknown>> => {
|
||||
const result = await api.credentialRemote.describe(refs)
|
||||
if (!result.ok) throw new Error('credential describe failed')
|
||||
return result.value as Record<string, unknown>
|
||||
}
|
||||
expect(await describe(['DEEPSEEK_API_KEY', 'TEST_API_KEY'])).toEqual({
|
||||
DEEPSEEK_API_KEY: { configured: true, source: 'file', writable: true },
|
||||
TEST_API_KEY: { configured: false, writable: true },
|
||||
})
|
||||
await api.credentials.set(req({ ref: 'TEST_API_KEY', value: 'write-only-fixture-secret' }))
|
||||
const configured = await api.credentials.describe(req({ refs: ['TEST_API_KEY'] }))
|
||||
if (!configured.result.ok) throw new Error('credential describe failed')
|
||||
expect(configured.result.value.credentials.TEST_API_KEY).toEqual({
|
||||
await api.credentialRemote.set('TEST_API_KEY', 'write-only-fixture-secret')
|
||||
expect((await describe(['TEST_API_KEY'])).TEST_API_KEY).toEqual({
|
||||
configured: true,
|
||||
source: 'file',
|
||||
writable: true,
|
||||
})
|
||||
await api.credentials.unset(req({ ref: 'TEST_API_KEY' }))
|
||||
const cleared = await api.credentials.describe(req({ refs: ['TEST_API_KEY'] }))
|
||||
if (!cleared.result.ok) throw new Error('credential describe failed')
|
||||
expect(cleared.result.value.credentials.TEST_API_KEY).toEqual({ configured: false, writable: true })
|
||||
await api.credentialRemote.unset('TEST_API_KEY')
|
||||
expect((await describe(['TEST_API_KEY'])).TEST_API_KEY).toEqual({ configured: false, writable: true })
|
||||
})
|
||||
|
||||
it('emits the todo/write snapshot at the real tool boundary: between tool/call and tool/result, timestamps monotonic', async () => {
|
||||
|
||||
@@ -175,7 +175,6 @@ describe('connection node half', () => {
|
||||
const { routes, connection, dispose } = await mounted({ trustedHosts: ['harness.example'] })
|
||||
const methods = [
|
||||
'host.openPath',
|
||||
'settings.describe', 'settings.update', 'credentials.describe', 'credentials.set',
|
||||
'llm.discoverModels', 'llm.models', 'agentPreset.openDocument',
|
||||
]
|
||||
for (const method of methods) {
|
||||
@@ -501,8 +500,7 @@ describe('connection node half over a real HTTP server', () => {
|
||||
const { port, close } = await serve(routes)
|
||||
try {
|
||||
const methods = [
|
||||
'settings.describe', 'settings.openDocument', 'settings.update', 'settings.replace', 'settings.mutate',
|
||||
'credentials.describe', 'credentials.set', 'credentials.unset',
|
||||
'settings.openDocument',
|
||||
'host.openPath',
|
||||
'llm.discoverModels',
|
||||
'agentPreset.openDocument',
|
||||
@@ -512,7 +510,7 @@ describe('connection node half over a real HTTP server', () => {
|
||||
expect([method, await call(port, method, 'localhost')]).toEqual([method, 401])
|
||||
expect([method, await call(port, method, 'harness.example')]).toEqual([method, 401])
|
||||
}
|
||||
expect(await call(port, 'settings.describe', 'other.example')).toBe(403)
|
||||
expect(await call(port, 'settings.openDocument', 'other.example')).toBe(403)
|
||||
|
||||
const declaredCookie = browserCookie(connection, 'harness.example')
|
||||
for (const method of methods) {
|
||||
@@ -521,7 +519,7 @@ describe('connection node half over a real HTTP server', () => {
|
||||
const loopbackAuthority = `127.0.0.1:${String(port)}`
|
||||
expect(await call(
|
||||
port,
|
||||
'settings.describe',
|
||||
'settings.openDocument',
|
||||
loopbackAuthority,
|
||||
browserCookie(connection, loopbackAuthority),
|
||||
)).toBe(404)
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
{
|
||||
"path": "../../core/tools"
|
||||
},
|
||||
{
|
||||
"path": "../../credentials/credentials"
|
||||
},
|
||||
{
|
||||
"path": "../../settings/settings"
|
||||
},
|
||||
{
|
||||
"path": "../../host/apiproxy"
|
||||
},
|
||||
|
||||
@@ -30,22 +30,16 @@ async function bench() {
|
||||
revision,
|
||||
})
|
||||
const describe = vi.fn(async () => ({
|
||||
rpcId: 'locale-describe' as never,
|
||||
result: {
|
||||
ok: true as const,
|
||||
value: { writable: true, hasDocument: true, namespaces: [namespace()] },
|
||||
},
|
||||
ok: true as const,
|
||||
value: { writable: true, hasDocument: true, namespaces: [namespace()] },
|
||||
}))
|
||||
const mutate = vi.fn(async (request: { ops: { value: string }[] }) => {
|
||||
preference = request.ops[0]!.value
|
||||
const mutate = vi.fn(async (_ns: string, ops: { value: string }[]) => {
|
||||
preference = ops[0]!.value
|
||||
revision += 1
|
||||
return {
|
||||
rpcId: 'locale-mutate' as never,
|
||||
result: { ok: true as const, value: namespace() },
|
||||
}
|
||||
return { ok: true as const, value: namespace() }
|
||||
})
|
||||
ctx.provide('connection', { api: { settings: { describe, mutate } }, isLoopback: true } as never)
|
||||
const events = new TestRemote(ctx)
|
||||
ctx.provide('connection', { api: {}, isLoopback: true } as never)
|
||||
const events = new TestRemote(ctx, { settings: { describe, mutate } })
|
||||
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
|
||||
return {
|
||||
ctx, slots: ctx.get('slots') as SlotRegistry, describe, mutate, events,
|
||||
|
||||
@@ -32,17 +32,17 @@ async function bench(preference?: string) {
|
||||
revision,
|
||||
})
|
||||
const describeRpc = vi.fn(async () => ({
|
||||
rpcId: 'locale-describe' as never,
|
||||
result: { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [namespace()] } },
|
||||
ok: true as const,
|
||||
value: { writable: true, hasDocument: true, namespaces: [namespace()] },
|
||||
}))
|
||||
const mutate = vi.fn(async (request: { ops: { value: string }[] }) => {
|
||||
stored = request.ops[0]!.value
|
||||
const mutate = vi.fn(async (_ns: string, ops: { value: string }[]) => {
|
||||
stored = ops[0]!.value
|
||||
revision += 1
|
||||
return { rpcId: 'locale-mutate' as never, result: { ok: true as const, value: namespace() } }
|
||||
return { ok: true as const, value: namespace() }
|
||||
})
|
||||
ctx.provide('connection', { api: { settings: { describe: describeRpc, mutate } }, isLoopback: true } as never)
|
||||
ctx.provide('connection', { api: {}, isLoopback: true } as never)
|
||||
// The settings transport and the forwarded-event port the plugin injects.
|
||||
new TestRemote(ctx)
|
||||
new TestRemote(ctx, { settings: { describe: describeRpc, mutate } })
|
||||
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
|
||||
await ctx.plugin({ inject: [...inject], apply }).await()
|
||||
return { ctx, locale: ctx.get('locale') as LocaleRuntime }
|
||||
|
||||
@@ -50,7 +50,9 @@ export type { AgentPresetOption, AgentPresetSettingsState } from './settings-sto
|
||||
export { AGENT_PRESET_SETTINGS_NS, writeDefaultPreset } from './settings-store.ts'
|
||||
|
||||
/** Required services (cordis fiber inject). */
|
||||
export const inject = ['slots', 'locale', 'connection', 'remote', 'remote.agentPresets', 'settingsScope']
|
||||
export const inject = [
|
||||
'slots', 'locale', 'connection', 'remote', 'remote.agentPresets', 'remote.settings', 'settingsScope',
|
||||
]
|
||||
|
||||
/**
|
||||
* Mount the General-settings row.
|
||||
@@ -58,11 +60,12 @@ export const inject = ['slots', 'locale', 'connection', 'remote', 'remote.agentP
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
const { api } = ctx.get('connection') as ConnectionHandle
|
||||
const controller = new AgentPresetSettingsController(api, ctx.remote, ctx.settingsScope.describe())
|
||||
const settingsWire = { settings: ctx.remote.settings }
|
||||
const controller = new AgentPresetSettingsController(settingsWire, ctx.remote, ctx.settingsScope.describe())
|
||||
// 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, ctx.remote, () => {
|
||||
const section = new AgentPresetSectionController({ ...api, ...settingsWire }, ctx.remote, () => {
|
||||
void controller.load()
|
||||
for (const read of rosterReaders) read()
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
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'
|
||||
|
||||
@@ -133,7 +134,7 @@ export class AgentPresetSectionController {
|
||||
readonly store: SnapshotStore<AgentPresetSectionState> = createSnapshotStore(INITIAL)
|
||||
|
||||
constructor(
|
||||
private readonly api: Pick<IApiClient, 'agentPresets' | 'settings' | 'host'>,
|
||||
private readonly api: SettingsWireFace & Pick<IApiClient, 'agentPresets' | 'host'>,
|
||||
private readonly remote: Pick<ClientRemote, 'agentPresets'>,
|
||||
/**
|
||||
* Called after this page changes the roster DIRECTORY, so the other
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
* namespace's `default` field, which is what the host resolves at creation.
|
||||
*/
|
||||
|
||||
import type { ClientRemote, 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 { AgentPresetRoster } from '@deepseek-ai/dsh-agent-presets/types'
|
||||
import type { SettingsDescribeFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { SettingsDescribeFace, SettingsWireFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
|
||||
/** The agent-preset settings namespace on the host wire. */
|
||||
export const AGENT_PRESET_SETTINGS_NS = 'agent-presets'
|
||||
@@ -37,18 +37,22 @@ export function messageOf(error: unknown): string {
|
||||
* @returns the failure message, or undefined once the write landed.
|
||||
*/
|
||||
export async function writeDefaultPreset(
|
||||
api: Pick<IApiClient, 'settings'>,
|
||||
api: SettingsWireFace,
|
||||
id: string,
|
||||
): Promise<string | undefined> {
|
||||
let response
|
||||
try {
|
||||
response = await api.settings.update({ ns: AGENT_PRESET_SETTINGS_NS, patch: { default: id } })
|
||||
response = await api.settings.update(
|
||||
AGENT_PRESET_SETTINGS_NS,
|
||||
{ default: id },
|
||||
undefined,
|
||||
)
|
||||
} catch (error) {
|
||||
// The transport rejected rather than answering; the caller must be able to
|
||||
// say so instead of the row silently snapping back.
|
||||
return messageOf(error)
|
||||
}
|
||||
return response.result.ok ? undefined : response.result.error.message
|
||||
return response.ok ? undefined : response.error.message
|
||||
}
|
||||
|
||||
/** One selectable preset. */
|
||||
@@ -181,7 +185,7 @@ export class AgentPresetSettingsController {
|
||||
* @param describeFace - the shared mirror's describe face (writability source).
|
||||
*/
|
||||
constructor(
|
||||
private readonly api: Pick<IApiClient, 'settings'>,
|
||||
private readonly api: SettingsWireFace,
|
||||
private readonly remote: Pick<ClientRemote, 'agentPresets'>,
|
||||
private readonly describeFace: SettingsDescribeFace,
|
||||
) {}
|
||||
|
||||
@@ -70,8 +70,20 @@ async function bench() {
|
||||
const locale = new LocaleRuntime(ctx)
|
||||
locale.setLocale('zh')
|
||||
ctx.provide('locale', locale)
|
||||
const remote = new TestRemote(ctx)
|
||||
const calls: string[] = []
|
||||
// The row reads `describe` to learn whether this browser may write at all,
|
||||
// and its default write is the one op this spec records.
|
||||
const settings = {
|
||||
describe: () => Promise.resolve({
|
||||
ok: true as const,
|
||||
value: { writable: true, hasDocument: true, namespaces: [] },
|
||||
}),
|
||||
update: (_ns: string, patch: unknown) => {
|
||||
calls.push(`settings:${JSON.stringify(patch)}`)
|
||||
return Promise.resolve({ ok: true as const, value: {} })
|
||||
},
|
||||
}
|
||||
const remote = new TestRemote(ctx, { settings })
|
||||
// The roster and the switch are the AgentPresets Remote namespace; the
|
||||
// shared double carries no generated namespaces, so this spec stages its
|
||||
// own. Registered twice on purpose: the nested key satisfies the plugin's
|
||||
@@ -112,14 +124,6 @@ async function bench() {
|
||||
return Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: { opened: true as const } } })
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
// The row reads this to learn whether this browser may write at all.
|
||||
describe: () => Promise.resolve({
|
||||
rpcId: 'r',
|
||||
result: { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [] } },
|
||||
}),
|
||||
update: (payload: { patch: unknown }) => { calls.push(`settings:${JSON.stringify(payload.patch)}`); return Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: {} } }) },
|
||||
},
|
||||
},
|
||||
} as never)
|
||||
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
|
||||
@@ -182,7 +186,9 @@ function sessionsDouble(state: {
|
||||
|
||||
describe('ui-agent-preset apply', () => {
|
||||
it('declares the services it uses', () => {
|
||||
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'remote.agentPresets', 'settingsScope'])
|
||||
expect(inject).toEqual([
|
||||
'slots', 'locale', 'connection', 'remote', 'remote.agentPresets', 'remote.settings', 'settingsScope',
|
||||
])
|
||||
})
|
||||
|
||||
it('registers the General row and the settings section', async () => {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ClientRemote, IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SettingsWireFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import { AgentPresetSectionController, draftBlocker } from '../src/client/section-store.ts'
|
||||
import type { CopyDraft, PresetRow } from '../src/client/section-store.ts'
|
||||
|
||||
@@ -65,7 +66,7 @@ const remoteFail = (message: string) =>
|
||||
function fakeApi(
|
||||
defaultId: { id: string },
|
||||
options: FakeOptions = {},
|
||||
): Pick<IApiClient, 'agentPresets' | 'settings' | 'host'> {
|
||||
): SettingsWireFace & Pick<IApiClient, 'agentPresets' | 'host'> {
|
||||
const record = (method: string, payload: unknown): void => { options.calls?.push({ method, payload }) }
|
||||
return {
|
||||
host: {
|
||||
@@ -84,15 +85,15 @@ function fakeApi(
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
update: (payload: { ns: string; patch: { default?: string } }) => {
|
||||
record('settings.update', payload)
|
||||
if (options.failSettings !== undefined) return fail(options.failSettings)
|
||||
/* v8 ignore next -- the controller only ever patches `default` */
|
||||
defaultId.id = payload.patch.default ?? defaultId.id
|
||||
return ok({})
|
||||
update: (ns: string, patch: { default?: string }) => {
|
||||
record('settings.update', { ns, patch })
|
||||
if (options.failSettings !== undefined) return remoteFail(options.failSettings)
|
||||
/* v8 ignore next -- the controller only ever sets `default` */
|
||||
defaultId.id = patch.default ?? defaultId.id
|
||||
return remoteOk({})
|
||||
},
|
||||
},
|
||||
} as unknown as Pick<IApiClient, 'agentPresets' | 'settings' | 'host'>
|
||||
} as unknown as SettingsWireFace & Pick<IApiClient, 'agentPresets' | 'host'>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -579,7 +580,7 @@ describe('deleting', () => {
|
||||
await controller.load()
|
||||
presets.clear()
|
||||
const broken = new AgentPresetSectionController(
|
||||
{ agentPresets: {}, settings: {}, host: {} } as unknown as Pick<IApiClient, 'agentPresets' | 'settings' | 'host'>,
|
||||
{ agentPresets: {}, settings: {}, host: {} } as unknown as SettingsWireFace & Pick<IApiClient, 'agentPresets' | 'host'>,
|
||||
{
|
||||
agentPresets: {
|
||||
list: () => Promise.reject(new Error('gone')),
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ClientRemote, IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SettingsWireFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/src/client/settings-mirror.ts'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
|
||||
/** The two faces the row reads: the roster Remote and the settings wire. */
|
||||
interface FakeWire {
|
||||
api: IApiClient
|
||||
api: SettingsWireFace
|
||||
remote: Pick<ClientRemote, 'agentPresets'>
|
||||
}
|
||||
|
||||
@@ -28,7 +29,7 @@ import { AgentPresetSeatController } from '../src/client/seat-store.ts'
|
||||
|
||||
type SeatSession = Pick<SessionSummary, 'id' | 'blank' | 'projectionValues'>
|
||||
|
||||
interface Recorded { ns: string; patch: unknown }
|
||||
interface Recorded { ns: string; ops: unknown }
|
||||
|
||||
/** A roster Remote answering a fixed set of rows, or refusing. */
|
||||
function fakeRoster(
|
||||
@@ -66,26 +67,23 @@ function fakeApi(
|
||||
// Host persistence is enabled in production only on the selected client path; a read-only provider answers writable:false
|
||||
// and the row disables its control instead of offering a refused write.
|
||||
describe: () => Promise.resolve({
|
||||
rpcId: 'r',
|
||||
result: {
|
||||
ok: true as const,
|
||||
value: { writable: options.readOnly !== true, hasDocument: true, namespaces: [] },
|
||||
},
|
||||
ok: true as const,
|
||||
value: { writable: options.readOnly !== true, hasDocument: true, namespaces: [] },
|
||||
}),
|
||||
update: (payload: { ns: string; patch: unknown }) => {
|
||||
options.writes?.push({ ns: payload.ns, patch: payload.patch })
|
||||
update: (ns: string, patch: { default?: unknown }) => {
|
||||
options.writes?.push({ ns, ops: patch })
|
||||
if (options.failWriteWith !== undefined) return Promise.reject(options.failWriteWith)
|
||||
if (options.failWrite !== undefined) {
|
||||
return Promise.resolve({ rpcId: 'r', result: { ok: false as const, error: { code: 'internal', message: options.failWrite, details: {} } } })
|
||||
return Promise.resolve({ ok: false as const, error: { code: 'internal', message: options.failWrite, details: {} } })
|
||||
}
|
||||
// A committed write moves the roster's default.
|
||||
for (const preset of presets) {
|
||||
preset.isDefault = preset.id === (payload.patch as { default?: string }).default
|
||||
preset.isDefault = preset.id === patch.default
|
||||
}
|
||||
return Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: {} } })
|
||||
return Promise.resolve({ ok: true as const, value: {} })
|
||||
},
|
||||
},
|
||||
} as unknown as IApiClient
|
||||
} as unknown as SettingsWireFace
|
||||
return {
|
||||
api,
|
||||
remote: fakeRoster(presets, options.failList === undefined ? {} : { failList: options.failList }),
|
||||
@@ -165,7 +163,7 @@ describe('the agent-preset settings controller', () => {
|
||||
|
||||
it('treats an unavailable optional namespace as an empty roster', async () => {
|
||||
const controller = derivedController({
|
||||
api: {} as IApiClient,
|
||||
api: {} as SettingsWireFace,
|
||||
remote: fakeRoster([], {
|
||||
failList: 'no active Remote method exports this endpoint',
|
||||
failListCode: 'invocation-unavailable',
|
||||
@@ -187,7 +185,10 @@ describe('the agent-preset settings controller', () => {
|
||||
|
||||
await controller.select('minimal')
|
||||
|
||||
expect(writes).toEqual([{ ns: AGENT_PRESET_SETTINGS_NS, patch: { default: 'minimal' } }])
|
||||
expect(writes).toEqual([{
|
||||
ns: AGENT_PRESET_SETTINGS_NS,
|
||||
ops: { default: 'minimal' },
|
||||
}])
|
||||
expect(controller.store.getSnapshot().currentValue).toBe('minimal')
|
||||
})
|
||||
|
||||
@@ -260,7 +261,7 @@ describe('the agent-preset settings controller', () => {
|
||||
|
||||
it('reports a transport that rejects rather than answering', async () => {
|
||||
const controller = derivedController({
|
||||
api: {} as IApiClient,
|
||||
api: {} as SettingsWireFace,
|
||||
remote: fakeRoster([], { throwOnList: true }),
|
||||
})
|
||||
|
||||
@@ -310,7 +311,7 @@ describe('the new-session chip controller', () => {
|
||||
},
|
||||
select: (agentId: SessionId, agentPreset: string) => {
|
||||
if (options.throwOn === 'select') return Promise.reject(new Error('socket closed'))
|
||||
options.writes?.push({ ns: 'select', patch: agentPreset })
|
||||
options.writes?.push({ ns: 'select', ops: agentPreset })
|
||||
return Promise.resolve(options.failSelect === undefined
|
||||
? { ok: true as const, value: agentPreset }
|
||||
: {
|
||||
@@ -434,7 +435,7 @@ describe('the new-session chip controller', () => {
|
||||
await controller.load()
|
||||
await controller.select('minimal')
|
||||
|
||||
expect(writes).toEqual([{ ns: 'select', patch: 'minimal' }])
|
||||
expect(writes).toEqual([{ ns: 'select', ops: 'minimal' }])
|
||||
expect(controller.store.getSnapshot().current).toBe('minimal')
|
||||
})
|
||||
|
||||
@@ -453,7 +454,7 @@ describe('the new-session chip controller', () => {
|
||||
|
||||
// Every later list movement calls apply(); an unspent stage would keep
|
||||
// switching sessions the user never picked for.
|
||||
expect(writes).toEqual([{ ns: 'select', patch: 'minimal' }])
|
||||
expect(writes).toEqual([{ ns: 'select', ops: 'minimal' }])
|
||||
})
|
||||
|
||||
it('drops the stage against a session that already started', async () => {
|
||||
@@ -535,7 +536,7 @@ describe('the new-session chip controller', () => {
|
||||
await controller.select('standard')
|
||||
await first
|
||||
|
||||
expect(writes).toEqual([{ ns: 'select', patch: 'minimal' }])
|
||||
expect(writes).toEqual([{ ns: 'select', ops: 'minimal' }])
|
||||
})
|
||||
|
||||
it('keeps a staged pick across a roster refresh', async () => {
|
||||
@@ -570,7 +571,7 @@ describe('the new-session chip controller', () => {
|
||||
const controller = derivedController({
|
||||
// The roster answered; the mirror's read is what failed, so the row
|
||||
// shows the current default without offering a write it never confirmed.
|
||||
api: { settings: { describe: () => Promise.reject(new Error('socket closed')) } } as unknown as IApiClient,
|
||||
api: { settings: { describe: () => Promise.reject(new Error('socket closed')) } } as unknown as SettingsWireFace,
|
||||
remote: fakeRoster([{ id: 'standard', trust: 'system', isDefault: true }]),
|
||||
})
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -51,7 +50,6 @@
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@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-settings": "workspace:^",
|
||||
@@ -65,7 +63,6 @@
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@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-store": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* created later through the host Settings API.
|
||||
*/
|
||||
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
||||
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
||||
@@ -44,7 +43,10 @@ export type {
|
||||
} from './settings-store.ts'
|
||||
|
||||
/** Required services (cordis fiber inject). */
|
||||
export const inject = ['commandUi', 'sessions', 'slots', 'locale', 'connection', 'remote', 'settingsScope', 'settingsSchema']
|
||||
export const inject = [
|
||||
'commandUi', 'sessions', 'slots', 'locale', 'remote', 'remote.settings',
|
||||
'settingsScope', 'settingsSchema',
|
||||
]
|
||||
|
||||
const ACCESS_NS = 'permission.access'
|
||||
|
||||
@@ -113,10 +115,9 @@ export function apply(ctx: ClientContext): void {
|
||||
|
||||
ctx.effect(() => ctx.locale.register('settings.permission', { zh, en }), 'ui-permission: settings row dictionaries')
|
||||
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
// The shared SettingsScope mirror updates after document commits and reconnects.
|
||||
const controller = new PermissionPresetSettingsController(
|
||||
ctx.settingsScope.describe(), connection.api, ctx.settingsSchema)
|
||||
ctx.settingsScope.describe(), { settings: ctx.remote.settings }, ctx.settingsSchema)
|
||||
const load = (): Promise<void> => controller.load()
|
||||
const select = (preset: string): Promise<void> => controller.select(preset)
|
||||
const injected = (): PermissionRowInjected => ({
|
||||
|
||||
@@ -6,14 +6,12 @@
|
||||
* back into the mirror.
|
||||
*/
|
||||
|
||||
import type {
|
||||
IApiClient, SettingsNamespaceView,
|
||||
} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import {
|
||||
createSnapshotStore, type SnapshotStore,
|
||||
} from '@deepseek-ai/dsh-client-store'
|
||||
import type {
|
||||
SchemaNode, SettingsDescribeFace, SettingsSchemaService,
|
||||
SchemaNode, SettingsDescribeFace, SettingsSchemaService, SettingsWireFace,
|
||||
} from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import { displayPermissionPreset } from './presentation.ts'
|
||||
|
||||
@@ -101,7 +99,7 @@ export class PermissionPresetSettingsController {
|
||||
*/
|
||||
constructor(
|
||||
private readonly describeFace: SettingsDescribeFace,
|
||||
private readonly api: Pick<IApiClient, 'settings'>,
|
||||
private readonly api: SettingsWireFace,
|
||||
private readonly schema: SettingsSchemaService,
|
||||
) {}
|
||||
|
||||
@@ -139,17 +137,17 @@ export class PermissionPresetSettingsController {
|
||||
draft.error = null
|
||||
})
|
||||
try {
|
||||
const response = await this.api.settings.mutate({
|
||||
ns: PERMISSION_SETTINGS_NS,
|
||||
ops: [{ op: 'set', path: ['defaultPreset'], value: preset }],
|
||||
expectedRevision: view.revision,
|
||||
})
|
||||
if (!response.result.ok) throw new Error(response.result.error.message)
|
||||
const response = await this.api.settings.mutate(
|
||||
PERMISSION_SETTINGS_NS,
|
||||
[{ op: 'set', path: ['defaultPreset'], value: preset }],
|
||||
view.revision,
|
||||
)
|
||||
if (!response.ok) throw new Error(response.error.message)
|
||||
this.saving = false
|
||||
if (this.disposed) return
|
||||
// The mirror publish reaches this row's own subscription, so the fold
|
||||
// is also what republishes the accepted value here.
|
||||
this.describeFace.acceptView(response.result.value)
|
||||
this.describeFace.acceptView(response.value)
|
||||
} catch (error) {
|
||||
this.saving = false
|
||||
if (this.disposed) return
|
||||
|
||||
@@ -13,7 +13,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { TestRemote, scriptedSettingsRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { apply as settingsApply, inject as settingsInject } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { CommandDecoration } from '@deepseek-ai/dsh-client-ui-commands/client'
|
||||
import type { PermissionSelect } from '@deepseek-ai/dsh-permission-presets/client'
|
||||
@@ -40,7 +40,8 @@ async function bench() {
|
||||
const locale = new LocaleRuntime(ctx)
|
||||
locale.setLocale('en')
|
||||
ctx.provide('locale', locale)
|
||||
const remote = new TestRemote(ctx)
|
||||
const settingsRemote = scriptedSettingsRemote()
|
||||
const remote = new TestRemote(ctx, { settings: settingsRemote.settings })
|
||||
ctx.slots.register({
|
||||
name: 'root',
|
||||
children: {
|
||||
|
||||
@@ -43,8 +43,9 @@ function view(defaultPreset: string, revision = 0): SettingsNamespaceView {
|
||||
}
|
||||
}
|
||||
|
||||
/** The settings namespace answers over the Remote carrier, which has no envelope. */
|
||||
function ok<T>(value: T) {
|
||||
return { rpcId: 'test', result: { ok: true as const, value } }
|
||||
return { ok: true as const, value }
|
||||
}
|
||||
|
||||
const dictionary: Record<string, string> = en
|
||||
@@ -153,11 +154,8 @@ describe('PermissionRow', () => {
|
||||
settings: {
|
||||
describe: () => describe.promise,
|
||||
mutate: () => Promise.resolve({
|
||||
rpcId: 'test',
|
||||
result: {
|
||||
ok: false as const,
|
||||
error: { code: 'settings-conflict', message: 'changed elsewhere', details: {} },
|
||||
},
|
||||
ok: false as const,
|
||||
error: { code: 'settings-conflict', message: 'changed elsewhere', details: {} },
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -35,8 +35,9 @@ function view(defaultPreset: string, revision = 0, schema: SettingsNamespaceView
|
||||
}
|
||||
}
|
||||
|
||||
/** The settings namespace answers over the Remote carrier, which has no envelope. */
|
||||
function ok<T>(value: T) {
|
||||
return { rpcId: 'test', result: { ok: true as const, value } }
|
||||
return { ok: true as const, value }
|
||||
}
|
||||
|
||||
/** The permission controller over a real mirror and one fake wire. */
|
||||
@@ -117,11 +118,11 @@ describe('permission settings store', () => {
|
||||
revision: 4,
|
||||
})
|
||||
await controller.select('workspace-write')
|
||||
expect(mutate).toHaveBeenCalledWith({
|
||||
ns: 'permission',
|
||||
ops: [{ op: 'set', path: ['defaultPreset'], value: 'workspace-write' }],
|
||||
expectedRevision: 4,
|
||||
})
|
||||
expect(mutate).toHaveBeenCalledWith(
|
||||
'permission',
|
||||
[{ op: 'set', path: ['defaultPreset'], value: 'workspace-write' }],
|
||||
4,
|
||||
)
|
||||
expect(controller.store.getSnapshot()).toMatchObject({
|
||||
status: 'ready',
|
||||
currentValue: 'workspace-write',
|
||||
@@ -140,11 +141,8 @@ describe('permission settings store', () => {
|
||||
const failing = permissionController({
|
||||
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
|
||||
mutate: () => Promise.resolve({
|
||||
rpcId: 'test',
|
||||
result: {
|
||||
ok: false as const,
|
||||
error: { code: 'settings-conflict', message: 'stale', details: {} },
|
||||
},
|
||||
ok: false as const,
|
||||
error: { code: 'settings-conflict', message: 'stale', details: {} },
|
||||
}),
|
||||
}).controller
|
||||
await failing.load()
|
||||
@@ -171,8 +169,8 @@ describe('permission settings store', () => {
|
||||
|
||||
const rejected = permissionController({
|
||||
describe: () => Promise.resolve({
|
||||
rpcId: 'test',
|
||||
result: { ok: false as const, error: { code: 'internal', message: 'offline', details: {} } },
|
||||
ok: false as const,
|
||||
error: { code: 'internal', message: 'offline', details: {} },
|
||||
}),
|
||||
mutate,
|
||||
}).controller
|
||||
|
||||
@@ -32,14 +32,11 @@ async function bench(isLoopback = true) {
|
||||
locale.setLocale('zh')
|
||||
ctx.provide('locale', locale)
|
||||
const settingsDescribe = vi.fn(() => Promise.resolve({
|
||||
rpcId: 'settings-general' as never,
|
||||
result: {
|
||||
ok: true as const,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: true,
|
||||
namespaces: [],
|
||||
},
|
||||
ok: true as const,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: true,
|
||||
namespaces: [],
|
||||
},
|
||||
}))
|
||||
const settingsOpenDocument = vi.fn(() => Promise.resolve({
|
||||
@@ -47,10 +44,10 @@ async function bench(isLoopback = true) {
|
||||
result: { ok: true as const, value: { opened: true as const } },
|
||||
}))
|
||||
ctx.provide('connection', {
|
||||
api: { settings: { describe: settingsDescribe, openDocument: settingsOpenDocument } },
|
||||
api: { settings: { openDocument: settingsOpenDocument } },
|
||||
isLoopback,
|
||||
} as never)
|
||||
new TestRemote(ctx)
|
||||
new TestRemote(ctx, { settings: { describe: settingsDescribe } })
|
||||
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
|
||||
return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, settingsDescribe, settingsOpenDocument }
|
||||
}
|
||||
|
||||
@@ -77,11 +77,8 @@ describe('SettingsDocumentAction', () => {
|
||||
const controller = derivedDocumentStore({
|
||||
settings: {
|
||||
describe: vi.fn(() => Promise.resolve({
|
||||
rpcId: 'document-action' as never,
|
||||
result: {
|
||||
ok: true as const,
|
||||
value: { writable: true, hasDocument: true, namespaces: [] },
|
||||
},
|
||||
ok: true as const,
|
||||
value: { writable: true, hasDocument: true, namespaces: [] },
|
||||
})),
|
||||
openDocument,
|
||||
},
|
||||
@@ -99,14 +96,8 @@ describe('SettingsDocumentAction', () => {
|
||||
|
||||
it('stays absent without a document and follows a mirror refresh to available', async () => {
|
||||
const describe = vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
rpcId: 'document-action-absent' as never,
|
||||
result: { ok: true as const, value: { writable: true, hasDocument: false, namespaces: [] } },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
rpcId: 'document-action-ready' as never,
|
||||
result: { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [] } },
|
||||
})
|
||||
.mockResolvedValueOnce({ ok: true as const, value: { writable: true, hasDocument: false, namespaces: [] } })
|
||||
.mockResolvedValueOnce({ ok: true as const, value: { writable: true, hasDocument: true, namespaces: [] } })
|
||||
const wire = { settings: { describe, openDocument: vi.fn() } } as never
|
||||
const mirror = new SettingsDescribeMirror(wire)
|
||||
const controller = new SettingsDocumentStore(wire, mirror)
|
||||
@@ -138,11 +129,8 @@ describe('SettingsDocumentAction', () => {
|
||||
const controller = derivedDocumentStore({
|
||||
settings: {
|
||||
describe: vi.fn(() => Promise.resolve({
|
||||
rpcId: 'document-action' as never,
|
||||
result: {
|
||||
ok: true as const,
|
||||
value: { writable: true, hasDocument: true, namespaces: [] },
|
||||
},
|
||||
ok: true as const,
|
||||
value: { writable: true, hasDocument: true, namespaces: [] },
|
||||
})),
|
||||
openDocument: vi.fn(() => Promise.resolve({
|
||||
rpcId: 'document-open-failed' as never,
|
||||
|
||||
@@ -9,18 +9,8 @@ function derivedDocumentStore(api: object) {
|
||||
return new SettingsDocumentStore(wire, new SettingsDescribeMirror(wire))
|
||||
}
|
||||
|
||||
function response(hasDocument = false): RpcResponse<{
|
||||
writable: boolean
|
||||
hasDocument: boolean
|
||||
namespaces: []
|
||||
}> {
|
||||
return {
|
||||
rpcId: 'settings-document' as never,
|
||||
result: {
|
||||
ok: true,
|
||||
value: { writable: true, hasDocument, namespaces: [] },
|
||||
},
|
||||
}
|
||||
function response(hasDocument = false) {
|
||||
return { ok: true, value: { writable: true, hasDocument, namespaces: [] } }
|
||||
}
|
||||
|
||||
function opened(): RpcResponse<{ opened: true }> {
|
||||
@@ -30,11 +20,8 @@ function opened(): RpcResponse<{ opened: true }> {
|
||||
}
|
||||
}
|
||||
|
||||
function describeFailed(message: string): RpcResponse<never> {
|
||||
return {
|
||||
rpcId: 'settings-document-failed' as never,
|
||||
result: { ok: false, error: { code: 'internal', message, details: {} } },
|
||||
}
|
||||
function describeFailed(message: string) {
|
||||
return { ok: false as const, error: { code: 'internal', message, details: {} } }
|
||||
}
|
||||
|
||||
describe('SettingsDocumentStore', () => {
|
||||
|
||||
@@ -18,11 +18,14 @@ async function bench() {
|
||||
getSnapshot: () => ({ active: 'zh', locales: [], revision: 0 }),
|
||||
subscribe: () => () => {},
|
||||
} as never)
|
||||
ctx.provide('connection', {
|
||||
api: { settings: { describe: async () => ({ result: { ok: false } }) } },
|
||||
isLoopback: false,
|
||||
} as never)
|
||||
ctx.provide('remote', { $on: () => () => {} } as never)
|
||||
ctx.provide('connection', { api: {}, isLoopback: false } as never)
|
||||
// The shell mounts ui-settings, which injects `remote.settings`; without the
|
||||
// namespace provided its fiber parks and no slot is ever declared.
|
||||
const settings = {
|
||||
describe: async () => ({ ok: false, error: { code: 'internal', message: 'no settings', details: {} } }),
|
||||
}
|
||||
ctx.provide('remote', { $on: () => () => {}, settings } as never)
|
||||
ctx.provide('remote.settings', settings as never)
|
||||
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
|
||||
return { ctx, slots: ctx.get('slots') as SlotRegistry }
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* the provider editor with extra fields: the route id is being *chosen* here,
|
||||
* and the settings address does not exist until it is. One `settings.mutate`
|
||||
* sets the whole profile at `providers.<route>`; the key travels separately
|
||||
* through `credentials.set` under the reference the profile records, exactly as
|
||||
* through `credentials/set` under the reference the profile records, exactly as
|
||||
* an existing provider's key does.
|
||||
*
|
||||
* The three fields a hand-declared route cannot default — endpoint, protocol,
|
||||
@@ -23,13 +23,14 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { JsonValue } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { apiKeyFailure } from './apiKey.ts'
|
||||
import { EditorFooter } from './EditorFooter.tsx'
|
||||
import { validateDeepSeekModels } from './DeepSeekModelsEditor.tsx'
|
||||
import { ModelListEditor } from './ModelListEditor.tsx'
|
||||
import type { ModelDraft } from './ModelListEditor.tsx'
|
||||
import { deriveKeyRef, messageOf } from './store.ts'
|
||||
import type { ModelsWire } from './store.ts'
|
||||
import type { en } from './locales.ts'
|
||||
import styles from './ModelsSection.module.css'
|
||||
|
||||
@@ -59,7 +60,7 @@ export interface CustomProviderCardProps {
|
||||
*/
|
||||
revision: number
|
||||
/** Wire faces for the write and for interrogating the endpoint. */
|
||||
api: Pick<IApiClient, 'settings' | 'credentials' | 'llm'>
|
||||
api: ModelsWire
|
||||
/** Section copy. */
|
||||
t: (key: keyof typeof en) => string
|
||||
/** Disable writes (read-only settings provider). */
|
||||
@@ -143,15 +144,15 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
|
||||
baseURL,
|
||||
models: models.map(model => ({ ...model })),
|
||||
}
|
||||
const response = await api.settings.mutate({
|
||||
ns: NS,
|
||||
ops: [{ op: 'set', path: ['providers', route], value: profile }],
|
||||
// `taken` is a snapshot too, so the id check alone cannot see a route
|
||||
// declared after this card opened; the revision makes that race a
|
||||
// `settings-conflict` instead of a write over the other profile.
|
||||
expectedRevision: openedAt,
|
||||
})
|
||||
if (!response.result.ok) return response.result.error.message
|
||||
// `taken` is a snapshot too, so the id check alone cannot see a route
|
||||
// declared after this card opened; the revision makes that race a
|
||||
// `settings-conflict` instead of a write over the other profile.
|
||||
const response = await api.settings.mutate(
|
||||
NS,
|
||||
[{ op: 'set', path: ['providers', route], value: profile as JsonValue }],
|
||||
openedAt,
|
||||
)
|
||||
if (!response.ok) return response.error.message
|
||||
// The provider now exists. A retry after the key write below fails must
|
||||
// not re-run this mutate: the revision it holds is the one this write
|
||||
// just superseded, so the Host would answer `settings-conflict` and the
|
||||
@@ -159,10 +160,10 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
|
||||
setCommitted(true)
|
||||
}
|
||||
if (storesKey) {
|
||||
const stored = await api.credentials.set({ ref: keyRef, value: keyValue })
|
||||
const stored = await api.credentials.set(keyRef, keyValue)
|
||||
// The profile landed; saying the key did not is the only honest report,
|
||||
// and the retry above now goes straight back to this write.
|
||||
if (!stored.result.ok) return stored.result.error.message
|
||||
if (!stored.ok) return stored.error.message
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { ModelsSettingsState, ModelsSettingsStore } from './store.ts'
|
||||
import type { ModelsSettingsState, ModelsSettingsStore, ModelsWire } from './store.ts'
|
||||
import { onboardingReadiness } from './store.ts'
|
||||
import type { SettingsSchemaOperations } from './schema-operations.ts'
|
||||
import { ProviderEditor } from './ProviderEditor.tsx'
|
||||
@@ -28,7 +27,7 @@ export interface DeepSeekOnboardingInjected {
|
||||
/** Shared Models-page join controller. */
|
||||
controller: ModelsSettingsStore
|
||||
/** Existing wire face reused by the Models credential editor. */
|
||||
api: Pick<IApiClient, 'settings' | 'credentials' | 'llm'>
|
||||
api: ModelsWire
|
||||
/** Settings schema and immutable path callbacks. */
|
||||
schema: SettingsSchemaOperations
|
||||
/** Feature copy. */
|
||||
|
||||
@@ -14,14 +14,13 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { Button, IconPlusOutline16, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { InjectFace, PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
// Type-only: pulls this package's SlotMap merge (the two Models child slots).
|
||||
import type {} from './slot-contract.ts'
|
||||
import { CustomProviderCard } from './CustomProviderCard.tsx'
|
||||
import { deriveKeyRef, messageOf, protocolChoices, providerUsable } from './store.ts'
|
||||
import type { ModelsSettingsStore, ProviderRow } from './store.ts'
|
||||
import type { ModelsSettingsStore, ModelsWire, ProviderRow } from './store.ts'
|
||||
import type { SettingsSchemaOperations } from './schema-operations.ts'
|
||||
import { ProviderEditor, type ProviderEditorProps } from './ProviderEditor.tsx'
|
||||
import { SubagentModelSelectionCard } from './SubagentModelSelectionCard.tsx'
|
||||
@@ -37,7 +36,7 @@ export interface ModelsSectionInjected {
|
||||
snapshot: ModelsSettingsStore['store']
|
||||
}
|
||||
/** Wire faces the editor writes through. */
|
||||
api: Pick<IApiClient, 'settings' | 'credentials' | 'llm'>
|
||||
api: ModelsWire
|
||||
/** Settings schema and immutable path callbacks. */
|
||||
schema: SettingsSchemaOperations
|
||||
/** Section copy. */
|
||||
@@ -112,20 +111,21 @@ function renderProviderEditor({ target, ...props }: ProviderEditorRenderProps):
|
||||
* @returns the failure message, or undefined once the write and reload landed.
|
||||
*/
|
||||
export async function removeProviderProfile(
|
||||
api: Pick<IApiClient, 'settings' | 'credentials'>,
|
||||
api: Pick<ModelsWire, 'settings' | 'credentials'>,
|
||||
controller: ModelsSettingsStore,
|
||||
target: { settingsNs: string; settingsPath: readonly string[]; credentialRef?: string },
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
if (target.credentialRef !== undefined) {
|
||||
const credential = await api.credentials.unset({ ref: target.credentialRef })
|
||||
if (!credential.result.ok) return credential.result.error.message
|
||||
const credential = await api.credentials.unset(target.credentialRef)
|
||||
if (!credential.ok) return credential.error.message
|
||||
}
|
||||
const response = await api.settings.mutate({
|
||||
ns: target.settingsNs,
|
||||
ops: [{ op: 'unset', path: [...target.settingsPath] }],
|
||||
})
|
||||
if (!response.result.ok) return response.result.error.message
|
||||
const response = await api.settings.mutate(
|
||||
target.settingsNs,
|
||||
[{ op: 'unset', path: [...target.settingsPath] }],
|
||||
undefined,
|
||||
)
|
||||
if (!response.ok) return response.error.message
|
||||
} catch (error) {
|
||||
// The transport rejected rather than answering; the caller must be able
|
||||
// to retry the idempotent operation instead of the row silently staying.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* One provider's editor card, hand-written per adapter family: the primary
|
||||
* field is a single write-only **API key** input (the page never asks for an
|
||||
* environment-variable name — a typed key stores through `credentials.set`
|
||||
* environment-variable name — a typed key stores through `credentials/set`
|
||||
* under the profile's reference, deriving `<ROUTE>_API_KEY` when the profile
|
||||
* has none. The pi-ai profile records that derivation as `apiKeyEnv` only when
|
||||
* a key is entered; a blank key materializes a reference-free profile for
|
||||
@@ -23,7 +23,9 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { CredentialView, IApiClient, SettingsNamespaceView, SettingsPathOpView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type {
|
||||
CredentialInfo, JsonValue, SettingsNamespaceView, SettingsPathOpView,
|
||||
} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import {
|
||||
DeepSeekModelsEditor, modelDrafts, validateDeepSeekModels,
|
||||
} from './DeepSeekModelsEditor.tsx'
|
||||
@@ -31,6 +33,7 @@ import { apiKeyFailure } from './apiKey.ts'
|
||||
import { EditorFooter } from './EditorFooter.tsx'
|
||||
import { ModelListEditor } from './ModelListEditor.tsx'
|
||||
import { deriveKeyRef, messageOf, protocolChoices } from './store.ts'
|
||||
import type { ModelsWire } from './store.ts'
|
||||
import type { SettingsSchemaOperations } from './schema-operations.ts'
|
||||
import type { en } from './locales.ts'
|
||||
import styles from './ModelsSection.module.css'
|
||||
@@ -64,7 +67,7 @@ export interface ProviderEditorProps {
|
||||
/** Path from the section root to this provider's profile. */
|
||||
settingsPath: readonly string[]
|
||||
/** Wire faces for writes and for interrogating a provider endpoint. */
|
||||
api: Pick<IApiClient, 'settings' | 'credentials' | 'llm'>
|
||||
api: ModelsWire
|
||||
/** Section copy. */
|
||||
t: (key: keyof typeof en) => string
|
||||
/** Disable writes (read-only settings provider). */
|
||||
@@ -117,7 +120,7 @@ export function pathOps(
|
||||
const ops: SettingsPathOpView[] = []
|
||||
for (const [key, value] of Object.entries(after)) {
|
||||
if (JSON.stringify(previous[key]) === JSON.stringify(value)) continue
|
||||
ops.push({ op: 'set', path: [...base, key], value })
|
||||
ops.push({ op: 'set', path: [...base, key], value: value as JsonValue })
|
||||
}
|
||||
for (const key of Object.keys(previous)) {
|
||||
if (!(key in after)) ops.push({ op: 'unset', path: [...base, key] })
|
||||
@@ -155,7 +158,7 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
|
||||
const { namespace, schema, settingsPath, api, t } = props
|
||||
const [draft, setDraft] = useState<Record<string, unknown>>(() => draftAt(schema, namespace, settingsPath))
|
||||
const [keyDraft, setKeyDraft] = useState('')
|
||||
const [keyState, setKeyState] = useState<CredentialView | undefined>(undefined)
|
||||
const [keyState, setKeyState] = useState<CredentialInfo | undefined>(undefined)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [failure, setFailure] = useState<string | undefined>(undefined)
|
||||
// A settings success advances both retry baselines immediately. Keeping the
|
||||
@@ -187,10 +190,10 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
|
||||
// neither a business rejection nor a transport failure may reach the
|
||||
// browser as an unhandled rejection, so the card simply renders without
|
||||
// the "already configured" hint.
|
||||
void api.credentials.describe({ refs: [keyRef] }).then(
|
||||
void api.credentials.describe([keyRef]).then(
|
||||
(response) => {
|
||||
if (stale || !response.result.ok) return
|
||||
setKeyState(response.result.value.credentials[keyRef])
|
||||
if (stale || !response.ok) return
|
||||
setKeyState(response.value[keyRef])
|
||||
},
|
||||
() => undefined,
|
||||
)
|
||||
@@ -279,19 +282,19 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
|
||||
? [{ op: 'set', path: [...settingsPath], value: {} }]
|
||||
: pathOps(settingsPath, committedOriginal, next)
|
||||
if (ops.length > 0) {
|
||||
const response = await api.settings.mutate({ ns, ops, expectedRevision })
|
||||
if (!response.result.ok) {
|
||||
return response.result.error.code === 'settings-conflict'
|
||||
const response = await api.settings.mutate(ns, ops, expectedRevision)
|
||||
if (!response.ok) {
|
||||
return response.error.code === 'settings-conflict'
|
||||
? t('conflict')
|
||||
: response.result.error.message
|
||||
: response.error.message
|
||||
}
|
||||
setCommittedOriginal(schema.getPath(response.result.value.user, settingsPath))
|
||||
setExpectedRevision(response.result.value.revision)
|
||||
setCommittedOriginal(schema.getPath(response.value.user, settingsPath))
|
||||
setExpectedRevision(response.value.revision)
|
||||
setDraft(next)
|
||||
}
|
||||
if (keyValue.length > 0) {
|
||||
const stored = await api.credentials.set({ ref: keyRef, value: keyValue })
|
||||
if (!stored.result.ok) return stored.result.error.message
|
||||
const stored = await api.credentials.set(keyRef, keyValue)
|
||||
if (!stored.ok) return stored.error.message
|
||||
}
|
||||
setKeyDraft('')
|
||||
return undefined
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { IApiClient, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SettingsWireFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { ModelsSettingsStore } from './store.ts'
|
||||
import type { en } from './locales.ts'
|
||||
import { messageOf } from './store.ts'
|
||||
@@ -15,7 +16,7 @@ export interface SubagentModelSelectionCardProps {
|
||||
/** Whether the settings provider accepts writes. */
|
||||
writable: boolean
|
||||
/** Settings wire face. */
|
||||
api: Pick<IApiClient, 'settings'>
|
||||
api: SettingsWireFace
|
||||
/** Models page controller to refresh after a commit. */
|
||||
controller: ModelsSettingsStore
|
||||
/** Localized Models copy. */
|
||||
@@ -45,13 +46,13 @@ export function SubagentModelSelectionCard({
|
||||
setSaving(true)
|
||||
setSaved(false)
|
||||
setError(undefined)
|
||||
void api.settings.update({
|
||||
ns: namespace.ns,
|
||||
patch: { enabled: !enabled },
|
||||
expectedRevision: namespace.revision,
|
||||
}).then(async (response) => {
|
||||
if (!response.result.ok) throw new Error(response.result.error.message)
|
||||
controller.acceptNamespace(response.result.value)
|
||||
void api.settings.update(
|
||||
namespace.ns,
|
||||
{ enabled: !enabled },
|
||||
namespace.revision,
|
||||
).then(async (response) => {
|
||||
if (!response.ok) throw new Error(response.error.message)
|
||||
controller.acceptNamespace(response.value)
|
||||
await controller.load()
|
||||
setSaved(true)
|
||||
}).catch((reason: unknown) => {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { WelcomeNotice } from './WelcomeNotice.tsx'
|
||||
import type { WelcomeNoticeInjected } from './WelcomeNotice.tsx'
|
||||
import { decodeWelcomeSection, WelcomeNoticeStore } from './welcome-store.ts'
|
||||
import { ModelsSettingsStore } from './store.ts'
|
||||
import type { ModelsWire } from './store.ts'
|
||||
import { createSettingsSchemaOperations } from './schema-operations.ts'
|
||||
import { en, zh, type ModelsKey } from './locales.ts'
|
||||
import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../onboarding-copy.ts'
|
||||
@@ -41,7 +42,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
|
||||
/** Dictionary namespace owned by this plugin. */
|
||||
const NS = 'settings.models'
|
||||
export type { ModelsSettingsState, ProviderRow } from './store.ts'
|
||||
export type { ModelsCredentials, ModelsSettingsState, ModelsWire, ProviderRow } from './store.ts'
|
||||
|
||||
/**
|
||||
* Refetch the page snapshot only after its first load: an unopened Models
|
||||
@@ -58,7 +59,10 @@ export function refreshIfLoaded(controller: ModelsSettingsStore): void {
|
||||
* ui-settings' apply, whose activation order relative to this one is NOT
|
||||
* constrained; registration depends on each slot through `slots.inject()`.
|
||||
*/
|
||||
export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope', 'settingsSchema']
|
||||
export const inject = [
|
||||
'slots', 'locale', 'connection', 'remote', 'remote.credentials', 'remote.settings',
|
||||
'settingsScope', 'settingsSchema',
|
||||
]
|
||||
|
||||
/**
|
||||
* Register the Models section once the `settings.section` declaration is on
|
||||
@@ -71,21 +75,29 @@ export function apply(ctx: ClientContext): void {
|
||||
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
const schema = createSettingsSchemaOperations(ctx.settingsSchema)
|
||||
const controller = new ModelsSettingsStore(connection.api, schema, ctx.settingsScope.describe())
|
||||
// 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.
|
||||
const wire: ModelsWire = {
|
||||
...connection.api,
|
||||
credentials: ctx.remote.credentials,
|
||||
settings: ctx.remote.settings,
|
||||
}
|
||||
const controller = new ModelsSettingsStore(wire, schema, ctx.settingsScope.describe())
|
||||
// Registration-time text (the nav label thunk) and the inject faces share
|
||||
// one bound translate; copy freshness rides the locale revision.
|
||||
const t = ctx.locale.bind(NS) as ModelsSectionInjected['t']
|
||||
const injected = (): ModelsSectionInjected => ({
|
||||
controller,
|
||||
hooks: { snapshot: controller.store },
|
||||
api: connection.api,
|
||||
api: wire,
|
||||
schema,
|
||||
t,
|
||||
})
|
||||
const deepSeekOnboardingInjected = (): DeepSeekOnboardingInjected => ({
|
||||
controller,
|
||||
hooks: { models: controller.store },
|
||||
api: connection.api,
|
||||
api: wire,
|
||||
schema,
|
||||
t,
|
||||
})
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/**
|
||||
* Models settings page store: one snapshot joining the configurable-provider
|
||||
* directory (`llm.providers`), the settings namespaces (shared settings mirror),
|
||||
* and the referenced credentials (`credentials.describe`). The host stays the
|
||||
* 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 {
|
||||
ConfigurableProviderView, CredentialView, IApiClient, SettingsNamespaceView,
|
||||
ClientRemote, ConfigurableProviderView, CredentialInfo, IApiClient, 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'
|
||||
import type { SettingsDescribeFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { SettingsDescribeFace, SettingsRemote } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { SettingsSchemaOperations } from './schema-operations.ts'
|
||||
|
||||
/**
|
||||
@@ -20,6 +20,21 @@ import type { SettingsSchemaOperations } from './schema-operations.ts'
|
||||
*/
|
||||
const PROBE_ROUTE = '\u0000probe'
|
||||
|
||||
/** The credentials Remote methods the Models page reads and writes through. */
|
||||
export type ModelsCredentials = Pick<ClientRemote['credentials'], 'describe' | 'set' | 'unset'>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export interface ModelsWire extends Pick<IApiClient, 'llm'> {
|
||||
/** 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
|
||||
}
|
||||
|
||||
/** One provider row the page renders. */
|
||||
export interface ProviderRow {
|
||||
/** The directory entry (route id, display name, settings address, live state). */
|
||||
@@ -31,14 +46,14 @@ export interface ProviderRow {
|
||||
/** The credential reference the resolved profile names, when one does. */
|
||||
apiKeyEnv: string | undefined
|
||||
/** Credential state for {@link apiKeyEnv}, once described. */
|
||||
credential: CredentialView | undefined
|
||||
credential: CredentialInfo | undefined
|
||||
/**
|
||||
* Credential state for the page's derived `<ROUTE>_API_KEY`, described only
|
||||
* while the profile names no reference — the provider-card seat's
|
||||
* `keyConfigured` fact for dormant and keyless rows, matching the editor's
|
||||
* own derivation rule.
|
||||
*/
|
||||
derivedCredential?: CredentialView
|
||||
derivedCredential?: CredentialInfo
|
||||
}
|
||||
|
||||
/** Page snapshot. */
|
||||
@@ -122,11 +137,11 @@ export class ModelsSettingsStore {
|
||||
private generation = 0
|
||||
|
||||
/**
|
||||
* @param api - the wire face (credentials/llm domains, and settings writes).
|
||||
* @param api - the page's wire faces (credentials Remote, llm reads, settings writes).
|
||||
* @param describeFace - the shared mirror's describe face (namespace views and writability).
|
||||
*/
|
||||
constructor(
|
||||
private readonly api: Pick<IApiClient, 'settings' | 'credentials' | 'llm'>,
|
||||
private readonly api: ModelsWire,
|
||||
private readonly schema: SettingsSchemaOperations,
|
||||
private readonly describeFace: SettingsDescribeFace,
|
||||
) {}
|
||||
@@ -193,16 +208,16 @@ export class ModelsSettingsStore {
|
||||
}
|
||||
})
|
||||
const refs = [...new Set(rows.map(row => row.apiKeyEnv ?? deriveKeyRef(row.entry.provider)))]
|
||||
let credentials: Record<string, CredentialView> = {}
|
||||
let credentials: Record<string, CredentialInfo> = {}
|
||||
let credentialError: string | null = null
|
||||
if (refs.length > 0) {
|
||||
try {
|
||||
const response = await this.api.credentials.describe({ refs })
|
||||
const response = await this.api.credentials.describe(refs)
|
||||
// Credential state is an enrichment for the Models page: neither a
|
||||
// business rejection nor a transport failure fails the load. The
|
||||
// onboarding projection below retains the failure distinction.
|
||||
if (response.result.ok) credentials = response.result.value.credentials
|
||||
else credentialError = response.result.error.message
|
||||
if (response.ok) credentials = response.value
|
||||
else credentialError = response.error.message
|
||||
} catch (error) {
|
||||
credentialError = messageOf(error)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { TestRemote, scriptedSettingsRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { apply as settingsApply, inject as settingsInject } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import { apply, inject, refreshIfLoaded } from '@deepseek-ai/dsh-client-ui-settings-models/client'
|
||||
import {
|
||||
@@ -24,14 +24,18 @@ async function bench(isLoopback = true, settings?: object, services: object = {}
|
||||
const locale = new LocaleRuntime(ctx)
|
||||
locale.setLocale('zh')
|
||||
ctx.provide('locale', locale)
|
||||
const remote = new TestRemote(ctx)
|
||||
// Without a settings face the mirror's reads fail and stay contained; the
|
||||
// Models join itself never fetches until a section actually loads. The real
|
||||
// ui-settings apply also provides the settingsSchema service.
|
||||
ctx.provide('connection', {
|
||||
api: settings === undefined ? services : { ...services, settings },
|
||||
isLoopback,
|
||||
} as never)
|
||||
const remote = new TestRemote(ctx, {
|
||||
credentials: {
|
||||
describe: vi.fn(() => Promise.resolve({ ok: true, value: {} })),
|
||||
set: vi.fn(),
|
||||
unset: vi.fn(),
|
||||
},
|
||||
// Without a settings face the mirror's reads fail and stay contained; the
|
||||
// Models join itself never fetches until a section actually loads. The real
|
||||
// ui-settings apply also provides the settingsSchema service.
|
||||
settings: settings ?? scriptedSettingsRemote().settings,
|
||||
})
|
||||
ctx.provide('connection', { api: services, isLoopback } as never)
|
||||
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
|
||||
return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, remote }
|
||||
}
|
||||
@@ -51,7 +55,10 @@ function declare(slots: SlotRegistry): () => void {
|
||||
|
||||
describe('ui-settings-models apply', () => {
|
||||
it('declares the services it uses', () => {
|
||||
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'settingsScope', 'settingsSchema'])
|
||||
expect(inject).toEqual([
|
||||
'slots', 'locale', 'connection', 'remote', 'remote.credentials', 'remote.settings',
|
||||
'settingsScope', 'settingsSchema',
|
||||
])
|
||||
})
|
||||
|
||||
it('registers the models nav entry for declarations before or after apply', async () => {
|
||||
@@ -243,21 +250,18 @@ describe('pushed invalidations', () => {
|
||||
const acknowledgement = { current: undefined as string | undefined }
|
||||
const settings = {
|
||||
describe: vi.fn(() => Promise.resolve({
|
||||
rpcId: 'apply-welcome' as never,
|
||||
result: {
|
||||
ok: true as const,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: false,
|
||||
namespaces: [{
|
||||
ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
|
||||
schema: {},
|
||||
value: acknowledgement.current === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: acknowledgement.current },
|
||||
applies: 'live' as const,
|
||||
secrets: [],
|
||||
revision: 0,
|
||||
}],
|
||||
},
|
||||
ok: true as const,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: false,
|
||||
namespaces: [{
|
||||
ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
|
||||
schema: {},
|
||||
value: acknowledgement.current === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: acknowledgement.current },
|
||||
applies: 'live' as const,
|
||||
secrets: [],
|
||||
revision: 0,
|
||||
}],
|
||||
},
|
||||
})),
|
||||
}
|
||||
@@ -284,21 +288,18 @@ describe('pushed invalidations', () => {
|
||||
it('joins the refreshed mirror view on a settings invalidation', async () => {
|
||||
let revision = 1
|
||||
const describe = vi.fn(() => Promise.resolve({
|
||||
rpcId: `apply-models-${revision}` as never,
|
||||
result: {
|
||||
ok: true as const,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: false,
|
||||
namespaces: [{
|
||||
ns: 'llm-test',
|
||||
schema: {},
|
||||
value: {},
|
||||
applies: 'live' as const,
|
||||
secrets: [],
|
||||
revision,
|
||||
}],
|
||||
},
|
||||
ok: true as const,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: false,
|
||||
namespaces: [{
|
||||
ns: 'llm-test',
|
||||
schema: {},
|
||||
value: {},
|
||||
applies: 'live' as const,
|
||||
secrets: [],
|
||||
revision,
|
||||
}],
|
||||
},
|
||||
}))
|
||||
const providers = vi.fn(() => Promise.resolve({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testi
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Schema from '@deepseek-ai/schemastery'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { JsonValue, RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import {
|
||||
ModelsSection, needsSetup, providerCopy, providerTargetLabel, removeProviderProfile,
|
||||
} from '../src/client/ModelsSection.tsx'
|
||||
@@ -90,7 +90,7 @@ function wireNamespaces(): SettingsNamespaceView[] {
|
||||
return [
|
||||
{
|
||||
ns: 'llm-deepseek',
|
||||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as unknown,
|
||||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
||||
value: {
|
||||
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
||||
baseURL: 'https://base',
|
||||
@@ -108,7 +108,7 @@ function wireNamespaces(): SettingsNamespaceView[] {
|
||||
ns: 'llm-plain',
|
||||
schema: JSON.parse(JSON.stringify(Schema.object({
|
||||
profiles: Schema.dict(Schema.object({ note: Schema.string() })),
|
||||
}).toJSON())) as unknown,
|
||||
}).toJSON())) as JsonValue,
|
||||
value: {},
|
||||
applies: 'live',
|
||||
secrets: [],
|
||||
@@ -116,7 +116,7 @@ function wireNamespaces(): SettingsNamespaceView[] {
|
||||
},
|
||||
{
|
||||
ns: 'llm-pi-ai',
|
||||
schema: JSON.parse(JSON.stringify(PiAiConfig.toJSON())) as unknown,
|
||||
schema: JSON.parse(JSON.stringify(PiAiConfig.toJSON())) as JsonValue,
|
||||
value: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy', headers: { 'X-Team': 'a' } }, zombie: {} } },
|
||||
user: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy', headers: { 'X-Team': 'a' } }, zombie: {} } },
|
||||
applies: 'live',
|
||||
@@ -125,7 +125,7 @@ function wireNamespaces(): SettingsNamespaceView[] {
|
||||
},
|
||||
{
|
||||
ns: 'subagent-model-selection',
|
||||
schema: JSON.parse(JSON.stringify(Schema.object({ enabled: Schema.boolean().default(false) }).toJSON())) as unknown,
|
||||
schema: JSON.parse(JSON.stringify(Schema.object({ enabled: Schema.boolean().default(false) }).toJSON())) as JsonValue,
|
||||
value: { enabled: false },
|
||||
applies: 'live',
|
||||
secrets: [],
|
||||
@@ -144,20 +144,25 @@ function fail<T>(message: string, code = 'settings-rejected'): RpcResponse<T> {
|
||||
result: { ok: false, error: { code, message, details: { ns: 'x' } } as never },
|
||||
}
|
||||
}
|
||||
/** Credentials answers over the Remote carrier, which has no envelope. */
|
||||
function remoteOk<T>(value: T) {
|
||||
return { ok: true as const, value }
|
||||
}
|
||||
function remoteFail(message: string, code = 'credential-rejected') {
|
||||
return { ok: false as const, error: { code, message, details: {} } }
|
||||
}
|
||||
|
||||
function scriptedFace(overrides: {
|
||||
update?: ReturnType<typeof vi.fn>
|
||||
replace?: ReturnType<typeof vi.fn>
|
||||
mutate?: ReturnType<typeof vi.fn>
|
||||
set?: ReturnType<typeof vi.fn>
|
||||
unset?: ReturnType<typeof vi.fn>
|
||||
} = {}) {
|
||||
const providerNamespace = wireNamespaces().find(view => view.ns === 'llm-pi-ai')!
|
||||
const update = overrides.update ?? vi.fn(() => Promise.resolve(ok(providerNamespace)))
|
||||
const replace = overrides.replace ?? vi.fn(() => Promise.resolve(ok(providerNamespace)))
|
||||
const mutate = overrides.mutate ?? vi.fn(() => Promise.resolve(ok(providerNamespace)))
|
||||
const set = overrides.set ?? vi.fn(() => Promise.resolve(ok({})))
|
||||
const unset = overrides.unset ?? vi.fn(() => Promise.resolve(ok({})))
|
||||
const update = overrides.update ?? vi.fn(() => Promise.resolve(remoteOk(providerNamespace)))
|
||||
const mutate = overrides.mutate ?? vi.fn(() => Promise.resolve(remoteOk(providerNamespace)))
|
||||
const set = overrides.set ?? vi.fn(() => Promise.resolve(remoteOk(undefined)))
|
||||
const unset = overrides.unset ?? vi.fn(() => Promise.resolve(remoteOk(undefined)))
|
||||
const face = {
|
||||
llm: {
|
||||
providers: vi.fn(() => Promise.resolve(ok({
|
||||
@@ -173,24 +178,23 @@ function scriptedFace(overrides: {
|
||||
models: vi.fn(() => Promise.resolve(ok({ groups: [], failures: [] }))),
|
||||
},
|
||||
settings: {
|
||||
describe: vi.fn(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: wireNamespaces() }))),
|
||||
describe: vi.fn(() => Promise.resolve(remoteOk({ writable: true, hasDocument: false, namespaces: wireNamespaces() }))),
|
||||
update,
|
||||
replace,
|
||||
mutate,
|
||||
},
|
||||
credentials: {
|
||||
describe: vi.fn((payload: { refs: string[] }) => Promise.resolve(ok({
|
||||
credentials: Object.fromEntries(payload.refs.map(ref => [ref, {
|
||||
describe: vi.fn((refs: string[]) => Promise.resolve(remoteOk(
|
||||
Object.fromEntries(refs.map(ref => [ref, {
|
||||
configured: ref === 'OPENAI_API_KEY',
|
||||
...ref === 'OPENAI_API_KEY' ? { source: 'file' } : {},
|
||||
writable: true,
|
||||
}])),
|
||||
}))),
|
||||
))),
|
||||
set,
|
||||
unset,
|
||||
},
|
||||
}
|
||||
return { face, update, replace, mutate, set, unset }
|
||||
return { face, update, mutate, set, unset }
|
||||
}
|
||||
|
||||
type WireFace = ConstructorParameters<typeof ModelsSettingsStore>[0]
|
||||
@@ -218,7 +222,7 @@ function cardSeatCalls(
|
||||
}
|
||||
|
||||
async function mountFace(scripted: ReturnType<typeof scriptedFace>) {
|
||||
const { face, update, replace, mutate, set, unset } = scripted
|
||||
const { face, update, mutate, set, unset } = scripted
|
||||
const mirror = new SettingsDescribeMirror(face as never)
|
||||
const controller = new ModelsSettingsStore(face as unknown as WireFace, settingsSchema, mirror)
|
||||
await controller.load()
|
||||
@@ -232,7 +236,7 @@ async function mountFace(scripted: ReturnType<typeof scriptedFace>) {
|
||||
renderSlot: renderSlot as unknown as ModelsSectionProps['renderSlot'],
|
||||
}
|
||||
const view = render(<ModelsSection {...injected} />)
|
||||
return { view, face, update, replace, mutate, set, unset, controller, mirror, renderSlot }
|
||||
return { view, face, update, mutate, set, unset, controller, mirror, renderSlot }
|
||||
}
|
||||
|
||||
async function mountSection(overrides: Parameters<typeof scriptedFace>[0] = {}) {
|
||||
@@ -245,10 +249,10 @@ async function mountSection(overrides: Parameters<typeof scriptedFace>[0] = {})
|
||||
*/
|
||||
async function mountFirstRun(overrides: Parameters<typeof scriptedFace>[0] = {}) {
|
||||
const scripted = scriptedFace(overrides)
|
||||
scripted.face.credentials.describe.mockImplementation((payload: { refs: string[] }) =>
|
||||
Promise.resolve(ok({
|
||||
credentials: Object.fromEntries(payload.refs.map(ref => [ref, { configured: false, writable: true }])),
|
||||
})))
|
||||
scripted.face.credentials.describe.mockImplementation((refs: string[]) =>
|
||||
Promise.resolve(remoteOk(
|
||||
Object.fromEntries(refs.map(ref => [ref, { configured: false, writable: true }])),
|
||||
)))
|
||||
return mountFace(scripted)
|
||||
}
|
||||
|
||||
@@ -295,12 +299,12 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('derives the draft seat\'s key fact from the page\'s conventional reference', async () => {
|
||||
const scripted = scriptedFace()
|
||||
scripted.face.credentials.describe.mockImplementation((payload: { refs: string[] }) => Promise.resolve(ok({
|
||||
credentials: Object.fromEntries(payload.refs.map(ref => [ref, {
|
||||
scripted.face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
||||
Object.fromEntries(refs.map(ref => [ref, {
|
||||
configured: ref === 'OPENAI_API_KEY' || ref === 'ANTHROPIC_API_KEY',
|
||||
writable: true,
|
||||
}])),
|
||||
})))
|
||||
)))
|
||||
const { renderSlot } = await mountFace(scripted)
|
||||
renderSlot.mockClear()
|
||||
fireEvent.click(screen.getByRole('button', { name: en.add }))
|
||||
@@ -332,7 +336,7 @@ describe('ModelsSection', () => {
|
||||
user: { enabled: true },
|
||||
revision: 5,
|
||||
}
|
||||
const update = vi.fn(() => Promise.resolve(ok(enabledNamespace)))
|
||||
const update = vi.fn(() => Promise.resolve(remoteOk(enabledNamespace)))
|
||||
await mountSection({ update })
|
||||
|
||||
const toggle = screen.getByRole('switch', { name: en.subagentModelSelectionToggle })
|
||||
@@ -340,18 +344,18 @@ describe('ModelsSection', () => {
|
||||
fireEvent.click(toggle)
|
||||
|
||||
await waitFor(() => { expect(toggle.getAttribute('aria-checked')).toBe('true') })
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
ns: 'subagent-model-selection',
|
||||
patch: { enabled: true },
|
||||
expectedRevision: 4,
|
||||
})
|
||||
expect(update).toHaveBeenCalledWith(
|
||||
'subagent-model-selection',
|
||||
{ enabled: true },
|
||||
4,
|
||||
)
|
||||
expect(screen.getByRole('status').textContent).toBe(en.subagentModelSelectionSaved)
|
||||
})
|
||||
|
||||
it('reports rejected subagent model-selection updates and permits a retry', async () => {
|
||||
const update = vi.fn()
|
||||
.mockResolvedValueOnce(fail<SettingsNamespaceView>('revision changed'))
|
||||
.mockResolvedValueOnce(ok({
|
||||
.mockResolvedValueOnce(remoteFail('revision changed', 'settings-rejected'))
|
||||
.mockResolvedValueOnce(remoteOk({
|
||||
...wireNamespaces().find(view => view.ns === 'subagent-model-selection')!,
|
||||
value: { enabled: true },
|
||||
revision: 5,
|
||||
@@ -372,12 +376,12 @@ describe('ModelsSection', () => {
|
||||
...wireNamespaces().find(view => view.ns === 'subagent-model-selection')!,
|
||||
value: null,
|
||||
} as unknown as SettingsNamespaceView
|
||||
const update = vi.fn()
|
||||
const mutate = vi.fn()
|
||||
render(
|
||||
<SubagentModelSelectionCard
|
||||
namespace={namespace}
|
||||
writable={false}
|
||||
api={{ settings: { update } } as never}
|
||||
api={{ settings: { mutate } } as never}
|
||||
controller={{ acceptNamespace: vi.fn(), load: vi.fn() } as never}
|
||||
t={t}
|
||||
/>,
|
||||
@@ -387,7 +391,7 @@ describe('ModelsSection', () => {
|
||||
expect(toggle.getAttribute('aria-checked')).toBe('false')
|
||||
expect((toggle as HTMLButtonElement).disabled).toBe(true)
|
||||
fireEvent.click(toggle)
|
||||
expect(update).not.toHaveBeenCalled()
|
||||
expect(mutate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders the unkeyed whole-section provider as an open setup card in the first-run posture', async () => {
|
||||
@@ -420,9 +424,9 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('marks only a confirmed missing reference and leaves native or unavailable state unmarked', async () => {
|
||||
const { face } = scriptedFace()
|
||||
face.credentials.describe.mockImplementation((payload: { refs: string[] }) => Promise.resolve(ok({
|
||||
credentials: Object.fromEntries(payload.refs.map(ref => [ref, { configured: false, writable: true }])),
|
||||
})))
|
||||
face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
||||
Object.fromEntries(refs.map(ref => [ref, { configured: false, writable: true }])),
|
||||
)))
|
||||
const controller = new ModelsSettingsStore(face as unknown as WireFace, settingsSchema, new SettingsDescribeMirror(face as never))
|
||||
await controller.load()
|
||||
render(<ModelsSection
|
||||
@@ -444,9 +448,9 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('turns the setup card into a row once the credential reports configured', async () => {
|
||||
const { face } = await mountFirstRun()
|
||||
face.credentials.describe.mockImplementation((payload: { refs: string[] }) => Promise.resolve(ok({
|
||||
credentials: Object.fromEntries(payload.refs.map(ref => [ref, { configured: true, writable: true }])),
|
||||
})))
|
||||
face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
||||
Object.fromEntries(refs.map(ref => [ref, { configured: true, writable: true }])),
|
||||
)))
|
||||
const controller = new ModelsSettingsStore(face as unknown as WireFace, settingsSchema, new SettingsDescribeMirror(face as never))
|
||||
await controller.load()
|
||||
cleanup()
|
||||
@@ -503,12 +507,12 @@ describe('ModelsSection', () => {
|
||||
})
|
||||
|
||||
it('stores a typed key write-only from the setup card without touching settings', async () => {
|
||||
const { set, update, face } = await mountFirstRun()
|
||||
const { set, mutate, face } = await mountFirstRun()
|
||||
const key = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||||
fireEvent.change(key, { target: { value: ' sk-live ' } })
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
await waitFor(() => { expect(set).toHaveBeenCalledWith({ ref: 'DEEPSEEK_API_KEY', value: 'sk-live' }) })
|
||||
expect(update).not.toHaveBeenCalled()
|
||||
await waitFor(() => { expect(set).toHaveBeenCalledWith('DEEPSEEK_API_KEY', 'sk-live') })
|
||||
expect(mutate).not.toHaveBeenCalled()
|
||||
// The saved key re-loads the join; the settings answer rides the shared
|
||||
// mirror, so the reload shows as a directory read rather than a describe.
|
||||
await waitFor(() => { expect(face.llm.providers.mock.calls.length).toBeGreaterThan(1) })
|
||||
@@ -520,8 +524,8 @@ describe('ModelsSection', () => {
|
||||
})
|
||||
|
||||
it('reuses the provider editor as a required credential-only onboarding form', async () => {
|
||||
let finishSet: ((response: RpcResponse<Record<string, never>>) => void) | undefined
|
||||
const set = vi.fn(() => new Promise<RpcResponse<Record<string, never>>>((resolve) => {
|
||||
let finishSet: ((response: { ok: true; value: undefined }) => void) | undefined
|
||||
const set = vi.fn(() => new Promise<{ ok: true; value: undefined }>((resolve) => {
|
||||
finishSet = resolve
|
||||
}))
|
||||
const { face, mutate } = scriptedFace({ set })
|
||||
@@ -567,13 +571,13 @@ describe('ModelsSection', () => {
|
||||
fireEvent.click(save)
|
||||
|
||||
expect(await screen.findByText(en.onboardingSaving)).toBeTruthy()
|
||||
expect(set).toHaveBeenCalledWith({ ref: 'DEEPSEEK_API_KEY', value: 'sk-onboarding' })
|
||||
expect(set).toHaveBeenCalledWith('DEEPSEEK_API_KEY', 'sk-onboarding')
|
||||
expect(mutate).not.toHaveBeenCalled()
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
|
||||
if (finishSet === undefined) throw new Error('credential write did not start')
|
||||
await act(async () => {
|
||||
finishSet?.(ok({}))
|
||||
finishSet?.(remoteOk(undefined))
|
||||
await Promise.resolve()
|
||||
})
|
||||
expect(onClose).toHaveBeenCalledWith(true)
|
||||
@@ -581,7 +585,7 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('applies customized deepseek fields as path ops', async () => {
|
||||
const { mutate } = await mountDeepSeekCard({
|
||||
mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
||||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||||
})
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
const baseURL = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||||
@@ -593,16 +597,16 @@ describe('ModelsSection', () => {
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||||
// Only the field that actually changed: reasoningEffort was already
|
||||
// 'high' in the loaded profile, so it produces no op.
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-deepseek',
|
||||
ops: [{ op: 'set', path: ['baseURL'], value: 'https://next2' }],
|
||||
expectedRevision: 0,
|
||||
})
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-deepseek',
|
||||
[{ op: 'set', path: ['baseURL'], value: 'https://next2' }],
|
||||
0,
|
||||
])
|
||||
})
|
||||
|
||||
it('materializes inherited models and adds an arbitrary DeepSeek id', async () => {
|
||||
const { mutate } = await mountDeepSeekCard({
|
||||
mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
||||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||||
})
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
expect(screen.getByText(en.modelsInherited)).toBeTruthy()
|
||||
@@ -620,9 +624,9 @@ describe('ModelsSection', () => {
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-deepseek',
|
||||
ops: [{
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-deepseek',
|
||||
[{
|
||||
op: 'set',
|
||||
path: ['models'],
|
||||
value: [
|
||||
@@ -630,8 +634,8 @@ describe('ModelsSection', () => {
|
||||
{ id: 'private-preview', name: 'Private Preview', contextWindow: 131_072 },
|
||||
],
|
||||
}],
|
||||
expectedRevision: 0,
|
||||
})
|
||||
0,
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects duplicate DeepSeek model ids before writing', async () => {
|
||||
@@ -706,7 +710,7 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('accepts a suffixed context window and stores the plain count', async () => {
|
||||
const { mutate } = await mountDeepSeekCard({
|
||||
mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
||||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||||
})
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
expandRow(1)
|
||||
@@ -730,9 +734,9 @@ describe('ModelsSection', () => {
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-deepseek',
|
||||
ops: [{
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-deepseek',
|
||||
[{
|
||||
op: 'set',
|
||||
path: ['models'],
|
||||
value: [
|
||||
@@ -740,8 +744,8 @@ describe('ModelsSection', () => {
|
||||
{ ...DEFAULT_DEEPSEEK_MODELS[1], contextWindow: 256_000 },
|
||||
],
|
||||
}],
|
||||
expectedRevision: 0,
|
||||
})
|
||||
0,
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps unreadable context-window text on screen and refuses the write', async () => {
|
||||
@@ -773,7 +777,7 @@ describe('ModelsSection', () => {
|
||||
const stored = { models: [{ id: 'user-only-model', name: 'User Only' }] }
|
||||
const overridden: SettingsNamespaceView = {
|
||||
ns: 'llm-deepseek',
|
||||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as unknown,
|
||||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
||||
value: { ...stored, defaultContextWindow: 1_000_000 },
|
||||
...base === undefined ? {} : { base },
|
||||
user: stored,
|
||||
@@ -858,7 +862,7 @@ describe('ModelsSection', () => {
|
||||
// inherited row displayed text no settings layer stores — and because an
|
||||
// unreadable buffer never settles, it stayed there indefinitely.
|
||||
const { mutate } = await mountDeepSeekCard({
|
||||
mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
||||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||||
})
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
expandRow(1)
|
||||
@@ -881,7 +885,7 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('edits an output cap per model and carries its text across a removal', async () => {
|
||||
const { mutate } = await mountDeepSeekCard({
|
||||
mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
||||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||||
})
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
expandRow(1)
|
||||
@@ -902,15 +906,15 @@ describe('ModelsSection', () => {
|
||||
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-deepseek',
|
||||
ops: [{
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-deepseek',
|
||||
[{
|
||||
op: 'set',
|
||||
path: ['models'],
|
||||
value: [{ ...DEFAULT_DEEPSEEK_MODELS[1], maxTokens: 64_000 }],
|
||||
}],
|
||||
expectedRevision: 0,
|
||||
})
|
||||
0,
|
||||
])
|
||||
})
|
||||
|
||||
it('settles a pasted id and refuses whitespace that would never match', async () => {
|
||||
@@ -952,7 +956,7 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('can empty and reset the model override, then clear optional fields without dropping hidden data', async () => {
|
||||
const { mutate } = await mountDeepSeekCard({
|
||||
mutate: vi.fn(() => Promise.resolve(ok(wireNamespaces()[0]))),
|
||||
mutate: vi.fn(() => Promise.resolve(remoteOk(wireNamespaces()[0]))),
|
||||
})
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
fireEvent.click(screen.getAllByLabelText(new RegExp(en.removeModel))[0] as HTMLElement)
|
||||
@@ -969,9 +973,9 @@ describe('ModelsSection', () => {
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-deepseek',
|
||||
ops: [{
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-deepseek',
|
||||
[{
|
||||
op: 'set',
|
||||
path: ['models'],
|
||||
value: [
|
||||
@@ -979,33 +983,33 @@ describe('ModelsSection', () => {
|
||||
DEFAULT_DEEPSEEK_MODELS[1],
|
||||
],
|
||||
}],
|
||||
expectedRevision: 0,
|
||||
})
|
||||
0,
|
||||
])
|
||||
})
|
||||
|
||||
it('clears an inherited override with an unset op, never a whole-section replace', async () => {
|
||||
// A whole-section replace would clobber sibling overrides to clear one field.
|
||||
const { replace, update, mutate } = await mountDeepSeekCard()
|
||||
const { mutate } = await mountDeepSeekCard()
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
const url = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||||
expect(url.value).toBe('https://base')
|
||||
fireEvent.change(url, { target: { value: '' } })
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||||
expect(replace).not.toHaveBeenCalled()
|
||||
expect(update).not.toHaveBeenCalled()
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-deepseek',
|
||||
ops: [{ op: 'unset', path: ['baseURL'] }],
|
||||
expectedRevision: 0,
|
||||
})
|
||||
// This editor clears one field through an unset op so it cannot clobber
|
||||
// sibling overrides with a whole-section replacement.
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-deepseek',
|
||||
[{ op: 'unset', path: ['baseURL'] }],
|
||||
0,
|
||||
])
|
||||
})
|
||||
|
||||
it('pins the deepseek placeholder and clears typed input back to inherited', async () => {
|
||||
const { face } = scriptedFace()
|
||||
const bare: SettingsNamespaceView = {
|
||||
ns: 'llm-deepseek',
|
||||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as unknown,
|
||||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
||||
value: {},
|
||||
applies: 'live',
|
||||
secrets: [],
|
||||
@@ -1033,12 +1037,12 @@ describe('ModelsSection', () => {
|
||||
})
|
||||
|
||||
it('rejects an invalid draft before writing', async () => {
|
||||
const { update } = await mountDeepSeekCard()
|
||||
const { mutate } = await mountDeepSeekCard()
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'not-a-url' } })
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
await screen.findByText(/baseURL/)
|
||||
expect(update).not.toHaveBeenCalled()
|
||||
expect(mutate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('edits a pi-ai profile with the curated fields only', async () => {
|
||||
@@ -1057,11 +1061,11 @@ describe('ModelsSection', () => {
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||||
// Only the edited field travels: apiKeyEnv and headers were already stored
|
||||
// with these values, so no op restates them.
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-pi-ai',
|
||||
ops: [{ op: 'set', path: ['providers', 'openai', 'baseURL'], value: 'https://proxy/v2' }],
|
||||
expectedRevision: 0,
|
||||
})
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-pi-ai',
|
||||
[{ op: 'set', path: ['providers', 'openai', 'baseURL'], value: 'https://proxy/v2' }],
|
||||
0,
|
||||
])
|
||||
})
|
||||
|
||||
it('adds a dormant provider with a derived reference and stores its key', async () => {
|
||||
@@ -1079,12 +1083,12 @@ describe('ModelsSection', () => {
|
||||
fireEvent.change(addKey, { target: { value: 'sk-ant' } })
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-pi-ai',
|
||||
ops: [{ op: 'set', path: ['providers', 'anthropic', 'apiKeyEnv'], value: 'ANTHROPIC_API_KEY' }],
|
||||
expectedRevision: 0,
|
||||
})
|
||||
await waitFor(() => { expect(set).toHaveBeenCalledWith({ ref: 'ANTHROPIC_API_KEY', value: 'sk-ant' }) })
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-pi-ai',
|
||||
[{ op: 'set', path: ['providers', 'anthropic', 'apiKeyEnv'], value: 'ANTHROPIC_API_KEY' }],
|
||||
0,
|
||||
])
|
||||
await waitFor(() => { expect(set).toHaveBeenCalledWith('ANTHROPIC_API_KEY', 'sk-ant') })
|
||||
})
|
||||
|
||||
it('keeps pi-ai provider-native authentication when no key is entered', async () => {
|
||||
@@ -1093,11 +1097,11 @@ describe('ModelsSection', () => {
|
||||
await screen.findByLabelText(en.provider)
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-pi-ai',
|
||||
ops: [{ op: 'set', path: ['providers', 'anthropic'], value: {} }],
|
||||
expectedRevision: 0,
|
||||
})
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-pi-ai',
|
||||
[{ op: 'set', path: ['providers', 'anthropic'], value: {} }],
|
||||
0,
|
||||
])
|
||||
expect(set).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -1115,10 +1119,10 @@ describe('ModelsSection', () => {
|
||||
} },
|
||||
revision: 1,
|
||||
}
|
||||
const mutate = vi.fn(() => Promise.resolve(ok(afterSettings)))
|
||||
const mutate = vi.fn(() => Promise.resolve(remoteOk(afterSettings)))
|
||||
const set = vi.fn()
|
||||
.mockResolvedValueOnce(fail('credential store unavailable', 'credential-rejected'))
|
||||
.mockResolvedValueOnce(ok({}))
|
||||
.mockResolvedValueOnce(remoteFail('credential store unavailable'))
|
||||
.mockResolvedValueOnce(remoteOk(undefined))
|
||||
const { face, controller, mirror } = await mountSection({ mutate, set })
|
||||
fireEvent.click(screen.getByText(en.add))
|
||||
await screen.findByLabelText(en.provider)
|
||||
@@ -1126,7 +1130,7 @@ describe('ModelsSection', () => {
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
await screen.findByText('credential store unavailable')
|
||||
expect(mutate).toHaveBeenCalledOnce()
|
||||
face.settings.describe.mockResolvedValue(ok({
|
||||
face.settings.describe.mockResolvedValue(remoteOk({
|
||||
writable: true,
|
||||
hasDocument: false,
|
||||
namespaces: wireNamespaces().map(namespace => namespace.ns === 'llm-pi-ai' ? afterSettings : namespace),
|
||||
@@ -1141,7 +1145,7 @@ describe('ModelsSection', () => {
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
await waitFor(() => { expect(set).toHaveBeenCalledTimes(2) })
|
||||
expect(mutate).toHaveBeenCalledOnce()
|
||||
expect(set).toHaveBeenLastCalledWith({ ref: 'ANTHROPIC_API_KEY', value: 'sk-ant' })
|
||||
expect(set).toHaveBeenLastCalledWith('ANTHROPIC_API_KEY', 'sk-ant')
|
||||
})
|
||||
|
||||
it('switches the add card target and degrades unknown or broken targets loudly', async () => {
|
||||
@@ -1161,7 +1165,7 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('surfaces a rejected settings write and never stores the key after it', async () => {
|
||||
const { set } = await mountSection({
|
||||
mutate: vi.fn(() => Promise.resolve(fail('llm-pi-ai: unknown pi-ai provider "bogus"'))),
|
||||
mutate: vi.fn(() => Promise.resolve(remoteFail('llm-pi-ai: unknown pi-ai provider "bogus"', 'settings-rejected'))),
|
||||
})
|
||||
fireEvent.click(screen.getByText(en.add))
|
||||
await screen.findByLabelText(en.provider)
|
||||
@@ -1202,7 +1206,7 @@ describe('ModelsSection', () => {
|
||||
// The stale-draft overwrite: two tabs open the same card, the other saves,
|
||||
// and this one must be refused rather than replay its opening snapshot.
|
||||
const { set } = await mountDeepSeekCard({
|
||||
mutate: vi.fn(() => Promise.resolve(fail('changed since it was read', 'settings-conflict'))),
|
||||
mutate: vi.fn(() => Promise.resolve(remoteFail('changed since it was read', 'settings-conflict'))),
|
||||
})
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
fireEvent.change(screen.getByLabelText<HTMLInputElement>(en.baseUrl), { target: { value: 'https://mine' } })
|
||||
@@ -1226,7 +1230,7 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('surfaces a shadowed credential write on the card', async () => {
|
||||
await mountFirstRun({
|
||||
set: vi.fn(() => Promise.resolve(fail('credentials: DEEPSEEK_API_KEY is shadowed by the read-only environment', 'credential-rejected'))),
|
||||
set: vi.fn(() => Promise.resolve(remoteFail('credentials: DEEPSEEK_API_KEY is shadowed by the read-only environment'))),
|
||||
})
|
||||
const key = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||||
fireEvent.change(key, { target: { value: 'sk-live' } })
|
||||
@@ -1237,11 +1241,11 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('locks the key input when the launch environment provides the credential', async () => {
|
||||
const { face } = await mountSection()
|
||||
face.credentials.describe.mockImplementation((payload: { refs: string[] }) => Promise.resolve(ok({
|
||||
credentials: Object.fromEntries(payload.refs.map(ref => [ref, {
|
||||
face.credentials.describe.mockImplementation((refs: string[]) => Promise.resolve(remoteOk(
|
||||
Object.fromEntries(refs.map(ref => [ref, {
|
||||
configured: ref === 'OPENAI_API_KEY', source: 'env', writable: false,
|
||||
}])),
|
||||
})))
|
||||
)))
|
||||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.editProvider) }))
|
||||
const editorKey = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
||||
await waitFor(() => { expect(editorKey.placeholder).toBe(en.keyEnvLocked) })
|
||||
@@ -1250,7 +1254,7 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('keeps a failed credential describe silent and the input usable', async () => {
|
||||
const { face, set } = await mountSection()
|
||||
face.credentials.describe.mockImplementation(() => Promise.resolve(fail('down', 'internal')) as never)
|
||||
face.credentials.describe.mockImplementation(() => Promise.resolve(remoteFail('down', 'internal')) as never)
|
||||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.editProvider) }))
|
||||
const editorKey = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
||||
expect(editorKey.placeholder).toBe(en.keyPlaceholderNative)
|
||||
@@ -1260,7 +1264,7 @@ describe('ModelsSection', () => {
|
||||
})
|
||||
|
||||
it('requires confirmation before removing a user-added provider', async () => {
|
||||
const { replace, mutate, unset } = await mountSection()
|
||||
const { mutate, unset } = await mountSection()
|
||||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
||||
const dialog = screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })
|
||||
expect(dialog.textContent).toContain(openaiCopy(en.deleteDescriptionWithCredential))
|
||||
@@ -1280,20 +1284,20 @@ describe('ModelsSection', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
||||
fireEvent.click(within(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) }))
|
||||
.getByRole('button', { name: openaiCopy(en.deleteConfirm) }))
|
||||
await waitFor(() => { expect(unset).toHaveBeenCalledWith({ ref: 'OPENAI_API_KEY' }) })
|
||||
await waitFor(() => { expect(unset).toHaveBeenCalledWith('OPENAI_API_KEY') })
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
|
||||
expect(unset.mock.invocationCallOrder[0]).toBeLessThan(mutate.mock.invocationCallOrder[0] as number)
|
||||
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
||||
expect(replace).not.toHaveBeenCalled()
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-pi-ai',
|
||||
ops: [{ op: 'unset', path: ['providers', 'openai'] }],
|
||||
})
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-pi-ai',
|
||||
[{ op: 'unset', path: ['providers', 'openai'] }],
|
||||
undefined,
|
||||
])
|
||||
})
|
||||
|
||||
it('blocks duplicate deletion while the confirmed removal is pending', async () => {
|
||||
let resolveRemoval!: (response: RpcResponse<SettingsNamespaceView>) => void
|
||||
const mutate = vi.fn(() => new Promise<RpcResponse<SettingsNamespaceView>>((resolve) => {
|
||||
let resolveRemoval!: (response: { ok: true; value: SettingsNamespaceView }) => void
|
||||
const mutate = vi.fn(() => new Promise<{ ok: true; value: SettingsNamespaceView }>((resolve) => {
|
||||
resolveRemoval = resolve
|
||||
}))
|
||||
await mountSection({ mutate })
|
||||
@@ -1309,7 +1313,7 @@ describe('ModelsSection', () => {
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: en.close }))
|
||||
expect(screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBe(dialog)
|
||||
expect(mutate).toHaveBeenCalledOnce()
|
||||
await act(async () => { resolveRemoval(ok(wireNamespaces()[2]!)) })
|
||||
await act(async () => { resolveRemoval(remoteOk(wireNamespaces()[2]!)) })
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: openaiCopy(en.deleteTitle) })).toBeNull()
|
||||
})
|
||||
@@ -1336,7 +1340,7 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('shows the read-only notice and disables mutations for a read-only provider', async () => {
|
||||
const { face } = await mountSection()
|
||||
face.settings.describe.mockImplementation(() => Promise.resolve(ok({
|
||||
face.settings.describe.mockImplementation(() => Promise.resolve(remoteOk({
|
||||
writable: false,
|
||||
hasDocument: false,
|
||||
namespaces: wireNamespaces(),
|
||||
@@ -1358,7 +1362,7 @@ describe('ModelsSection', () => {
|
||||
})
|
||||
|
||||
it('toggles the row editor closed on a second edit click and on cancel', async () => {
|
||||
const { update } = await mountSection()
|
||||
const { mutate } = await mountSection()
|
||||
const edit = screen.getByRole('button', { name: openaiCopy(en.editProvider) })
|
||||
fireEvent.click(edit)
|
||||
await waitFor(() => { expect(screen.queryAllByLabelText(en.keyInput).length).toBe(1) })
|
||||
@@ -1368,7 +1372,7 @@ describe('ModelsSection', () => {
|
||||
await waitFor(() => { expect(screen.queryAllByLabelText(en.keyInput).length).toBe(1) })
|
||||
fireEvent.click(screen.getByText(en.cancel))
|
||||
expect(screen.queryAllByLabelText(en.keyInput)).toHaveLength(0)
|
||||
expect(update).not.toHaveBeenCalled()
|
||||
expect(mutate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cancels the add card back to the add button', async () => {
|
||||
@@ -1420,22 +1424,22 @@ describe('ModelsSection', () => {
|
||||
it('removes by unsetting the profile path, never by rebuilding the section', async () => {
|
||||
// The page only needs to name the profile path; rebuilding the section
|
||||
// would widen the write for no benefit.
|
||||
const { face, mutate, replace, controller } = await mountSection()
|
||||
const { face, mutate, controller } = await mountSection()
|
||||
await removeProviderProfile(
|
||||
face as unknown as Parameters<typeof removeProviderProfile>[0],
|
||||
controller,
|
||||
{ settingsNs: 'llm-plain', settingsPath: ['ghost-profile'] },
|
||||
)
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-plain',
|
||||
ops: [{ op: 'unset', path: ['ghost-profile'] }],
|
||||
})
|
||||
expect(replace).not.toHaveBeenCalled()
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-plain',
|
||||
[{ op: 'unset', path: ['ghost-profile'] }],
|
||||
undefined,
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps the snapshot untouched and reports the message when a removal write is refused', async () => {
|
||||
const { face, controller } = await mountSection({
|
||||
mutate: vi.fn(() => Promise.resolve(fail('read-only'))),
|
||||
mutate: vi.fn(() => Promise.resolve(remoteFail('read-only', 'settings-rejected'))),
|
||||
})
|
||||
const before = controller.store.getSnapshot().rows
|
||||
const failure = await removeProviderProfile(
|
||||
@@ -1449,8 +1453,8 @@ describe('ModelsSection', () => {
|
||||
|
||||
it('keeps a failed identified deletion recoverable in its confirmation dialog', async () => {
|
||||
const mutate = vi.fn()
|
||||
.mockResolvedValueOnce(fail('the host refused'))
|
||||
.mockResolvedValueOnce(ok(wireNamespaces()[2]!))
|
||||
.mockResolvedValueOnce(remoteFail('the host refused', 'settings-rejected'))
|
||||
.mockResolvedValueOnce(remoteOk(wireNamespaces()[2]!))
|
||||
const { unset } = await mountSection({ mutate })
|
||||
fireEvent.click(screen.getByRole('button', { name: openaiCopy(en.removeProvider) }))
|
||||
const dialog = screen.getByRole('dialog', { name: openaiCopy(en.deleteTitle) })
|
||||
@@ -1478,15 +1482,16 @@ describe('ModelsSection', () => {
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: providerCopy(en.deleteConfirm, target) }))
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
|
||||
expect(unset).not.toHaveBeenCalled()
|
||||
expect(mutate.mock.calls[0]?.[0]).toEqual({
|
||||
ns: 'llm-pi-ai',
|
||||
ops: [{ op: 'unset', path: ['providers', 'zombie'] }],
|
||||
})
|
||||
expect(mutate.mock.calls[0]).toEqual([
|
||||
'llm-pi-ai',
|
||||
[{ op: 'unset', path: ['providers', 'zombie'] }],
|
||||
undefined,
|
||||
])
|
||||
})
|
||||
|
||||
it('does not remove provider settings when its managed credential removal is refused', async () => {
|
||||
const { face, controller, mutate } = await mountSection({
|
||||
unset: vi.fn(() => Promise.resolve(fail('credential is read-only', 'credential-rejected'))),
|
||||
unset: vi.fn(() => Promise.resolve(remoteFail('credential is read-only'))),
|
||||
})
|
||||
const failure = await removeProviderProfile(
|
||||
face as unknown as Parameters<typeof removeProviderProfile>[0],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Schema from '@deepseek-ai/schemastery'
|
||||
import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { JsonValue, RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { DeepSeekOnboardingDialog } from '../src/client/DeepSeekOnboardingDialog.tsx'
|
||||
import type { DeepSeekOnboardingDialogProps } from '../src/client/DeepSeekOnboardingDialog.tsx'
|
||||
@@ -21,11 +21,12 @@ let nextRpc = 0
|
||||
function ok<T>(value: T): RpcResponse<T> {
|
||||
return { rpcId: `onboarding-${nextRpc++}` as never, result: { ok: true, value } }
|
||||
}
|
||||
function fail<T>(message: string): RpcResponse<T> {
|
||||
return {
|
||||
rpcId: `onboarding-${nextRpc++}` as never,
|
||||
result: { ok: false, error: { code: 'internal', message, details: {} } },
|
||||
}
|
||||
/** Credentials answers over the Remote carrier, which has no envelope. */
|
||||
function remoteOk<T>(value: T) {
|
||||
return { ok: true as const, value }
|
||||
}
|
||||
function remoteFail(message: string) {
|
||||
return { ok: false as const, error: { code: 'internal', message, details: {} } }
|
||||
}
|
||||
|
||||
const DeepSeekConfig = Schema.object({
|
||||
@@ -49,7 +50,7 @@ function deepSeekNamespace(apiKeyEnv: string | null): SettingsNamespaceView {
|
||||
const value = apiKeyEnv === null ? {} : { apiKeyEnv }
|
||||
return {
|
||||
ns: 'llm-deepseek',
|
||||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as unknown,
|
||||
schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
|
||||
value,
|
||||
base: value,
|
||||
user: {},
|
||||
@@ -81,12 +82,12 @@ function harness(options: {
|
||||
let fileConfigured = false
|
||||
const configured = options.configured ?? (() => fileConfigured)
|
||||
const apiKeyEnv = options.apiKeyEnv === undefined ? 'DEEPSEEK_API_KEY' : options.apiKeyEnv
|
||||
const mutate = vi.fn(() => Promise.resolve(ok(deepSeekNamespace(apiKeyEnv))))
|
||||
const set = vi.fn((_payload: { ref: string; value: string }) => {
|
||||
const mutate = vi.fn(() => Promise.resolve(remoteOk(deepSeekNamespace(apiKeyEnv))))
|
||||
const set = vi.fn((_ref: string, _value: string) => {
|
||||
if (options.setReject !== undefined) return Promise.reject(new Error(options.setReject))
|
||||
if (options.setFailure !== undefined) return Promise.resolve(fail(options.setFailure))
|
||||
if (options.setFailure !== undefined) return Promise.resolve(remoteFail(options.setFailure))
|
||||
fileConfigured = true
|
||||
return Promise.resolve(ok({}))
|
||||
return Promise.resolve(remoteOk(undefined))
|
||||
})
|
||||
const face = {
|
||||
llm: {
|
||||
@@ -106,7 +107,7 @@ function harness(options: {
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
describe: () => Promise.resolve(ok({
|
||||
describe: () => Promise.resolve(remoteOk({
|
||||
writable: options.settingsWritable ?? true,
|
||||
hasDocument: false,
|
||||
namespaces: options.settingsNamespace === false ? [] : [deepSeekNamespace(apiKeyEnv)],
|
||||
@@ -115,18 +116,16 @@ function harness(options: {
|
||||
},
|
||||
credentials: {
|
||||
describe: () => options.describeFailure === undefined
|
||||
? Promise.resolve(ok({
|
||||
credentials: {
|
||||
DEEPSEEK_API_KEY: {
|
||||
configured: configured(),
|
||||
...configured() && options.credential?.source !== undefined
|
||||
? { source: options.credential.source }
|
||||
: {},
|
||||
writable: options.credential?.writable ?? true,
|
||||
},
|
||||
? Promise.resolve(remoteOk({
|
||||
DEEPSEEK_API_KEY: {
|
||||
configured: configured(),
|
||||
...configured() && options.credential?.source !== undefined
|
||||
? { source: options.credential.source }
|
||||
: {},
|
||||
writable: options.credential?.writable ?? true,
|
||||
},
|
||||
}))
|
||||
: Promise.resolve(fail(options.describeFailure)),
|
||||
: Promise.resolve(remoteFail(options.describeFailure)),
|
||||
set,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Schema from '@deepseek-ai/schemastery'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { JsonValue, RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { ModelsSection, providerCopy } from '../src/client/ModelsSection.tsx'
|
||||
import type { ModelsSectionInjected, ModelsSectionProps } from '../src/client/ModelsSection.tsx'
|
||||
import { CustomProviderCard } from '../src/client/CustomProviderCard.tsx'
|
||||
@@ -45,15 +45,22 @@ function ok<T>(value: T): RpcResponse<T> {
|
||||
function fail<T>(message: string, code: string): RpcResponse<T> {
|
||||
return { rpcId: `r-${nextRpc++}` as never, result: { ok: false, error: { code, message, details: {} } as never } }
|
||||
}
|
||||
/** Credentials answers over the Remote carrier, which has no envelope. */
|
||||
function remoteOk<T>(value: T) {
|
||||
return { ok: true as const, value }
|
||||
}
|
||||
function remoteFail(message: string, code = 'credential-rejected') {
|
||||
return { ok: false as const, error: { code, message, details: {} } }
|
||||
}
|
||||
|
||||
function piAiNamespace(
|
||||
providers: Record<string, unknown>,
|
||||
userProviders: Record<string, unknown> = providers,
|
||||
baseProviders: Record<string, unknown> = {},
|
||||
providers: Record<string, JsonValue>,
|
||||
userProviders: Record<string, JsonValue> = providers,
|
||||
baseProviders: Record<string, JsonValue> = {},
|
||||
): SettingsNamespaceView {
|
||||
return {
|
||||
ns: 'llm-pi-ai',
|
||||
schema: JSON.parse(JSON.stringify(PiAiConfig.toJSON())) as unknown,
|
||||
schema: JSON.parse(JSON.stringify(PiAiConfig.toJSON())) as JsonValue,
|
||||
// `value` is the effective section; `user` is only the layer this page
|
||||
// writes. They differ whenever a composition `base` supplies something.
|
||||
value: { providers },
|
||||
@@ -66,11 +73,11 @@ function piAiNamespace(
|
||||
}
|
||||
|
||||
function scriptedFace(options: {
|
||||
providers?: Record<string, unknown>
|
||||
providers?: Record<string, JsonValue>
|
||||
/** User layer, when it differs from the effective section. */
|
||||
userProviders?: Record<string, unknown>
|
||||
userProviders?: Record<string, JsonValue>
|
||||
/** Composition layer, for a route a `cordis.yml` pins rather than the page. */
|
||||
baseProviders?: Record<string, unknown>
|
||||
baseProviders?: Record<string, JsonValue>
|
||||
/** Routes the adapter reports as hand-declared; the rest come back as shipped. */
|
||||
declaredRoutes?: readonly string[]
|
||||
discover?: ReturnType<typeof vi.fn>
|
||||
@@ -82,8 +89,8 @@ function scriptedFace(options: {
|
||||
}
|
||||
const namespace = piAiNamespace(providers, options.userProviders ?? providers, options.baseProviders ?? {})
|
||||
const discover = options.discover ?? vi.fn(() => Promise.resolve(ok({ models: [] })))
|
||||
const mutate = options.mutate ?? vi.fn(() => Promise.resolve(ok(namespace)))
|
||||
const set = options.set ?? vi.fn(() => Promise.resolve(ok({})))
|
||||
const mutate = options.mutate ?? vi.fn(() => Promise.resolve(remoteOk(namespace)))
|
||||
const set = options.set ?? vi.fn(() => Promise.resolve(remoteOk(undefined)))
|
||||
const face = {
|
||||
llm: {
|
||||
providers: vi.fn(() => Promise.resolve(ok({
|
||||
@@ -100,15 +107,13 @@ function scriptedFace(options: {
|
||||
discoverModels: discover,
|
||||
},
|
||||
settings: {
|
||||
describe: vi.fn(() => Promise.resolve(ok({ writable: true, namespaces: [namespace] }))),
|
||||
update: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
describe: vi.fn(() => Promise.resolve(remoteOk({ writable: true, namespaces: [namespace] }))),
|
||||
mutate,
|
||||
},
|
||||
credentials: {
|
||||
describe: vi.fn((payload: { refs: string[] }) => Promise.resolve(ok({
|
||||
credentials: Object.fromEntries(payload.refs.map(ref => [ref, { configured: false, writable: true }])),
|
||||
}))),
|
||||
describe: vi.fn((refs: string[]) => Promise.resolve(remoteOk(
|
||||
Object.fromEntries(refs.map(ref => [ref, { configured: false, writable: true }])),
|
||||
))),
|
||||
set,
|
||||
unset: vi.fn(),
|
||||
},
|
||||
@@ -132,11 +137,16 @@ function firstProbe(discover: ReturnType<typeof vi.fn>): unknown {
|
||||
return call
|
||||
}
|
||||
|
||||
/** The first recorded settings write; fails the case when nothing was written. */
|
||||
/**
|
||||
* The first recorded settings write, as one record. The Remote method takes
|
||||
* three positional arguments; the cases read the write as a whole, so the
|
||||
* regrouping lives here rather than in every assertion.
|
||||
*/
|
||||
function firstMutate(mutate: ReturnType<typeof vi.fn>): MutateCall {
|
||||
const call = mutate.mock.calls[0]?.[0] as MutateCall | undefined
|
||||
const call = mutate.mock.calls[0] as [string, MutateCall['ops'], number | undefined] | undefined
|
||||
if (call === undefined) throw new Error('no settings write was recorded')
|
||||
return call
|
||||
const [ns, ops, expectedRevision] = call
|
||||
return { ns, ops, ...expectedRevision === undefined ? {} : { expectedRevision } }
|
||||
}
|
||||
|
||||
async function mountSection(options: Parameters<typeof scriptedFace>[0] = {}) {
|
||||
@@ -190,7 +200,7 @@ describe('protocolChoices', () => {
|
||||
const { namespace } = scriptedFace()
|
||||
expect(protocolChoices(namespace, settingsSchema)).toEqual(PROTOCOLS)
|
||||
expect(protocolChoices(undefined, settingsSchema)).toEqual([])
|
||||
const plain = { ...namespace, schema: JSON.parse(JSON.stringify(Schema.object({}).toJSON())) as unknown }
|
||||
const plain = { ...namespace, schema: JSON.parse(JSON.stringify(Schema.object({}).toJSON())) as JsonValue }
|
||||
expect(protocolChoices(plain, settingsSchema)).toEqual([])
|
||||
await Promise.resolve()
|
||||
})
|
||||
@@ -734,7 +744,7 @@ describe('hand-declared providers', () => {
|
||||
// meanwhile makes this a conflict rather than an overwrite.
|
||||
expectedRevision: 7,
|
||||
})
|
||||
expect(set).toHaveBeenCalledWith({ ref: 'ACME_GATEWAY_API_KEY', value: 'gw-key' })
|
||||
expect(set).toHaveBeenCalledWith('ACME_GATEWAY_API_KEY', 'gw-key')
|
||||
})
|
||||
|
||||
it('scopes each card to fields a provider can actually own', async () => {
|
||||
@@ -901,8 +911,8 @@ describe('hand-declared providers', () => {
|
||||
|
||||
it('retries only the key after the profile landed, and reports the provider on cancel', async () => {
|
||||
const set = vi.fn()
|
||||
.mockResolvedValueOnce(fail('credential store is read-only', 'credential-rejected'))
|
||||
.mockResolvedValueOnce(ok({}))
|
||||
.mockResolvedValueOnce(remoteFail('credential store is read-only'))
|
||||
.mockResolvedValueOnce(remoteOk(undefined))
|
||||
const { mutate, onClose } = mountCard({}, { set })
|
||||
|
||||
fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } })
|
||||
@@ -917,7 +927,7 @@ describe('hand-declared providers', () => {
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
expect(mutate).toHaveBeenCalledTimes(1)
|
||||
// The key is stored trimmed, matching the editor.
|
||||
expect(set).toHaveBeenNthCalledWith(1, { ref: 'ACME_API_KEY', value: 'gw-key' })
|
||||
expect(set).toHaveBeenNthCalledWith(1, 'ACME_API_KEY', 'gw-key')
|
||||
|
||||
// The provider exists now, so the fields describing it are settled and
|
||||
// only the key can still be corrected.
|
||||
@@ -932,11 +942,11 @@ describe('hand-declared providers', () => {
|
||||
// first write superseded, so the Host would answer settings-conflict and
|
||||
// the key could never be stored from here at all.
|
||||
expect(mutate).toHaveBeenCalledTimes(1)
|
||||
expect(set).toHaveBeenNthCalledWith(2, { ref: 'ACME_API_KEY', value: 'gw-key-2' })
|
||||
expect(set).toHaveBeenNthCalledWith(2, 'ACME_API_KEY', 'gw-key-2')
|
||||
})
|
||||
|
||||
it('reports the created provider when cancelled after its profile landed', async () => {
|
||||
const set = vi.fn().mockResolvedValue(fail('nope', 'credential-rejected'))
|
||||
const set = vi.fn().mockResolvedValue(remoteFail('nope'))
|
||||
const { onClose } = mountCard({}, { set })
|
||||
|
||||
fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } })
|
||||
@@ -1122,7 +1132,7 @@ describe('hand-declared providers', () => {
|
||||
})
|
||||
|
||||
it('surfaces a refused write and a rejected transport without closing', async () => {
|
||||
const refused = vi.fn(() => Promise.resolve(fail('read-only settings', 'settings-rejected')))
|
||||
const refused = vi.fn(() => Promise.resolve(remoteFail('read-only settings', 'settings-rejected')))
|
||||
const { onClose } = mountCard({ api: { ...scriptedFace({ mutate: refused }).face } as never })
|
||||
|
||||
fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } })
|
||||
@@ -1150,7 +1160,7 @@ describe('hand-declared providers', () => {
|
||||
})
|
||||
|
||||
it('reports a stored profile whose key write was refused', async () => {
|
||||
const set = vi.fn(() => Promise.resolve(fail('credential is read-only', 'credential-rejected')))
|
||||
const set = vi.fn(() => Promise.resolve(remoteFail('credential is read-only')))
|
||||
const { onClose } = mountCard({ api: { ...scriptedFace({ set }).face } as never })
|
||||
|
||||
fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } })
|
||||
@@ -1363,7 +1373,7 @@ describe('API key field', () => {
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
|
||||
await waitFor(() => { expect(set).toHaveBeenCalled() })
|
||||
expect((set.mock.calls[0]?.[0] as { value: string }).value).toBe('sk-abc')
|
||||
expect(set.mock.calls[0]?.[1]).toBe('sk-abc')
|
||||
})
|
||||
|
||||
it('blocks the interrogation too, rather than spending a round trip on a refused key', async () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** Pure first-run readiness projection over the shared Models join. */
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { CredentialView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { CredentialInfo } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ModelsSettingsState, ProviderRow } from '../src/client/store.ts'
|
||||
import { onboardingReadiness, providerUsable } from '../src/client/store.ts'
|
||||
|
||||
const missingCredential: CredentialView = { configured: false, writable: true }
|
||||
const missingCredential: CredentialInfo = { configured: false, writable: true }
|
||||
|
||||
function row(overrides: Partial<ProviderRow> = {}): ProviderRow {
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,17 @@ function fail<T>(message: string): RpcResponse<T> {
|
||||
return { rpcId: `r-${nextRpc++}` as never, result: { ok: false, error: { code: 'internal', message, details: {} } } }
|
||||
}
|
||||
|
||||
/** Credentials answers over the Remote carrier, which has no envelope. */
|
||||
type RemoteAnswer<T> =
|
||||
| { readonly ok: true; readonly value: T }
|
||||
| { readonly ok: false; readonly error: { code: string; message: string; details: object } }
|
||||
function remoteOk<T>(value: T): RemoteAnswer<T> {
|
||||
return { ok: true, value }
|
||||
}
|
||||
function remoteFail<T>(message: string): RemoteAnswer<T> {
|
||||
return { ok: false, error: { code: 'internal', message, details: {} } }
|
||||
}
|
||||
|
||||
const DIRECTORY = [
|
||||
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true },
|
||||
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: true },
|
||||
@@ -43,8 +54,8 @@ const NAMESPACES = [
|
||||
|
||||
function api(overrides: {
|
||||
providers?: () => Promise<RpcResponse<{ providers: typeof DIRECTORY }>>
|
||||
describeSettings?: () => Promise<RpcResponse<{ writable: boolean; namespaces: typeof NAMESPACES }>>
|
||||
describeCredentials?: (refs: string[]) => Promise<RpcResponse<{ credentials: Record<string, unknown> }>>
|
||||
describeSettings?: () => Promise<RemoteAnswer<{ writable: boolean; hasDocument: boolean; namespaces: typeof NAMESPACES }>>
|
||||
describeCredentials?: (refs: readonly string[]) => Promise<RemoteAnswer<Record<string, unknown>>>
|
||||
} = {}) {
|
||||
const seenRefs: string[][] = []
|
||||
const face = {
|
||||
@@ -53,19 +64,19 @@ function api(overrides: {
|
||||
models: () => Promise.resolve(ok({ groups: [], failures: [] })),
|
||||
},
|
||||
settings: {
|
||||
describe: overrides.describeSettings ?? (() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: NAMESPACES }))),
|
||||
update: () => Promise.resolve(fail('unused')),
|
||||
replace: () => Promise.resolve(fail('unused')),
|
||||
describe: overrides.describeSettings
|
||||
?? (() => Promise.resolve(remoteOk({ writable: true, hasDocument: false, namespaces: NAMESPACES }))),
|
||||
mutate: () => Promise.resolve(remoteFail('the store spec issues no writes')),
|
||||
},
|
||||
credentials: {
|
||||
describe: (payload: { refs: string[] }) => {
|
||||
seenRefs.push(payload.refs)
|
||||
return (overrides.describeCredentials ?? (refs => Promise.resolve(ok({
|
||||
credentials: Object.fromEntries(refs.map(ref => [ref, { configured: ref === 'OPENAI_API_KEY', writable: true }])),
|
||||
}))))(payload.refs)
|
||||
describe: (refs: readonly string[]) => {
|
||||
seenRefs.push([...refs])
|
||||
return (overrides.describeCredentials ?? (asked => Promise.resolve(remoteOk(
|
||||
Object.fromEntries(asked.map(ref => [ref, { configured: ref === 'OPENAI_API_KEY', writable: true }])),
|
||||
))))(refs)
|
||||
},
|
||||
set: () => Promise.resolve(ok({})),
|
||||
unset: () => Promise.resolve(ok({})),
|
||||
set: () => Promise.resolve(remoteOk(undefined)),
|
||||
unset: () => Promise.resolve(remoteOk(undefined)),
|
||||
},
|
||||
}
|
||||
const wire = face as never
|
||||
@@ -104,7 +115,7 @@ describe('ModelsSettingsStore', () => {
|
||||
})
|
||||
|
||||
it('degrades the credential badge, not the page, when the credential domain fails', async () => {
|
||||
const { face, mirror } = api({ describeCredentials: () => Promise.resolve(fail('no provider')) })
|
||||
const { face, mirror } = api({ describeCredentials: () => Promise.resolve(remoteFail('no provider')) })
|
||||
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
|
||||
await store.load()
|
||||
const state = store.store.getSnapshot()
|
||||
@@ -173,7 +184,7 @@ describe('ModelsSettingsStore', () => {
|
||||
describe('edge joins', () => {
|
||||
it('treats a non-object profile as having no credential reference', async () => {
|
||||
const { face, mirror } = api({
|
||||
describeSettings: () => Promise.resolve(ok({
|
||||
describeSettings: () => Promise.resolve(remoteOk({
|
||||
writable: true,
|
||||
hasDocument: false,
|
||||
namespaces: [{
|
||||
@@ -200,7 +211,7 @@ describe('edge joins', () => {
|
||||
|
||||
it('describes the derived reference for a row whose profile names none', async () => {
|
||||
const { face, mirror, seenRefs } = api({
|
||||
describeSettings: () => Promise.resolve(ok({
|
||||
describeSettings: () => Promise.resolve(remoteOk({
|
||||
writable: true,
|
||||
hasDocument: false,
|
||||
namespaces: [{ ns: 'llm-pi-ai', schema: {}, value: { providers: {} }, applies: 'live' as const, secrets: [], revision: 0 }] as never,
|
||||
@@ -210,9 +221,9 @@ describe('edge joins', () => {
|
||||
{ provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], active: false },
|
||||
] as never,
|
||||
})),
|
||||
describeCredentials: refs => Promise.resolve(ok({
|
||||
credentials: Object.fromEntries(refs.map(ref => [ref, { configured: true, writable: true }])),
|
||||
})),
|
||||
describeCredentials: refs => Promise.resolve(remoteOk(
|
||||
Object.fromEntries(refs.map(ref => [ref, { configured: true, writable: true }])),
|
||||
)),
|
||||
})
|
||||
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
|
||||
await store.load()
|
||||
@@ -226,7 +237,7 @@ describe('edge joins', () => {
|
||||
})
|
||||
|
||||
it('surfaces a settings describe failure', async () => {
|
||||
const { face, mirror } = api({ describeSettings: () => Promise.resolve(fail('settings down')) })
|
||||
const { face, mirror } = api({ describeSettings: () => Promise.resolve(remoteFail('settings down')) })
|
||||
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
|
||||
await store.load()
|
||||
expect(store.store.getSnapshot()).toMatchObject({ status: 'error', error: 'settings down' })
|
||||
@@ -252,8 +263,8 @@ describe('edge joins', () => {
|
||||
describeSettings: () => {
|
||||
settingsCall += 1
|
||||
return Promise.resolve(settingsCall === 1
|
||||
? ok({ writable: true, hasDocument: false, namespaces: NAMESPACES })
|
||||
: fail('settings refresh down'))
|
||||
? remoteOk({ writable: true, hasDocument: false, namespaces: NAMESPACES })
|
||||
: remoteFail('settings refresh down'))
|
||||
},
|
||||
})
|
||||
const store = new ModelsSettingsStore(face, settingsSchema, mirror)
|
||||
|
||||
@@ -29,8 +29,9 @@ afterEach(() => {
|
||||
document.getElementById('root')?.remove()
|
||||
})
|
||||
|
||||
function response<T>(value: T) {
|
||||
return { rpcId: 'welcome-rpc' as never, result: { ok: true as const, value } }
|
||||
/** The settings namespace answers over the Remote carrier, which has no envelope. */
|
||||
function remoteAnswer<T>(value: T) {
|
||||
return { ok: true as const, value }
|
||||
}
|
||||
|
||||
function welcomeView(value: unknown, revision = 0) {
|
||||
@@ -53,7 +54,7 @@ const useSessionPendingInteraction: WelcomeNoticeProps['useSessionPendingInterac
|
||||
function mount(
|
||||
version?: string,
|
||||
mutateImpl: () => Promise<unknown> = () =>
|
||||
Promise.resolve(response(welcomeView({ [WELCOME_NOTICE_ACK_FIELD]: WELCOME_NOTICE_VERSION }, 1))),
|
||||
Promise.resolve(remoteAnswer(welcomeView({ [WELCOME_NOTICE_ACK_FIELD]: WELCOME_NOTICE_VERSION }, 1))),
|
||||
) {
|
||||
const appRoot = document.createElement('div')
|
||||
appRoot.id = 'root'
|
||||
@@ -61,7 +62,7 @@ function mount(
|
||||
const mutate = vi.fn(mutateImpl)
|
||||
const api = {
|
||||
settings: {
|
||||
describe: () => Promise.resolve(response({
|
||||
describe: () => Promise.resolve(remoteAnswer({
|
||||
writable: true,
|
||||
hasDocument: false,
|
||||
namespaces: [welcomeView(version === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: version })],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { RpcResponse } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { SettingsSchemaService } from '@deepseek-ai/dsh-client-ui-settings/src/client/schema.ts'
|
||||
import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/src/client/settings-mirror.ts'
|
||||
@@ -11,9 +10,9 @@ import {
|
||||
|
||||
const schemaService = new SettingsSchemaService(new Context())
|
||||
|
||||
let rpc = 0
|
||||
function ok<T>(value: T): RpcResponse<T> {
|
||||
return { rpcId: `welcome-${rpc++}` as never, result: { ok: true, value } }
|
||||
/** The settings namespace answers over the Remote carrier, which has no envelope. */
|
||||
function ok<T>(value: T) {
|
||||
return { ok: true as const, value }
|
||||
}
|
||||
|
||||
function namespace(value: unknown = {}, revision = 0) {
|
||||
@@ -91,11 +90,11 @@ describe('WelcomeNoticeStore', () => {
|
||||
await mirror.load()
|
||||
await controller.load()
|
||||
await expect(controller.acknowledge()).resolves.toBe(true)
|
||||
expect(mutate).toHaveBeenCalledWith({
|
||||
ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
|
||||
ops: [{ op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION }],
|
||||
expectedRevision: 3,
|
||||
})
|
||||
expect(mutate).toHaveBeenCalledWith(
|
||||
WELCOME_NOTICE_SETTINGS_NAMESPACE,
|
||||
[{ op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION }],
|
||||
3,
|
||||
)
|
||||
expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true })
|
||||
// The write answer folded into the mirror; no re-read followed.
|
||||
expect(describeCall).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"dsh": {
|
||||
"client": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-connection",
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-ui-settings",
|
||||
"@deepseek-ai/dsh-api-remotes"
|
||||
@@ -48,7 +47,6 @@
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
@@ -57,7 +55,6 @@
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-store": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
|
||||
|
||||
@@ -9,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)
|
||||
@@ -50,20 +49,20 @@ 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', 'settingsScope']
|
||||
export const inject = ['slots', 'locale', 'remote', 'remote.credentials', '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')
|
||||
|
||||
const bash = new BashCardController(ctx.settingsScope.bind({ namespace: SHELL_NS }))
|
||||
const agentLoop = new AgentLoopCardController(ctx.settingsScope.bind({ namespace: AGENT_LOOP_NS }))
|
||||
const webSearch = new WebSearchCardController(ctx.settingsScope.bind({ namespace: WEB_SEARCH_NS }), api)
|
||||
const webSearch = new WebSearchCardController(
|
||||
ctx.settingsScope.bind({ namespace: WEB_SEARCH_NS }), ctx.remote.credentials)
|
||||
|
||||
// The credential a card reports is not part of any settings section, so its
|
||||
// scope publishes nothing when one is written. This is the only signal that
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* covers everything the card shows.
|
||||
*/
|
||||
|
||||
import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import {
|
||||
@@ -39,13 +39,16 @@ export interface WebSearchSettings {
|
||||
maxUses?: number
|
||||
}
|
||||
|
||||
/** The credentials Remote methods this card reads and writes through. */
|
||||
export type WebSearchCredentials = Pick<ClientRemote['credentials'], 'describe' | 'set'>
|
||||
|
||||
/** What the credentials domain last reported, and for which reference. */
|
||||
interface CredentialState {
|
||||
/** Reference this answer describes; a stale response for another one is dropped. */
|
||||
ref: string
|
||||
/** Whether any layer supplies a value for it. */
|
||||
configured: boolean
|
||||
/** Whether `credentials.set` can affect it; false disables the control. */
|
||||
/** Whether `credentials/set` can affect it; false disables the control. */
|
||||
writable: boolean
|
||||
}
|
||||
|
||||
@@ -79,11 +82,11 @@ export class WebSearchCardController {
|
||||
|
||||
/**
|
||||
* @param scope - the bound settings scope for the `web-search-deepseek` namespace.
|
||||
* @param api - wire face used for the credential the section references.
|
||||
* @param credentials - Remote face used for the credential the section references.
|
||||
*/
|
||||
constructor(
|
||||
private readonly scope: SettingsScope<WebSearchSettings>,
|
||||
private readonly api: Pick<IApiClient, 'credentials'>,
|
||||
private readonly credentials: WebSearchCredentials,
|
||||
) {
|
||||
this.form = new CardForm(
|
||||
scope,
|
||||
@@ -122,16 +125,16 @@ export class WebSearchCardController {
|
||||
this.credential = { ref, configured: false, writable: true }
|
||||
this.store.set(this.projection())
|
||||
}
|
||||
let response: Awaited<ReturnType<IApiClient['credentials']['describe']>>
|
||||
let response: Awaited<ReturnType<WebSearchCredentials['describe']>>
|
||||
try {
|
||||
response = await this.api.credentials.describe({ refs: [ref] })
|
||||
response = await this.credentials.describe([ref])
|
||||
} catch (_credentialReadFailure) {
|
||||
// The card stays usable without this: the key control simply reports the
|
||||
// last state it knew, and a write still reaches the Host.
|
||||
return
|
||||
}
|
||||
if (!response.result.ok || ref !== refOf(this.scope.getSnapshot())) return
|
||||
const view = response.result.value.credentials[ref]
|
||||
if (!response.ok || ref !== refOf(this.scope.getSnapshot())) return
|
||||
const view = response.value[ref]
|
||||
const next: CredentialState = {
|
||||
ref,
|
||||
configured: view?.configured ?? false,
|
||||
@@ -172,7 +175,7 @@ export class WebSearchCardController {
|
||||
*/
|
||||
private async writeKey(value: string): Promise<boolean> {
|
||||
try {
|
||||
await this.api.credentials.set({ ref: refOf(this.scope.getSnapshot()), value })
|
||||
await this.credentials.set(refOf(this.scope.getSnapshot()), value)
|
||||
} catch (_credentialWriteFailure) {
|
||||
// Refusals surface through the re-read below: the Host is the only
|
||||
// authority on whether the key now exists.
|
||||
|
||||
@@ -26,30 +26,24 @@ async function bench(served?: string[]) {
|
||||
const locale = new LocaleRuntime(ctx)
|
||||
locale.setLocale('zh')
|
||||
ctx.provide('locale', locale)
|
||||
const describeCredentials = vi.fn(() => Promise.resolve({ rpcId: 'c', result: { ok: false, error: {} } }))
|
||||
const describeCredentials = vi.fn(() => Promise.resolve({ ok: false, error: { code: 'internal', message: 'no provider', details: {} } }))
|
||||
const describeSettings = vi.fn(() => Promise.resolve(served === undefined
|
||||
? { rpcId: 's', result: { ok: false, error: {} } }
|
||||
? { ok: false, error: { code: 'internal', message: 'no provider', details: {} } }
|
||||
: {
|
||||
rpcId: 's',
|
||||
result: {
|
||||
ok: true,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: true,
|
||||
namespaces: served.map(ns => ({
|
||||
ns, schema: {}, value: {}, applies: 'live', secrets: [], revision: 0,
|
||||
})),
|
||||
},
|
||||
ok: true,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: true,
|
||||
namespaces: served.map(ns => ({
|
||||
ns, schema: {}, value: {}, applies: 'live', secrets: [], revision: 0,
|
||||
})),
|
||||
},
|
||||
}))
|
||||
const remote = new TestRemote(ctx)
|
||||
ctx.provide('connection', {
|
||||
isLoopback: true,
|
||||
api: {
|
||||
settings: { describe: describeSettings },
|
||||
credentials: { describe: describeCredentials },
|
||||
},
|
||||
} as never)
|
||||
const remote = new TestRemote(ctx, {
|
||||
credentials: { describe: describeCredentials, set: vi.fn() },
|
||||
settings: { describe: describeSettings },
|
||||
})
|
||||
ctx.provide('connection', { isLoopback: true, api: {} } as never)
|
||||
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
|
||||
return { ctx, slots: ctx.get('slots') as SlotRegistry, describeCredentials, describeSettings, remote }
|
||||
}
|
||||
@@ -63,7 +57,7 @@ function declareRoot(slots: SlotRegistry): () => void {
|
||||
|
||||
describe('ui-settings-plugins apply', () => {
|
||||
it('declares the services it uses', () => {
|
||||
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'settingsScope'])
|
||||
expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.credentials', 'settingsScope'])
|
||||
})
|
||||
|
||||
it('registers one Plugins section and declares the tab and card slots', async () => {
|
||||
|
||||
@@ -30,11 +30,11 @@ function acceptWrites<T>(host: StubSettingsScope<T>): void {
|
||||
|
||||
function credentialsApi(configured: boolean) {
|
||||
const describe = vi.fn(() => Promise.resolve({
|
||||
rpcId: 'c-1' as never,
|
||||
result: { ok: true as const, value: { credentials: { DEEPSEEK_API_KEY: { configured, writable: true } } } },
|
||||
ok: true as const,
|
||||
value: { DEEPSEEK_API_KEY: { configured, writable: true } },
|
||||
}))
|
||||
const set = vi.fn(() => Promise.resolve({ rpcId: 'c-2' as never, result: { ok: true as const, value: {} } }))
|
||||
return { api: { credentials: { describe, set } } as never, describe, set }
|
||||
const set = vi.fn(() => Promise.resolve({ ok: true as const, value: undefined }))
|
||||
return { api: { describe, set } as never, describe, set }
|
||||
}
|
||||
|
||||
describe('CardForm', () => {
|
||||
@@ -412,13 +412,13 @@ describe('WebSearchCardController', () => {
|
||||
expect(credentials.set).not.toHaveBeenCalled()
|
||||
|
||||
credentials.describe.mockImplementation(() => Promise.resolve({
|
||||
rpcId: 'c-1' as never,
|
||||
result: { ok: true as const, value: { credentials: { DEEPSEEK_API_KEY: { configured: true, writable: true } } } },
|
||||
ok: true as const,
|
||||
value: { DEEPSEEK_API_KEY: { configured: true, writable: true } },
|
||||
}))
|
||||
face.save()
|
||||
await vi.waitFor(() => { expect(credentials.set).toHaveBeenCalled() })
|
||||
|
||||
expect(credentials.set).toHaveBeenCalledWith({ ref: 'DEEPSEEK_API_KEY', value: 'ds-secret' })
|
||||
expect(credentials.set).toHaveBeenCalledWith('DEEPSEEK_API_KEY', 'ds-secret')
|
||||
expect(host.set).not.toHaveBeenCalled()
|
||||
await vi.waitFor(() => {
|
||||
expect(face.hooks.webSearchCard.getSnapshot()).toMatchObject({ dirty: false, apiKeyConfigured: true })
|
||||
@@ -454,8 +454,8 @@ describe('WebSearchCardController', () => {
|
||||
|
||||
// A key written on another surface reaches this card only through this signal.
|
||||
credentials.describe.mockImplementation(() => Promise.resolve({
|
||||
rpcId: 'c-1' as never,
|
||||
result: { ok: true as const, value: { credentials: { DEEPSEEK_API_KEY: { configured: true, writable: true } } } },
|
||||
ok: true as const,
|
||||
value: { DEEPSEEK_API_KEY: { configured: true, writable: true } },
|
||||
}))
|
||||
controller.refreshCredential('DEEPSEEK_API_KEY')
|
||||
|
||||
@@ -475,7 +475,7 @@ describe('WebSearchCardController', () => {
|
||||
face.save()
|
||||
await vi.waitFor(() => { expect(credentials.set).toHaveBeenCalled() })
|
||||
|
||||
expect(credentials.set).toHaveBeenCalledWith({ ref: 'SEARCH_KEY', value: 'ds-secret' })
|
||||
expect(credentials.set).toHaveBeenCalledWith('SEARCH_KEY', 'ds-secret')
|
||||
})
|
||||
|
||||
it('reports a key the Host did not store as a failed save', async () => {
|
||||
@@ -497,7 +497,7 @@ describe('WebSearchCardController', () => {
|
||||
const host = stubSettingsScope<WebSearchSettings>()
|
||||
const describe = vi.fn(() => Promise.reject(new Error('offline')))
|
||||
const set = vi.fn(() => Promise.reject(new Error('offline')))
|
||||
const controller = new WebSearchCardController(host.scope, { credentials: { describe, set } } as never)
|
||||
const controller = new WebSearchCardController(host.scope, { describe, set })
|
||||
const face = controller.inject()
|
||||
await vi.waitFor(() => { expect(describe).toHaveBeenCalled() })
|
||||
|
||||
@@ -516,10 +516,10 @@ describe('WebSearchCardController', () => {
|
||||
it('ignores a credential read the Host refused', async () => {
|
||||
const host = stubSettingsScope<WebSearchSettings>()
|
||||
const describe = vi.fn(() => Promise.resolve({
|
||||
rpcId: 'c-1' as never,
|
||||
result: { ok: false as const, error: { code: 'credentials-unavailable', message: 'no provider' } },
|
||||
ok: false as const,
|
||||
error: { code: 'internal', message: 'no credential provider', details: {} },
|
||||
}))
|
||||
const controller = new WebSearchCardController(host.scope, { credentials: { describe, set: vi.fn() } } as never)
|
||||
const controller = new WebSearchCardController(host.scope, { describe, set: vi.fn() })
|
||||
await vi.waitFor(() => { expect(describe).toHaveBeenCalled() })
|
||||
|
||||
expect(controller.inject().hooks.webSearchCard.getSnapshot().apiKeyConfigured).toBe(false)
|
||||
@@ -546,16 +546,13 @@ describe('WebSearchCardController', () => {
|
||||
describe('ConfigurablePluginsTabController', () => {
|
||||
function settingsApi(namespaces: string[]) {
|
||||
const describe = vi.fn(() => Promise.resolve({
|
||||
rpcId: 's-1' as never,
|
||||
result: {
|
||||
ok: true as const,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: true,
|
||||
namespaces: namespaces.map(ns => ({
|
||||
ns, schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0,
|
||||
})),
|
||||
},
|
||||
ok: true as const,
|
||||
value: {
|
||||
writable: true,
|
||||
hasDocument: true,
|
||||
namespaces: namespaces.map(ns => ({
|
||||
ns, schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 0,
|
||||
})),
|
||||
},
|
||||
}))
|
||||
return { mirror: new SettingsDescribeMirror({ settings: { describe } } as never), describe }
|
||||
|
||||
@@ -32,13 +32,15 @@ export type { SettingsScopeController, SettingsScopeBinder } from './settings-sc
|
||||
export type { SettingsScope, SettingsScopeSnapshot, SettingsScopeSpec } from './settings-contract.ts'
|
||||
export type { SettingsSchemaService } from './schema.ts'
|
||||
export type { SchemaNode } from './schema.ts'
|
||||
export type { SettingsDescribeFace, SettingsDescribeView, SettingsMirrorSnapshot } from './settings-mirror.ts'
|
||||
export type {
|
||||
SettingsDescribeFace, SettingsDescribeView, SettingsMirrorSnapshot, SettingsRemote, SettingsWireFace,
|
||||
} from './settings-mirror.ts'
|
||||
|
||||
/**
|
||||
* Required services: the wire handle for the mirror's reads and the forwarded
|
||||
* settings invalidation the mirror refreshes on.
|
||||
*/
|
||||
export const inject = ['connection', 'remote']
|
||||
export const inject = ['connection', 'remote', 'remote.settings']
|
||||
|
||||
/**
|
||||
* Provide the settings-namespace scope service over one shared describe
|
||||
@@ -52,10 +54,10 @@ export const inject = ['connection', 'remote']
|
||||
export function apply(ctx: Context): void {
|
||||
const schema = new SettingsSchemaService(ctx)
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
const mirror = new SettingsDescribeMirror(
|
||||
connection.api,
|
||||
connection.isLoopback ? 'host' : 'memory',
|
||||
)
|
||||
// Captured once here, where `remote.settings` is declared in this plugin's
|
||||
// own `inject`; the binder hands the same face to every scope it binds.
|
||||
const wire = { settings: ctx.remote.settings }
|
||||
const mirror = new SettingsDescribeMirror(wire, connection.isLoopback ? 'host' : 'memory')
|
||||
ctx.effect(() => {
|
||||
const disposers = [
|
||||
ctx.remote.$on('settings/document-updated', () => { void mirror.load() }),
|
||||
@@ -68,5 +70,5 @@ export function apply(ctx: Context): void {
|
||||
void mirror.ensure()
|
||||
return () => { for (const dispose of disposers) dispose() }
|
||||
}, 'ui-settings: describe mirror invalidations')
|
||||
new SettingsScopeBinder(ctx, { mirror, schema })
|
||||
new SettingsScopeBinder(ctx, { mirror, schema, wire })
|
||||
}
|
||||
|
||||
@@ -9,10 +9,24 @@
|
||||
* through {@link SettingsDescribeMirror.acceptView}.
|
||||
*/
|
||||
|
||||
import type { IApiClient, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ClientRemote, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
|
||||
type SettingsFace = Pick<IApiClient, 'settings'>
|
||||
/**
|
||||
* The settings Remote methods browser configuration surfaces may reach: the
|
||||
* redacted read plus merge, replacement, and path-addressed writes.
|
||||
* Named once here so the consumers share one face instead of each re-deriving
|
||||
* it from the namespace.
|
||||
*/
|
||||
export type SettingsRemote = Pick<ClientRemote['settings'], 'describe' | 'update' | 'replace' | 'mutate'>
|
||||
|
||||
/** Wire face carrying the settings Remote namespace. */
|
||||
export interface SettingsWireFace {
|
||||
/** The settings Remote namespace. */
|
||||
settings: SettingsRemote
|
||||
}
|
||||
|
||||
type SettingsFace = SettingsWireFace
|
||||
|
||||
/** The full `settings.describe` answer the mirror serves. */
|
||||
export interface SettingsDescribeView {
|
||||
@@ -180,10 +194,10 @@ export class SettingsDescribeMirror implements SettingsDescribeFace {
|
||||
const generation = ++this.generation
|
||||
let outcome: { view: SettingsDescribeView } | { failure: string }
|
||||
try {
|
||||
const response = await this.api.settings.describe({})
|
||||
outcome = response.result.ok
|
||||
? { view: response.result.value }
|
||||
: { failure: response.result.error.message }
|
||||
const response = await this.api.settings.describe()
|
||||
outcome = response.ok
|
||||
? { view: response.value }
|
||||
: { failure: response.error.message }
|
||||
} catch (error) {
|
||||
outcome = { failure: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { Service } from '@deepseek-ai/cordis'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {
|
||||
ConnectionHandle, IApiClient, SettingsNamespaceView, SettingsPathOpView,
|
||||
ConnectionHandle, JsonValue, SettingsNamespaceView, SettingsPathOpView,
|
||||
} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
// Type-only, and deliberately NOT `@deepseek-ai/dsh-api-remotes/client`: this
|
||||
@@ -30,9 +30,9 @@ import type {} from '@deepseek-ai/dsh-api-remotes/types'
|
||||
import type {} from '@deepseek-ai/dsh-settings/types'
|
||||
import type { SettingsSchemaService } from './schema.ts'
|
||||
import type { SettingsScope, SettingsScopeSnapshot, SettingsScopeSpec } from './settings-contract.ts'
|
||||
import { SettingsDescribeMirror, type SettingsDescribeFace } from './settings-mirror.ts'
|
||||
import { SettingsDescribeMirror, type SettingsDescribeFace, type SettingsWireFace } from './settings-mirror.ts'
|
||||
|
||||
type SettingsFace = Pick<IApiClient, 'settings'>
|
||||
type SettingsFace = SettingsWireFace
|
||||
|
||||
/**
|
||||
* One namespace's derived view over the shared describe mirror, plus that
|
||||
@@ -104,7 +104,7 @@ export class SettingsScopeController<T> implements SettingsScope<T> {
|
||||
* @returns settlement after the write and any latest-write recovery read.
|
||||
*/
|
||||
set(field: string, value: unknown): Promise<void> {
|
||||
return this.write({ op: 'set', path: [field], value })
|
||||
return this.write({ op: 'set', path: [field], value: value as JsonValue })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,25 +123,21 @@ export class SettingsScopeController<T> implements SettingsScope<T> {
|
||||
const revision = this.pendingRevision ?? this.getSnapshot().revision
|
||||
let response: Awaited<ReturnType<SettingsFace['settings']['mutate']>>
|
||||
try {
|
||||
response = await this.api.settings.mutate({
|
||||
ns: this.spec.namespace,
|
||||
ops: [op],
|
||||
...(revision === undefined ? {} : { expectedRevision: revision }),
|
||||
})
|
||||
response = await this.api.settings.mutate(this.spec.namespace, [op], revision)
|
||||
} catch (_settingsWriteFailure) {
|
||||
await this.recover(generation)
|
||||
return
|
||||
}
|
||||
if (!response.result.ok) {
|
||||
if (!response.ok) {
|
||||
await this.recover(generation)
|
||||
return
|
||||
}
|
||||
if (this.disposed) return
|
||||
if (generation === this.writeGeneration) {
|
||||
this.pendingRevision = undefined
|
||||
this.mirror.acceptView(response.result.value)
|
||||
this.mirror.acceptView(response.value)
|
||||
} else {
|
||||
this.pendingRevision = response.result.value.revision
|
||||
this.pendingRevision = response.value.revision
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -235,16 +231,26 @@ declare module '@deepseek-ai/cordis' {
|
||||
export class SettingsScopeBinder extends Service {
|
||||
private readonly mirror: SettingsDescribeMirror
|
||||
private readonly schema: SettingsSchemaService
|
||||
private readonly wire: SettingsWireFace
|
||||
|
||||
/**
|
||||
* @param ctx - the providing plugin's context.
|
||||
* @param config - the shared describe mirror every bound scope derives from,
|
||||
* plus the settings-owned schema operations.
|
||||
* the settings-owned schema operations, and the settings Remote namespace the
|
||||
* bound scopes write through. The namespace is captured here rather than read
|
||||
* inside {@link bind}, because a Service reads `ctx` as its *consumer's*
|
||||
* fiber: reading it there would make every caller declare `remote.settings`
|
||||
* in its own `inject`.
|
||||
*/
|
||||
constructor(ctx: Context, config: { mirror: SettingsDescribeMirror; schema: SettingsSchemaService }) {
|
||||
constructor(ctx: Context, config: {
|
||||
mirror: SettingsDescribeMirror
|
||||
schema: SettingsSchemaService
|
||||
wire: SettingsWireFace
|
||||
}) {
|
||||
super(ctx, 'settingsScope')
|
||||
this.mirror = config.mirror
|
||||
this.schema = config.schema
|
||||
this.wire = config.wire
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,7 +278,7 @@ export class SettingsScopeBinder extends Service {
|
||||
const ctx = this.ctx
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
const controller = new SettingsScopeController<T>(
|
||||
connection.api,
|
||||
this.wire,
|
||||
spec,
|
||||
this.mirror,
|
||||
connection.isLoopback ? 'host' : 'memory',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user