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

@@ -9,7 +9,7 @@ import { dirname } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import { installModelSelection } from '@deepseek-ai/dsh-agent'
import type { Agent, ModelSelection, ModelSelectionRef, AgentOptions, AgentStatus } from '@deepseek-ai/dsh-agent'
import { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-default-model'
import type {} from '@deepseek-ai/dsh-agent-presets/types'
import { AttachmentError } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import { contentHasImage, createUserMessage, freezeMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
@@ -3467,10 +3467,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
}),
// Allowlisted host events ride one verbatim wrapper frame each. The
// allowlist is api-remotes', and `ctx.remote.$on` is the consumer
// face; nothing here projects, redacts, or renames. Registered ahead
// of the derived frames below so a forwarded event still precedes the
// invalidation derived from it (`settings/document-updated` before
// its `host/models-changed`), which is the order a client sees.
// face; nothing here projects, redacts, or renames.
...API_REMOTE_FORWARDED_EVENTS.map(name => ctx.on(
name,
// The allowlist's shape assertion proves each name is a real,
@@ -3485,37 +3482,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
}))
}),
)),
// The recompose itself registers nothing (it re-parents the agent's
// scope onto a standing mount that may already exist), so the
// logged selection is the only commit point a client can follow.
ctx.on('session/event', (session: Session, event: SessionEvent) => {
if (event.type !== 'agent-preset/selected') return
queue.push(frame({
type: 'host/session-preset-changed',
sessionId: session.id,
agentPreset: event.data.agentPreset,
}))
}),
ctx.on('settings/document-updated', (ns) => {
// The RAW-section event, not the resolved one: a field going from
// inherited to overridden leaves the resolved value equal, and a
// configuration client still has to re-read (its held revision is
// stale, and the field's meaning changed).
const name = String(ns)
// A provider's own settings carry its model catalog and endpoint,
// so a change there invalidates the model list even when the route
// set is untouched — `llm/adapters-updated` alone misses it. The
// Agent default section is the other such source: it names the
// selection every session with no logged one resolves to, so an
// externally edited default (another tab, a hand-edited
// settings.yaml) has to reach an open selector too.
if (modelProviderNamespaces().has(name) || name === String(AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE)) {
queue.push(frame({ type: 'host/models-changed' }))
}
}),
ctx.on('llm/adapters-updated', () => {
queue.push(frame({ type: 'host/models-changed' }))
}),
]
return queue.iterate(signal, () => { for (const dispose of disposers) dispose() })
},

View File

@@ -88,7 +88,5 @@ export const hostFrameSchema = z.discriminatedUnion('type', [
// structural contract belongs to the owner package's cordis `Events`
// declaration — the host validated JSON-safety before forwarding.
z.object({ type: z.literal('host/remote-event'), event: z.string().min(1), args: z.array(z.unknown()) }),
z.object({ type: z.literal('host/session-preset-changed'), sessionId: sessionIdSchema, agentPreset: z.string() }),
z.object({ type: z.literal('host/models-changed') }),
z.object({ type: z.literal('stream/error'), error: rpcErrorSchema }),
]) as unknown as z.ZodType<HostFrame>

View File

@@ -150,23 +150,4 @@ export type HostFrame =
* per-event frame variant.
*/
| { type: 'host/remote-event'; event: string; args: JsonValue[] }
/**
* One blank session was recomposed onto another agent preset (the logged
* `agent-preset/selected` commit point, read off the session stream). The
* registry-wide `commands/change` forwarded above cannot stand in for it:
* recomposing re-parents that agent's scope without registering anything,
* so a preset already mounted for another session produces no registry
* change at all. Clients refetch the catalogs this session's composition
* decides (`command.list`, `skill.list`) for this sessionId alone, and fold
* the preset id into their session row — the RPC echo reaches only the
* client that issued the switch, so the row is where every other one learns
* it.
*/
| { type: 'host/session-preset-changed'; sessionId: SessionId; agentPreset: string }
/**
* The provider topology changed (`llm/adapters-updated` passthrough):
* routes registered or dropped, or the configurable directory moved. Pure
* invalidation: clients refetch `llm.providers`/`llm.models`/`session.models`.
*/
| { type: 'host/models-changed' }
| { type: 'stream/error'; error: RpcError }

View File

@@ -4,7 +4,8 @@
* (which providers CAN be configured, and where their settings live) with the
* live route registry; `llm.models` is the session-independent model catalog
* (the same groups as `session.models`, without a per-session selection).
* Both invalidate on the `host/models-changed` frame.
* Clients invalidate from the forwarded `llm/adapters-updated` and
* `settings/document-updated` owner events.
*/
import type { RpcRequest, RpcResponse } from './rpc.ts'