refactor(web): consume owner remote events directly

This commit is contained in:
imccyu
2026-08-11 16:50:03 +08:00
parent 80f1a064f5
commit e0bbe42242
75 changed files with 353 additions and 350 deletions

View File

@@ -10,6 +10,8 @@ When the Host reports that no adapter serves the session's route (`session.model
Directories are per-session, resolved lazily through `ctx.models.directoryFor(sessionId)`, and disposed with the session scope. Addressed subagent sessions expose neither entry, and their directory rejects loads, selections, and reconnect refreshes, because ordinary Agent-bound model RPCs would activate persisted child history outside the direct-parent continuation path.
Every resident directory refetches directly on forwarded `llm/adapters-updated` and `settings/document-updated` owner events. Provider topology, provider catalogs, and the default selection therefore converge without the Host or client runtime deriving a separate model-change alias.
The `/client` exports are the plugin body (`apply`/`inject`), `ModelService`, `ModelDirectory` with its state fields, and the seat's injected face type.
## Model Experience

View File

@@ -10,6 +10,8 @@ Host 报告的 `ModelSelection` 是唯一的选择事实,其中包含提供方
目录按会话惰性解析(`ctx.models.directoryFor(sessionId)`),随会话作用域一并释放。已寻址 subagent 会话不公开任一入口,其目录会拒绝加载、选择与重新连接刷新,因为绑定到 agent(智能体)的普通模型 RPC 会在直接 parent 继续执行路径之外激活持久化 child 历史。
每一份常驻目录都会直接在转发的 owner 事件 `llm/adapters-updated` 与 `settings/document-updated` 上重拉。因此提供方拓扑、提供方目录与默认选择都能收敛,Host 与 client runtime 无需再派生一个单独的模型变更别名。
`/client` 导出面为插件本体(`apply`/`inject`)、`ModelService`、`ModelDirectory` 及其状态形状、slot 注入面类型。
## 模型体验

View File

@@ -34,7 +34,8 @@
"inject": [
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-command"
"@deepseek-ai/dsh-client-ui-command",
"@deepseek-ai/dsh-api-remotes"
],
"platform": "web"
}
@@ -45,6 +46,7 @@
},
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-api-remotes": "workspace:^",
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
@@ -59,9 +61,11 @@
"react": "^18.2.0"
},
"devDependencies": {
"@deepseek-ai/dsh-api-remotes": "workspace:^",
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-command": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",

View File

@@ -12,6 +12,8 @@
* history outside the direct-parent continuation path.
*/
import type { ModelSelection, SessionModels } from '@deepseek-ai/dsh-client-connection/client'
// Type-only: pulls the forwarded Host-event face and ctx.remote merge.
import type {} from '@deepseek-ai/dsh-api-remotes/client'
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
import type { CommandServiceContract, SelectOption } from '@deepseek-ai/dsh-client-ui-command/client'
// Type-only: pulls the ui-conversation SlotMap merge (the input.model seat).
@@ -96,7 +98,7 @@ function selectionOf(state: ModelDirectoryState, id: string): ModelSelection | u
const NS = 'model'
/** Required services: the contribution registry, the seat's slot registry, locale, and the service's own faces. */
export const inject = ['command', 'connection', 'locale', 'sessions', 'slots']
export const inject = ['command', 'connection', 'locale', 'sessions', 'slots', 'remote']
/**
* Client plugin body: mount ModelService, register the `model` dictionaries,

View File

@@ -32,7 +32,7 @@ interface LiveState {
/** The `ctx.models` session model-selection service. */
export class ModelService extends Service {
static inject = ['connection', 'sessions']
static inject = ['connection', 'sessions', 'remote']
private readonly live: LiveState = { directories: new Map() }
@@ -49,14 +49,15 @@ export class ModelService extends Service {
ctx.on('connection/reset', () => {
for (const directory of this.live.directories.values()) directory.resetConnected()
})
// Provider topology changed on the host (a settings-born route appeared
// or dropped): refresh every open directory in the background so pickers
// show the new catalog without a reopen. Failures stay on each store.
ctx.on('models/changed', () => {
// Either source can change the directory: registry topology commits and
// settings documents that carry provider catalogs or default selection.
const refresh = (): void => {
for (const directory of this.live.directories.values()) {
directory.load().catch(() => undefined)
}
})
}
ctx.remote.$on('llm/adapters-updated', refresh)
ctx.remote.$on('settings/document-updated', refresh)
}
/**

View File

@@ -13,6 +13,7 @@ import { describe, expect, it } from 'vitest'
import { createScope } from '@deepseek-ai/dsh-client-runtime/client'
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
import type { ModelSelection } from '@deepseek-ai/dsh-client-connection/client'
import type { CommandContribution, SelectOption } from '@deepseek-ai/dsh-client-ui-command/client'
import type { ModelSelectInjected } from '../src/client/slots.ts'
@@ -112,6 +113,7 @@ async function bench() {
? { parentSessionId: sid('parent'), childSessionId: id, mode: 'continuable' as const }
: undefined,
})
new TestRemote(ctx)
const fiber = ctx.plugin({ inject: [...inject], apply })
await fiber.await()
await ctx.plugin(function probe() {}).await()
@@ -245,14 +247,14 @@ describe('ui-model dual entry', () => {
expect(b.blockOf('s1')).toBeUndefined()
b.setRoutable(false)
b.ctx.emit('models/changed')
b.ctx.remote.$dispatch('llm/adapters-updated', [])
await Promise.resolve()
await Promise.resolve()
expect(b.blockOf('s1')?.reason).toBe(zh['blocked.composer'])
// Recovering clears it without a reload of the surface.
b.setRoutable(true)
b.ctx.emit('models/changed')
b.ctx.remote.$dispatch('settings/document-updated', ['llm-deepseek', 1])
await Promise.resolve()
await Promise.resolve()
expect(b.blockOf('s1')).toBeUndefined()

View File

@@ -37,6 +37,9 @@
},
{
"path": "../../support/invariants"
},
{
"path": "../../api/remotes/tsconfig.client.json"
}
]
}