refactor(gui): one plugin-package shape — dshClient manifests, clientBundle preset, purity gate over all nine

Every client plugin package carries dshClient ({platform, inject,
immediately?}) and emits lib/client.js through the shared clientBundle
preset; exports["./client"] points at the bundle. The infrastructure
tier (connection, runtime, ui-theme, i18n, hmr) declares immediately: true
in its manifest — absent means lazy. The bundle purity gate covers all
nine packages: platform modules stay external, INLINE_SAFE wire layers
inline, any other cross-plugin value import is a build error. Migrations
that rule forced: scopeOf became a SessionsService method and
transportError moved into dsh-host-apiproxy's wire layer; the store
engine stays in runtime under a documented temporary exemption
(TODO(webload/store-rehome)).
This commit is contained in:
imccyu
2026-07-23 21:55:59 +08:00
parent b58f0989f9
commit c2e0c16d6a
18 changed files with 152 additions and 125 deletions
@@ -15,14 +15,10 @@
*/
import { Service } from 'cordis'
import type { Context } from 'cordis'
// Value import MUST use the /client subpath: only that specifier is in the
// bundle externals (CLIENT_EXTERNALS), so it resolves to the shared runtime
// module at load time. A bare-specifier value import gets INLINED as a second
// module instance whose private scope-tag Symbol never matches the one
// SessionsService tags contexts with — scopeOf then always returns undefined
// in the browser while unit tests (single-instance path resolution) stay green.
import { scopeOf } from '@deepseek-ai/dsh-client-runtime/client'
import type { Session, SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
// Type-only imports: a plugin-to-plugin value import is a bundle purity
// error, so scope resolution goes through the sessions service (scopeOf
// method) instead of the standalone helper.
import type { Session, SessionId, SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
/** Scope-addressed conversation service (root singleton, provided as `conversation`). */
export class ConversationService extends Service {
@@ -83,11 +79,17 @@ export class ConversationService extends Service {
/** Resolve the caller scope's Session or throw on root contexts. */
private scopedSession(op: string): Session {
const id = scopeOf(this.ctx)
const id = this.scopeId(op)
return this.requireSessions().manager.get(id)
}
/** Read the caller's session scope tag via the sessions service; root contexts fail loud. */
private scopeId(op: string): SessionId {
const id = this.requireSessions().scopeOf(this.ctx)
if (id === undefined) {
throw new Error(`conversation.${op} requires a session scope — address one via ctx.sessions.scope(id).conversation`)
}
return this.requireSessions().manager.get(id)
return id
}
private requireSessions(): SessionsService {