Merge origin/master into worktree/web-plugin-config

Resolves the vendored-package rescope (cordis -> @deepseek-ai/cordis,
schemastery -> @deepseek-ai/schemastery) against this branch's settings
namespaces: the bash capability namespace, its two executors' section
installs, and the new plugin-config client package all move to the scoped
specifiers.
This commit is contained in:
Yichen Jiang
2026-08-10 23:13:11 +08:00
1738 changed files with 11013 additions and 8201 deletions

View File

@@ -91,9 +91,9 @@ If `test:gui` is red on code you did not touch, neither silently fix nor ignore
Bringing up a new `packages/client/<name>` plugin package (ui-workspace is a complete example; ui-sidebar/ui-question are minimal skeletons):
1. **Package skeleton**: `package.json` (`@deepseek-ai/dsh-client-<name>`, exports `.`/`./invariant`/`./client`/`./src/*`/`./package.json`, `dshClient` manifest, `files` list), `tsconfig.json` (extends `tsconfig.base.client.json`, one `references` entry per workspace dependency plus `support/invariants`), `tsdown.config.ts` (`clientBundle(id, ['lib/types/index.js', 'lib/types/invariant.js'])`), `src/index.ts` (empty node-half apply), `src/invariant.ts` (companion with a real reason), `src/css-modules.d.ts` when using CSS Modules, `README.md` with the Model Experience section.
2. **Three registration surfaces, all required** (missing any one fails at a different, later point): the `tsconfig.client.json` aggregate `references` entry; a `dshClient` row in `packages/bundle/web-app/cordis.patch.yml`; a `packages/bundle/web-app/package.json` dependency (profile boots resolve bare row names through the healed `$DSH_HOME/profiles/node_modules` fallback, which mirrors the app's and each bundle's declared dependencies — a row whose package no manifest declares fails to import). `pnpm-workspace.yaml` already globs `packages/*/*`.
3. **dshClient manifest semantics**: `platform: 'web'` always; `immediately: true` only for stage-one-prefetch infrastructure rows. `inject` lists package-name dependency edges — they are **informational only** (preflight display, HMR diffing); they do not sequence entry activation or apply order. Activation order is cordis fiber inject waiting on *services*, nothing else.
1. **Package skeleton**: `package.json` (`@deepseek-ai/dsh-client-<name>`, exports `.`/`./invariant`/`./client`/`./src/*`/`./package.json`, `dsh.client` manifest, `files` list), `tsconfig.json` (extends `tsconfig.base.client.json`, one `references` entry per workspace dependency plus `support/invariants`), `tsdown.config.ts` (`clientBundle(id, ['lib/types/index.js', 'lib/types/invariant.js'])`), `src/index.ts` (empty node-half apply), `src/invariant.ts` (companion with a real reason), `src/css-modules.d.ts` when using CSS Modules, `README.md` with the Model Experience section.
2. **Three registration surfaces, all required** (missing any one fails at a different, later point): the `tsconfig.client.json` aggregate `references` entry; a `dsh.client` row in `packages/bundle/web-app/cordis.patch.yml`; a `packages/bundle/web-app/package.json` dependency (profile boots resolve bare row names through the healed `$DSH_HOME/profiles/node_modules` fallback, which mirrors the app's and each bundle's declared dependencies — a row whose package no manifest declares fails to import). `pnpm-workspace.yaml` already globs `packages/*/*`.
3. **dsh.client manifest semantics**: `platform: 'web'` always; `immediately: true` only for stage-one-prefetch infrastructure rows. `inject` lists package-name dependency edges — they are **informational only** (preflight display, HMR diffing); they do not sequence entry activation or apply order. Activation order is cordis fiber inject waiting on *services*, nothing else.
4. **Registering into another package's slot**: apply order is unconstrained, and a business service is not a declaration barrier. Use `ctx.slots.inject(name, () => ctx.slots.register(...))`; it waits on the actual declaration, removes the contribution when that declaration collapses, reruns after redeclaration, and leaves with the caller's plugin fiber. Return a generator yielding each registration when several contributions must install and roll back atomically. A bare `slots.register` into an undeclared slot remains an error; keep service edges only for services the contribution actually reads.
5. Rebuild the bundle (`pnpm --filter <pkg> bundle`) before probing a live `dsh web` server — the registry serves `lib/client.js`, not sources.

View File

@@ -22,10 +22,12 @@
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"inject": [],
"platform": "web",
"immediately": true
"dsh": {
"client": {
"inject": [],
"platform": "web",
"immediately": true
}
},
"license": "BSD-3-Clause",
"dependencies": {
@@ -35,7 +37,7 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"schemastery": "^3.18.0",
"@deepseek-ai/schemastery": "^3.18.0",
"ws": "^8.21.0"
},
"files": [
@@ -47,12 +49,12 @@
"peerDependencies": {
"@deepseek-ai/dsh-host-webserver": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@types/ws": "^8.18.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
}
}

View File

@@ -3,7 +3,7 @@
* the shared API client, and lets the runtime object layer start the stream
* controller with its sinks.
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { IApiClient } from './api.ts'
import { ConnectionController, type ConnectionConfig, type ConnectionSinks, type ConnectionState } from './connection.ts'
import { FixtureApiClient } from './fixture.ts'

View File

@@ -1,6 +1,6 @@
/** Host HTTP bridge for browser-client RPC. */
import type { Context } from 'cordis'
import z from 'schemastery'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-attachment'
// Activates the httpServer Context merge used below.
import type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-connection'

View File

@@ -1,6 +1,6 @@
/** Host registry and HTTP adapter for generic Connection RPC channels. */
import { Context, Service } from 'cordis'
import { Context, Service } from '@deepseek-ai/cordis'
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
import {
clientRequestSchema,
@@ -32,7 +32,7 @@ interface ConnectionRpcInterceptor {
readonly options: ConnectionRpcHandlerOptions
}
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
/** Host Connection transport and RPC registrations. */
connection: HostConnectionHandle

View File

@@ -2,7 +2,7 @@
* Connection plugin browser-half apply: ctx.connection handle mounting, mode
* selection off the page URL, and the single-consumer stream-loop ownership.
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { apply, type ConnectionHandle } from '../src/client/index.ts'
import type { RpcMessage } from '../src/client/api.ts'

View File

@@ -2,7 +2,7 @@
import { EventEmitter, once } from 'node:events'
import { createServer, request as httpRequest } from 'node:http'
import { PassThrough, Readable } from 'node:stream'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import type { AddressInfo } from 'node:net'
import type { IncomingMessage, ServerResponse } from 'node:http'

View File

@@ -22,28 +22,30 @@
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"inject": [],
"platform": "web",
"immediately": true
"dsh": {
"client": {
"inject": [],
"platform": "web",
"immediately": true
}
},
"license": "BSD-3-Clause",
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "^3.18.0"
},
"peerDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/cordis-plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-client-modules": "^0.0.1",
"@deepseek-ai/dsh-host-webserver": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@cordisjs/plugin-loader": "workspace:^",
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-client-modules": "workspace:^",
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
},
"files": [
"lib/index.js",

View File

@@ -61,8 +61,8 @@
* fiberless (the next rebuilt frame retries from scratch); an apply failure
* leaves a FAILED fiber for the shell's status projection. Both log loudly.
*/
import type { Context } from 'cordis'
import type { Entry, Loader } from '@cordisjs/plugin-loader'
import type { Context } from '@deepseek-ai/cordis'
import type { Entry, Loader } from '@deepseek-ai/cordis-plugin-loader'
import type { PluginsEventFrame } from '../events.ts'
import { EVENTS_ENDPOINT } from '../events.ts'

View File

@@ -8,8 +8,8 @@
*/
import { statSync } from 'node:fs'
import type { ServerResponse } from 'node:http'
import type { Context } from 'cordis'
import z from 'schemastery'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
// Empty type imports carry the clientModuleHost/httpServer Context merges.
import type {} from '@deepseek-ai/dsh-client-modules'
import type {} from '@deepseek-ai/dsh-host-webserver'

View File

@@ -3,7 +3,7 @@
* @module @deepseek-ai/dsh-client-hmr/invariant
*/
import type { Context, Fiber } from 'cordis'
import type { Context, Fiber } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-hmr'

View File

@@ -5,7 +5,7 @@
import { mkdtempSync, rmSync, statSync, unlinkSync, utimesSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { WebBootGraph, ClientModuleHostService } from '@deepseek-ai/dsh-client-modules'
import type { WebRoute, HttpServerService } from '@deepseek-ai/dsh-host-webserver'

View File

@@ -22,13 +22,15 @@
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-runtime"
],
"platform": "web",
"immediately": true
"dsh": {
"client": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-runtime"
],
"platform": "web",
"immediately": true
}
},
"license": "BSD-3-Clause",
"peerDependencies": {
@@ -37,7 +39,7 @@
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"devDependencies": {
@@ -46,12 +48,12 @@
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@types/react": "~18.3.1",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"dependencies": {
"@deepseek-ai/dsh-settings": "workspace:^",
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "^3.18.0"
},
"files": [
"lib/index.js",

View File

@@ -9,7 +9,7 @@
* ui-slots): in THIS unit the map holds only this package's own merges, but
* consumers merge more namespaces in and the intersection keeps them
* string-typed. The rule fires on the narrow-map view, not real redundancy. */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import {
type BoundActions, type LocaleDictOf, type LocaleNamespaceMap, type Translate, type TranslateNS,
} from '@deepseek-ai/dsh-client-ui-slots'
@@ -68,7 +68,7 @@ export interface LocaleSnapshot {
revision: number
}
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
locale: LocaleService
}

View File

@@ -1,6 +1,6 @@
/** Host registration for the browser locale preference. */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { LOCALE_SETTINGS_NAMESPACE, LocaleSettingsSchema } from './locale-settings.ts'

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-locale'

View File

@@ -1,6 +1,6 @@
/** Locale preference stored in the Host user-settings document. */
import z from 'schemastery'
import z from '@deepseek-ai/schemastery'
/** Settings namespace owned by the locale plugin. */
export const LOCALE_SETTINGS_NAMESPACE = 'locale'

View File

@@ -1,7 +1,7 @@
/** locale apply wiring: service + dictionaries provision, declaration-aware
* Language row registration, snapshot projection into the row store, and
* recovery after an HMR collapse of the declaring entry. */
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import {

View File

@@ -1,4 +1,4 @@
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import { Settings, settingsNamespace, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
import {

View File

@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { apply as nodeApply } from '@deepseek-ai/dsh-client-locale'
import { apply as clientApply, COMMON_NS, LocaleService, inject } from '@deepseek-ai/dsh-client-locale/client'
import * as LocaleInvariant from '@deepseek-ai/dsh-client-locale/invariant'

View File

@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { stubSettingsScope, type StubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
import type { LocaleSettings, LocaleSnapshot } from '@deepseek-ai/dsh-client-locale/client'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'

View File

@@ -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/client/modules/README.md
README.md: 7b4c9b72e782dbdbb69d711ae7e022771afebace
README.zh.md: 6420f6324f38979af5428a9ad428f33525009f1f
README.md: a1d578850c2518a85dc32f048768b78caf5ffec4
README.zh.md: 772a4870f7ef6730d9d3d4db434ed771d97984f0

View File

@@ -8,7 +8,7 @@ Lazy CJS model (web2): executing a plugin bundle only REGISTERS its factory (`wi
Resolution branch order (`import(specifier)`): platform seed word → shell instance; memoized record → surface; shell-own static registry (`registerStatic`, app-shell) → module; registered factory → materialize; graph row (`window.__DSH_BOOT__`) → load its external classic script + materialize; anything else throws — the runtime mirror of the build-time bundle purity gate. The synchronous `require` handed to factories walks the same order minus the asynchronous load branch and records observed edges into the module record. `prefetch` is the stage-one arrival hook (script load and factory registration only; concurrent calls share one in-flight task); `invalidate` drops the factory and materialized record so the next prefetch/import reloads the script (the HMR hook).
The Node half scans enabled Loader entries for web `dshClient` packages, resolves each `exports["./client"]`, hashes the built bundle into the boot graph, and serves it with its source map under `/plugins`. Source launch maps host imports to TypeScript source but still consumes this built client export; missing files share one build instruction followed by a package/path list, while unrelated filesystem errors remain separate failures.
The Node half scans enabled Loader entries for web `dsh.client` packages, resolves each `exports["./client"]`, hashes the built bundle into the boot graph, and serves it with its source map under `/plugins`. Source launch maps host imports to TypeScript source but still consumes this built client export; missing files share one build instruction followed by a package/path list, while unrelated filesystem errors remain separate failures.
## Model Experience

View File

@@ -8,7 +8,7 @@
解析分支顺序(`import(specifier)`):平台种子词 → 外壳实例;记忆化记录 → 表层;外壳自身的静态注册表(`registerStatic`,app-shell)→ 模块;已注册 factory → 物化;模块图记录(`window.__DSH_BOOT__`)→ 加载外部 classic script + 物化;其他情况一律抛出异常。这是构建时组合包纯度门禁的运行时镜像。交给 factory 的同步 `require` 采用相同顺序,但不含异步加载分支,并把观察到的边记录到模块记录中。`prefetch` 是第一阶段到达钩子(只加载脚本并注册 factory;并发调用共享一个进行中的任务);`invalidate` 会丢弃 factory 与物化记录,使下一次 prefetch/import 重新加载脚本;它是 HMR(热模块替换)钩子。
Node 侧会扫描已启用的 Loader 配置项以发现 web `dshClient` 包,解析每个 `exports["./client"]`,把构建后的组合包哈希写入启动图,并通过 `/plugins` 提供该文件及其 sourcemap。源码启动会把宿主侧导入映射到 TypeScript 源码,但仍消费这一构建后的客户端导出;缺失文件共享一条构建说明,随后以 package/path list 列出各项,而无关的文件系统错误仍是独立故障。
Node 侧会扫描已启用的 Loader 配置项以发现 web `dsh.client` 包,解析每个 `exports["./client"]`,把构建后的组合包哈希写入启动图,并通过 `/plugins` 提供该文件及其 sourcemap。源码启动会把宿主侧导入映射到 TypeScript 源码,但仍消费这一构建后的客户端导出;缺失文件共享一条构建说明,随后以 package/path list 列出各项,而无关的文件系统错误仍是独立故障。
## 模型体验

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-client-modules",
"description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dshClient scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
"description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -22,10 +22,12 @@
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"platform": "web",
"inject": [],
"immediately": true
"dsh": {
"client": {
"platform": "web",
"inject": [],
"immediately": true
}
},
"scripts": {
"bundle": "tsdown",
@@ -33,10 +35,10 @@
},
"license": "BSD-3-Clause",
"devDependencies": {
"@cordisjs/plugin-loader": "workspace:^",
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
},
"files": [
"lib/index.js",
@@ -46,6 +48,6 @@
],
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
}
}

View File

@@ -9,7 +9,7 @@
* a no-op against the already-registered entry.
* @module @deepseek-ai/dsh-client-modules/client
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { DshWindow } from './manifest.ts'
export { ClientModuleSystem } from './system.ts'

View File

@@ -30,10 +30,10 @@
* composes the wire.
*/
import type {} from 'cordis'
import type {} from '@deepseek-ai/cordis'
import type { ClientModuleSystem } from './system.ts'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
/** The client module system the web shell builds at boot (provided by the `./client` wrapper plugin). */
modules: ClientModuleLoader
@@ -44,7 +44,7 @@ declare module 'cordis' {
* One composed client entry pushed by the host (a graph row). Wire
* single source: the host node half (package root) produces this same shape.
* `immediately` marks stage-one prefetch; `inject` is informational graph
* metadata (the authoritative edges live in each package's dshClient
* metadata (the authoritative edges live in each package's `dsh.client`
* declaration and reach fibers through entry creation).
*/
export interface WebBootEntry {

View File

@@ -1,6 +1,6 @@
/**
* Node half of the client module system (dshClient dual-face package): scans
* the host Loader's entries for `dshClient` packages, composes the
* Node half of the client module system (`dsh.client` dual-face package): scans
* the host Loader's entries for packages declaring `dsh.client`, composes the
* `window.__DSH_BOOT__` entry graph (wire single source: {@link WebBootEntry}
* in `./client/manifest.ts`), serves `/plugins/<id>/client.js` and its source
* map, taps the index render to inject the boot manifest, and provides the
@@ -26,9 +26,9 @@ import { readFile } from 'node:fs/promises'
import type { IncomingMessage, ServerResponse } from 'node:http'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { Service } from 'cordis'
import type { Context } from 'cordis'
import type {} from '@cordisjs/plugin-loader'
import { Service } from '@deepseek-ai/cordis'
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/cordis-plugin-loader'
import type {} from '@deepseek-ai/dsh-host-webserver'
import type { WebBootEntry, WebBootGraph } from './client/manifest.ts'
@@ -36,14 +36,14 @@ export type {
BootManifest, BootModuleRow, BootPluginRow, WebBootEntry, WebBootGraph,
} from './client/manifest.ts'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
/** The web plugin table (provided by the client-modules node half). */
clientModuleHost: ClientModuleHostService
}
}
/** package.json `dshClient` declaration fields, validated one by one after reading the file. */
/** package.json `dsh.client` declaration fields, validated one by one after reading the file. */
interface DshClientDeclaration {
inject?: string[]
platform: string
@@ -51,7 +51,7 @@ interface DshClientDeclaration {
immediately?: boolean
}
/** Resolved package metadata for one dshClient package (cached per name, never expires). */
/** Resolved package metadata for one `dsh.client` package (cached per name, never expires). */
interface PkgMeta {
clientPath: string
inject?: string[]
@@ -105,21 +105,21 @@ interface WebPluginRecord {
clientPath: string
}
/** Narrow an unknown parsed JSON value to the dshClient declaration, throwing on malformed fields. */
/** Narrow an unknown parsed JSON value to the `dsh.client` declaration, throwing on malformed fields. */
function parseDshClient(pkgName: string, value: unknown): DshClientDeclaration | undefined {
if (value === undefined) return undefined
if (typeof value !== 'object' || value === null) {
throw new Error(`client-modules: ${pkgName} has a non-object dshClient declaration`)
throw new Error(`client-modules: ${pkgName} has a non-object dsh.client declaration`)
}
const decl = value as Record<string, unknown>
if (typeof decl.platform !== 'string') {
throw new Error(`client-modules: ${pkgName} dshClient.platform must be a string`)
throw new Error(`client-modules: ${pkgName} dsh.client.platform must be a string`)
}
if (decl.inject !== undefined && (!Array.isArray(decl.inject) || decl.inject.some(i => typeof i !== 'string'))) {
throw new Error(`client-modules: ${pkgName} dshClient.inject must be a string array`)
throw new Error(`client-modules: ${pkgName} dsh.client.inject must be a string array`)
}
if (decl.immediately !== undefined && typeof decl.immediately !== 'boolean') {
throw new Error(`client-modules: ${pkgName} dshClient.immediately must be a boolean`)
throw new Error(`client-modules: ${pkgName} dsh.client.immediately must be a boolean`)
}
return {
platform: decl.platform,
@@ -175,7 +175,7 @@ export function injectBootManifest(html: string, graph: WebBootGraph): string {
}
/**
* The web plugin table service: incremental dshClient scan + wire composition
* The web plugin table service: incremental `dsh.client` scan + wire composition
* + bundle route + index tap. Construction runs the activation scan
* synchronously — a malformed declaration or missing bundle among the
* already-loaded entries aggregates into one loud throw (FAILED fiber; the
@@ -186,7 +186,7 @@ export class ClientModuleHostService extends Service {
private readonly table = new Map<string, WebPluginRecord>()
// Negative verdicts (unresolvable specifier — builtins like cordis:include,
// subpath rows — or a package without a web dshClient declaration) are
// subpath rows — or a package without a web `dsh.client` declaration) are
// cached as null and never expire: plugin-set changes take effect on restart.
private readonly pkgMeta = new Map<string, PkgMeta | null>()
private readonly rebuildListeners = new Set<(id: string, rev: string) => void>()
@@ -342,14 +342,18 @@ export class ClientModuleHostService extends Service {
return null
}
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as Record<string, unknown>
const decl = parseDshClient(pkgName, pkg.dshClient)
const dsh = pkg.dsh
const decl = parseDshClient(
pkgName,
dsh !== null && typeof dsh === 'object' ? (dsh as Record<string, unknown>).client : undefined,
)
if (decl === undefined || decl.platform !== 'web') {
this.pkgMeta.set(pkgName, null)
return null
}
const clientRel = clientExportOf(pkgName, pkg.exports)
if (clientRel === undefined) {
throw new Error(`client-modules: ${pkgName} declares dshClient but exports no "./client" bundle`)
throw new Error(`client-modules: ${pkgName} declares dsh.client but exports no "./client" bundle`)
}
const meta: PkgMeta = {
clientPath: join(dirname(pkgPath), clientRel),

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-modules'

View File

@@ -5,7 +5,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it } from 'vitest'
import type { HttpServerService, WebRoute } from '@deepseek-ai/dsh-host-webserver'
import { ClientModuleHostService } from '../src/index.ts'
@@ -17,8 +17,11 @@ afterEach(() => {
root = undefined
})
/** Create a resolvable dshClient package whose client export points at the returned path. */
function writePackage(packageName: string): string {
/** Create a resolvable package whose client export points at the returned path. */
function writePackage(
packageName: string,
metadata: Record<string, unknown> = { dsh: { client: { platform: 'web' } } },
): string {
root ??= realpathSync(mkdtempSync(join(tmpdir(), 'dsh-client-modules-')))
const pkgRoot = join(root, 'node_modules', ...packageName.split('/'))
const clientPath = join(pkgRoot, 'lib', 'client.js')
@@ -29,7 +32,7 @@ function writePackage(packageName: string): string {
'./client': './lib/client.js',
'./package.json': './package.json',
},
dshClient: { platform: 'web' },
...metadata,
}))
return clientPath
}
@@ -66,6 +69,20 @@ function construct(packageNames: string[]): ClientModuleHostService {
}
describe('client bundle activation', () => {
it('allows sibling dsh roles', () => {
const currentName = '@fixture/current-client-field'
const clientPath = writePackage(currentName, {
dsh: {
bundle: { patch: './cordis.patch.yml' },
client: { platform: 'web' },
profile: { bundles: [] },
},
})
mkdirSync(dirname(clientPath), { recursive: true })
writeFileSync(clientPath, 'module.exports = {}\n')
expect(construct([currentName]).graph().entries.map(entry => entry.id)).toEqual([currentName])
})
it('groups missing bundles under one source-build instruction with a package/path list', () => {
const firstName = '@fixture/missing-first'
const secondName = '@fixture/missing-second'

View File

@@ -22,13 +22,15 @@
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-typert-registry"
],
"platform": "web",
"immediately": true
"dsh": {
"client": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-typert-registry"
],
"platform": "web",
"immediately": true
}
},
"license": "BSD-3-Clause",
"dependencies": {
@@ -54,7 +56,7 @@
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-type-meta": "^0.0.1",
"@deepseek-ai/dsh-typert-registry": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
@@ -62,8 +64,8 @@
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"@types/react": "~18.3.1",
"cordis": "^4.0.0-rc.7",
"schemastery": "^3.18.0"
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"@deepseek-ai/schemastery": "^3.18.0"
},
"files": [
"lib/index.js",

View File

@@ -15,8 +15,8 @@
* — a cold session's host Agent is already disposed while its client actx
* stays alive for history viewing.
*/
import { Context as CordisContext } from 'cordis'
import type { Context, Fiber } from 'cordis'
import { Context as CordisContext } from '@deepseek-ai/cordis'
import type { Context, Fiber } from '@deepseek-ai/cordis'
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import type { TypeRTClientRemote, TypeRTRemoteScopeApi } from '@deepseek-ai/dsh-type-meta'

View File

@@ -7,7 +7,7 @@
* [SessionsPort](./sessions-port.ts). Widening this interface is the
* explicit act of widening what features may do to the sessions domain.
*/
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type {
RpcResult, SessionId, SubagentAddress,
} from '@deepseek-ai/dsh-client-connection/client'

View File

@@ -1,4 +1,4 @@
import { Service } from 'cordis'
import { Service } from '@deepseek-ai/cordis'
/** Shared lifecycle and stable-entry storage for one Conversation Definition registry. */
export abstract class ConversationDefinitionRegistry<Definition> extends Service {

View File

@@ -1,4 +1,4 @@
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { ConversationNodeDefinition } from '../contract/conversation.ts'
import { ConversationDefinitionRegistry } from './definition-registry.ts'

View File

@@ -1,4 +1,4 @@
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { ConversationViewDefinition } from '../contract/conversation.ts'
import { ConversationDefinitionRegistry } from './definition-registry.ts'

View File

@@ -1,5 +1,5 @@
/** Browser runtime services for slots, sessions, workspaces, and connection-stream delivery. */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { ConnectionHandle, SessionId } from '@deepseek-ai/dsh-client-connection/client'
import type { TypeRTContext } from '@deepseek-ai/dsh-type-meta'
import type { MaybeSnapshotSelectorHook, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
@@ -146,7 +146,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
}
}
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Events {
/**
* A slot's definition or registration set changed.

View File

@@ -1,4 +1,4 @@
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type {
HostFrame, IApiClient, MuxFrame, RpcRequest, SessionId,
} from '@deepseek-ai/dsh-client-connection/client'

View File

@@ -14,7 +14,7 @@
* tears its scope down immediately unless it is the staged one, whose scope
* survives frozen (read-only view) until the stage moves on.
*/
import type { Context, Fiber } from 'cordis'
import type { Context, Fiber } from '@deepseek-ai/cordis'
import type {
IApiClient, RpcError, RpcResult, SessionId, SubagentAddress, WorkspaceId,
} from '@deepseek-ai/dsh-client-connection/client'

View File

@@ -1,6 +1,6 @@
// Sessions remain resident after creation so they continue consuming mux frames off-screen.
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import type {

View File

@@ -1,6 +1,6 @@
/** Host-backed settings-namespace synchronization for browser plugins. */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type {
ConnectionHandle, IApiClient, SettingsNamespaceView, SettingsPathOpView,
} from '@deepseek-ai/dsh-client-connection/client'

View File

@@ -14,8 +14,8 @@
* holds this package's 'root' row in this compilation unit, but consumers
* merge keys in; the rule fires on the narrow-map view, not on real
* redundancy. */
import { Service } from 'cordis'
import type { Context } from 'cordis'
import { Service } from '@deepseek-ai/cordis'
import type { Context } from '@deepseek-ai/cordis'
import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
import type {
LocaleFace, OwnerOf, SlotEntryDef, SlotMap, SlotRenderer, SlotRendererHost,

View File

@@ -1,6 +1,6 @@
/** WorkspacesService projects the Workspace object manager for UI consumers. */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type {
DirectoryListing, IApiClient, RpcError,
SessionId, WorkspaceId, WorkspaceView,

View File

@@ -8,7 +8,7 @@
* `keyof SlotMap & string` is the declare-merge key pattern: SlotMap is empty
* in this compilation unit (intersection reads `never`) but consumers merge
* keys in; the rule fires on the empty-map view, not on real redundancy. */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { SlotMap } from '@deepseek-ai/dsh-client-ui-slots'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'

View File

@@ -3,7 +3,7 @@
* connection handle, stream-loop sink wiring into the object layer, and the
* fiber-scoped loop teardown.
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
import type { ConnectionSinks } from '@deepseek-ai/dsh-client-connection/client'

View File

@@ -1,4 +1,4 @@
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import { ConversationEventRegistry } from '../src/client/conversation/event-registry.ts'

View File

@@ -3,7 +3,7 @@
* a fired key must already carry a bumped version (emission follows the
* applied mutation), bogus payloads fail loud, foreign events pass.
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as RuntimeInvariant from '../src/invariant.ts'

View File

@@ -1,4 +1,4 @@
/** Node half: the empty host apply (Loader governance + dshClient discovery placeholder). */
/** Node half: the empty host apply (Loader governance + dsh.client discovery placeholder). */
import { describe, expect, it } from 'vitest'
import { apply } from '../src/index.ts'

View File

@@ -6,14 +6,14 @@
* and a subject-less root dispatch stays unfiltered. Scope-owned listeners
* dispose with the fiber.
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import { createScope, scopeOf } from '../src/client/agents/scope.ts'
const sid = (k: string): SessionId => k as SessionId
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Events {
/**
* Test-only routed probe event.

View File

@@ -6,7 +6,7 @@
* deferral — the stage follows list.current), binding identity, breadcrumb
* projection, create.
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import { SessionCreateError, SessionsService, scopeOf } from '../src/client/sessions/service.ts'

View File

@@ -1,5 +1,5 @@
import { Context } from 'cordis'
import z from 'schemastery'
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { describe, expect, it, vi } from 'vitest'
import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-client-connection/client'
import {

View File

@@ -5,7 +5,7 @@
* contract (double install / not installed / non-root key), store instance
* resolution and lifecycle on the ledger axis, and the entry-unload cascade.
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import type { FC } from 'react'
import type { SlotRendererHost } from '@deepseek-ai/dsh-client-ui-slots'

View File

@@ -4,7 +4,7 @@
* ctx 'session/preset-changed'; each established connection generation →
* ctx 'connection/reset' (the forced cache-invalidation broadcast).
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import type { ConnectionHandle, ConnectionSinks } from '@deepseek-ai/dsh-client-connection/client'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'

View File

@@ -1,4 +1,4 @@
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import type { SessionId, WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-client-connection/client'
import { SessionsService } from '../src/client/sessions/service.ts'

View File

@@ -20,15 +20,15 @@
},
"license": "BSD-3-Clause",
"dependencies": {
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "^3.18.0"
},
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"cordis": "^4.0.0-rc.7"
"@deepseek-ai/cordis": "^4.0.0-rc.7"
},
"files": [
"lib/index.js",

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-schema-form'

View File

@@ -6,7 +6,7 @@
* @module @deepseek-ai/dsh-client-schema-form/model
*/
import Schema from 'schemastery'
import Schema from '@deepseek-ai/schemastery'
/** Live schemastery node; the renderer reads only its structural relations. */
export type SchemaNode = Schema

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import * as SchemaFormInvariant from '@deepseek-ai/dsh-client-schema-form/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import Schema from 'schemastery'
import Schema from '@deepseek-ai/schemastery'
import {
deletePath, getPath, hasPath, nodeAtPath, rehydrateSchema, setPath, validateDraft,
} from '../src/model.ts'

View File

@@ -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/client/test-runtime/README.md
README.md: 455d6f564cea2cb8f88165a8bba1047c762d2fb0
README.zh.md: 7c4bd0e552c71f55e3766a0c64580cc178461310
README.md: d5c0797c37168578f08a08f3d5d57670d7973db0
README.zh.md: 57854213c3a9ea28850665eb26d07bc824c017e8

View File

@@ -8,7 +8,7 @@ The doubles implement the same outward faces features receive through ctx (`Test
Local DOM snapshots: `declare(children)` registers an auto frame whose per-key `<div data-slot>` wrappers are snapshot roots; `renderSlot(key, owner)` returns the slot-local view (container, scoped Testing Library queries, in-place `update(owner)`); a registered snapshot serializer folds CSS-module class hashes (`_frame_a1b2c3` → `frame`) to keep `.snap` files structural and collapses `<svg>` internals to a `data-content` fingerprint. Suites needing a custom page frame use `root.declare(children, Frame)` instead; `mount(plugin)` runs a real fiber with fail-loud service prechecks, and `dispose()` tears down views, feature fibers, minted scopes, and persisted store state on one axis.
Not part of the product plugin graph (no `dshClient`); feature packages depend on it in `devDependencies` only.
Not part of the product plugin graph (no `dsh.client`); feature packages depend on it in `devDependencies` only.
## Model Experience

View File

@@ -8,7 +8,7 @@
局部 DOM 快照:`declare(children)` 注册自动 frame,逐 key 的 `<div data-slot>` 包裹层即快照根;`renderSlot(key, owner)` 返回该 slot 的局部视图(container、限定范围的 Testing Library 查询、原位 `update(owner)`);注册的快照序列化器把 CSS-module 哈希类名折回语义名(`_frame_a1b2c3` → `frame`)保持 `.snap` 只含结构,并把 `<svg>` 内部折叠为 `data-content` 指纹。需要自定义页面 frame 的套件改用 `root.declare(children, Frame)`;`mount(plugin)` 在真实 fiber 上运行并对缺失服务先行报错;`dispose()` 沿单一轴拆除视图、feature fiber、已铸 scope 与持久化 store 状态。
不属于产品插件图(无 `dshClient`);feature 包仅以 `devDependencies` 依赖之。
不属于产品插件图(无 `dsh.client`);feature 包仅以 `devDependencies` 依赖之。
## 模型体验

View File

@@ -30,7 +30,7 @@
"@deepseek-ai/dsh-client-web-react": "^0.0.1",
"@deepseek-ai/dsh-host-apiproxy": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -42,7 +42,7 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@types/react": "~18.3.1",
"@types/react-dom": "~18.3.0",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},

View File

@@ -5,7 +5,7 @@
* declaration, registration, scope, store, inject, rendering, updates, and
* disposal without hand-building the machinery per suite.
*
* Not part of the product plugin graph (no `dshClient`); feature packages
* Not part of the product plugin graph (no `dsh.client`); feature packages
* depend on it in devDependencies only. It copies no SlotCore/renderer/store
* machinery — everything mounts the production implementations.
* @module @deepseek-ai/dsh-client-test-runtime
@@ -14,8 +14,8 @@
* `keyof SlotMap & string` is the declare-merge key pattern (see ui-slots):
* this compilation unit sees only the runtime's 'root' row, but consumer
* programs merge their own keys in; the rule fires on the narrow-map view. */
import { Context, Inject } from 'cordis'
import type { Fiber, Plugin } from 'cordis'
import { Context, Inject } from '@deepseek-ai/cordis'
import type { Fiber, Plugin } from '@deepseek-ai/cordis'
import { createElement, Fragment, useSyncExternalStore } from 'react'
import type { ReactNode } from 'react'
import { act, render, within } from '@testing-library/react'

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-test-runtime'

View File

@@ -1,5 +1,5 @@
/** Test-owned sessions face: the SlotsService host contract over declarative fixtures. */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { AttachmentIdType } from '@deepseek-ai/dsh-attachment'
import { createScope, scopeOf, SessionProvideChannel } from '@deepseek-ai/dsh-client-runtime/client'
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import * as TestRuntimeInvariant from '@deepseek-ai/dsh-client-test-runtime/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'

View File

@@ -32,6 +32,14 @@ const CSS_VIRTUAL_SUFFIX = '.mjs'
*/
export const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|brand)(\/|$)/
/**
* Vendored framework libraries: rescoped into @deepseek-ai, so the gate below
* would read them as plugin packages. They carry no cross-plugin runtime
* identity to share — the framework itself is a platform module (external),
* while these are ordinary libraries a browser bundle inlines.
*/
const VENDORED_LIBRARY = /^@deepseek-ai\/(cosmokit|schemastery)(\/|$)/
/** Generated descriptor/codec contribution with no shared runtime identity. */
const GENERATED_REMOTE = /^@deepseek-ai\/dsh-[a-z0-9]+(?:-[a-z0-9]+)*\/remote$/
@@ -208,6 +216,7 @@ function clientConfig(id: string, entry: string): UserConfig {
resolveId(source: string) {
if (!source.startsWith('@deepseek-ai/')) return null
if (CLIENT_EXTERNALS.includes(source)) return null // platform module: external wins
if (VENDORED_LIBRARY.test(source)) return null // vendored library: inline, no shared identity
if (INLINE_SAFE.test(source) || GENERATED_REMOTE.test(source)) return null // wire contribution: inline is the point
throw new Error(
`client bundle purity: "${source}" is not a platform module (CLIENT_EXTERNALS), an inline-safe wire layer, or a generated /remote contribution — `

View File

@@ -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/client/ui-agent-preset/README.md
README.md: 32a4e7d9e25d3c70d2cc2e8a01c94d093d19659c
README.zh.md: b65a1bdf926f7a34bc3813833ca5ac2d3b6dfabd
README.md: 008066114e9c49e5c74299979e24c27a4c9621c9
README.zh.md: e07d5994ae196cd03be7818fe4ade1aafda9aa55

View File

@@ -26,6 +26,8 @@ Options and the current default both come from one `agentPreset.list` call. The
A locally authored preset is exactly as privileged as the plugins it names, so the list marks `user` rows rather than presenting every preset as shipped and vetted.
Preset files publish one unlocalized `name` and `description`, which Web uses for every `user` row and unknown `system` row. For the four shipped ids (`standard`, `code`, `minimal`, and `cordis`), Web resolves both fields from its active locale only when the roster marks the row `system`; an identically named `user` preset keeps its file metadata.
The row re-reads on `settings/changed` for its own namespace and on `connection/reset`: the roster is a live directory and the default is a settings field, so an external edit or a reconnect can both move it.
## The management section

View File

@@ -26,6 +26,8 @@ chip 以部署默认值打开,其选择是**暂存**的——该界面先于
本地创作的 preset 的权限恰好等于它所引用的插件,因此列表会标注 `user` 行,而不是把每个 preset 都呈现为随附且已审核的。
preset 文件提供一套未国际化的 `name` 与 `description`,Web 将其用于所有 `user` 行和未知的 `system` 行。对于四个随附 id(`standard`、`code`、`minimal` 与 `cordis`),只有名单将该行标记为 `system` 时,Web 才会从当前 locale 解析这两个字段;同名的 `user` preset 仍使用其文件元数据。
本行在自身命名空间的 `settings/changed` 以及 `connection/reset` 时重新读取:名单是一个活动目录,默认值是一项设置,外部编辑与重新连接都可能改变它。
## 管理分区

View File

@@ -22,15 +22,17 @@
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-conversation",
"@deepseek-ai/dsh-client-ui-settings"
],
"platform": "web"
"dsh": {
"client": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-conversation",
"@deepseek-ai/dsh-client-ui-settings"
],
"platform": "web"
}
},
"scripts": {
"bundle": "tsdown",
@@ -47,7 +49,7 @@
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
"@deepseek-ai/dsh-client-web-react": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"devDependencies": {
@@ -62,7 +64,7 @@
"@deepseek-ai/dsh-client-web-react": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@types/react": "~18.3.1",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"files": [

View File

@@ -15,6 +15,7 @@ import { IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
// Type-only: pulls the ui-conversation SlotMap merge (the header actions).
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { AgentPresetSettingsState } from './settings-store.ts'
import { presetDisplayText } from './locales.ts'
import css from './AgentPresetLabel.module.css'
/** Registration-side business face for the header label. */
@@ -53,10 +54,11 @@ export function AgentPresetLabel({
if (preset === undefined) return null
const option = options.find(entry => entry.id === preset)
const text = option === undefined ? undefined : presetDisplayText(option, t)
return (
<span className={css.label} title={option?.description ?? t('headerHint')}>
<span className={css.label} title={text?.description ?? t('headerHint')}>
<IconThinkOutline16 className={css.icon} />
{option?.name ?? preset}
{text?.name ?? preset}
</span>
)
}

View File

@@ -8,7 +8,7 @@ import { useEffect, useState } from 'react'
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { AgentPresetSettingsState } from './settings-store.ts'
import type { AgentPresetSettingsKey } from './locales.ts'
import { presetDisplayText, type AgentPresetSettingsKey } from './locales.ts'
import { PresetMenu } from './PresetMenu.tsx'
import css from './AgentPresetRow.module.css'
@@ -52,11 +52,11 @@ export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetR
// every session shares the host composition — the row simply does not exist.
if (state.status === 'unavailable') return null
const busy = state.status === 'loading' || state.status === 'saving'
// The metadata name is what every other surface shows — the id is the
// addressing, not the label. A preset that names itself nothing falls back
// to its id, which is then all there is to say about it.
// Every preset surface applies the same display-copy rule. The id remains
// addressing rather than a label, except where no display name exists.
const chosen = state.options.find(option => option.id === state.currentValue)
const label = state.currentValue === '' ? t('loading') : (chosen?.name ?? state.currentValue)
const chosenText = chosen === undefined ? undefined : presetDisplayText(chosen, t)
const label = state.currentValue === '' ? t('loading') : (chosenText?.name ?? state.currentValue)
const description: string = state.error ?? t('description')
return (
@@ -69,7 +69,7 @@ export function AgentPresetRow({ load, select, useAgentPreset, t }: AgentPresetR
options={state.options}
selectedId={state.currentValue}
label={label}
userTrustLabel={t('userTrust')}
t={t}
buttonClassName={css.selector}
chevronClassName={css.chevron}
disabled={busy || !state.writable || state.options.length === 0}

View File

@@ -19,6 +19,7 @@ import { IconChevronDownOutline14, IconThinkOutline16, Menu } from '@deepseek-ai
// Type-only: pulls the ui-conversation SlotMap merge (the hero seat).
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { AgentPresetSeatState } from './seat-store.ts'
import { presetDisplayText } from './locales.ts'
import css from './AgentPresetSeat.module.css'
/** Registration-side business face for the hero chip. */
@@ -57,22 +58,26 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
if (state.options.length === 0 || state.current === '') return null
const chosen = state.options.find(option => option.id === state.current)
const chosenText = chosen === undefined ? undefined : presetDisplayText(chosen, t)
return (
<Menu
open={open}
onClose={() => { setOpen(false) }}
items={state.options.map(option => ({
id: option.id,
// Name and description together: the id alone never said what a
// preset does, which is the whole reason the metadata exists.
label: (
<span className={css.item}>
<span className={css.itemName}>{option.name ?? option.id}</span>
<span className={css.itemDesc}>{option.description ?? t('noDescription')}</span>
</span>
),
}))}
items={state.options.map((option) => {
const text = presetDisplayText(option, t)
return {
id: option.id,
// Name and description together: the id alone never says what a
// preset does, which is why the roster carries display copy.
label: (
<span className={css.item}>
<span className={css.itemName}>{text.name}</span>
<span className={css.itemDesc}>{text.description ?? t('noDescription')}</span>
</span>
),
}
})}
selectedId={state.current}
onSelect={(id) => {
setOpen(false)
@@ -91,7 +96,7 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
onClick={() => { setOpen(value => !value) }}
>
<IconThinkOutline16 className={css.seatIcon} />
{chosen?.name ?? state.current}
{chosenText?.name ?? state.current}
<IconChevronDownOutline14 className={css.chevron} />
</button>
)}

View File

@@ -18,7 +18,7 @@ import {
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { draftBlocker, type AgentPresetSectionState } from './section-store.ts'
import type { AgentPresetSettingsKey } from './locales.ts'
import { presetDisplayText, type AgentPresetSettingsKey } from './locales.ts'
import css from './AgentPresetSection.module.css'
/** Registration-side business face for the management section. */
@@ -77,11 +77,13 @@ function CopyDialog({ state, t, actions }: CopyDialogProps): ReactNode {
const draft = state.copy
const blocker = draft === null ? undefined : draftBlocker(draft, state.rows)
const message = draft === null ? null : draft.error ?? (blocker === undefined ? null : t(blocker))
const source = draft === null ? undefined : state.rows.find(row => row.id === draft.from)
const sourceTitle = source === undefined ? draft?.fromTitle : presetDisplayText(source, t).name
return (
<Modal
open={draft !== null}
onClose={() => { actions.cancelCopy() }}
title={draft === null ? t('copyTitle') : `${t('copyTitle')} · ${t('copyOf')} ${draft.fromTitle}`}
title={draft === null ? t('copyTitle') : `${t('copyTitle')} · ${t('copyOf')} ${sourceTitle}`}
closeLabel={t('close')}
description={t('copyIntro')}
className={css.dialog as string}
@@ -143,6 +145,11 @@ function CopyDialog({ state, t, actions }: CopyDialogProps): ReactNode {
export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
const { useAgentPresetSection, t, load } = props
const state = useAgentPresetSection(snapshot => snapshot)
const viewedId = state.view?.id
const viewedRow = viewedId === undefined ? undefined : state.rows.find(row => row.id === viewedId)
const viewedTitle = state.view === null
? ''
: viewedRow === undefined ? state.view.title : presetDisplayText(viewedRow, t).name
useEffect(() => {
void load()
@@ -170,13 +177,15 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
<p className={css.intro}>{t('sectionIntro')}</p>
{state.error === null ? null : <p className={css.error} role="alert">{state.error}</p>}
{([['system', t('builtInGroup')], ['user', t('customGroup')]] as const).map(([trust, heading]) => {
const group = state.rows.filter(row => row.trust === trust)
const group = state.rows
.filter(row => row.trust === trust)
.map(row => ({ row, text: presetDisplayText(row, t) }))
if (group.length === 0) return null
return (
<section key={trust} className={css.group}>
<h3 className={css.groupHead}>{heading}</h3>
<ul className={css.cards}>
{group.map(row => (
{group.map(({ row, text }) => (
<li
key={row.id}
className={row.broken !== undefined
@@ -196,12 +205,12 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
disabled={row.isDefault || row.broken !== undefined}
// Without this the name is the whole card read aloud —
// title, badge, description, id.
aria-label={`${row.broken !== undefined ? t('brokenBadge') : row.isDefault ? t('inUse') : t('setDefault')}: ${row.name ?? row.id}`}
aria-label={`${row.broken !== undefined ? t('brokenBadge') : row.isDefault ? t('inUse') : t('setDefault')}: ${text.name}`}
title={row.broken ?? (row.isDefault ? t('inUse') : t('setDefault'))}
onClick={() => { void props.makeDefault(row.id) }}
>
<span className={css.cardHead}>
<span className={css.cardName}>{row.name ?? row.id}</span>
<span className={css.cardName}>{text.name}</span>
{row.broken !== undefined
? <span className={css.brokenBadge}>{t('brokenBadge')}</span>
: null}
@@ -210,7 +219,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
</span>
{row.isDefault ? <span className={css.inUse}>{t('inUse')}</span> : null}
</span>
<span className={css.cardDesc}>{row.description ?? t('noDescription')}</span>
<span className={css.cardDesc}>{text.description ?? t('noDescription')}</span>
{row.broken === undefined
? null
: <span className={css.cardBrokenReason} role="alert">{row.broken}</span>}
@@ -231,7 +240,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
type="button"
className={css.iconButton}
data-tip={t('view')}
aria-label={`${t('view')}: ${row.name ?? row.id}`}
aria-label={`${t('view')}: ${text.name}`}
onClick={() => { void props.view(row.id) }}
>
<IconBrowseOutline16 />
@@ -243,7 +252,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
type="button"
className={css.iconButton}
data-tip={state.hasDocument ? t('openLocation') : t('showLocation')}
aria-label={`${state.hasDocument ? t('openLocation') : t('showLocation')}: ${row.name ?? row.id}`}
aria-label={`${state.hasDocument ? t('openLocation') : t('showLocation')}: ${text.name}`}
onClick={() => { void props.openLocation(row.id) }}
>
<IconFolderOpenOutline16 />
@@ -256,7 +265,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
data-tip={row.broken !== undefined
? t('brokenNoCopy')
: state.authorable ? t('duplicate') : t('duplicateUnavailable')}
aria-label={`${t('duplicate')}: ${row.name ?? row.id}`}
aria-label={`${t('duplicate')}: ${text.name}`}
onClick={() => { props.beginCopy(row.id) }}
>
<IconCopyOutline16 />
@@ -267,7 +276,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
type="button"
className={`${css.iconButton} ${css.iconDanger}`}
data-tip={t('delete')}
aria-label={`${t('delete')}: ${row.name ?? row.id}`}
aria-label={`${t('delete')}: ${text.name}`}
onClick={() => { props.confirmDelete(row.id) }}
>
<IconTrashOutline16 />
@@ -325,7 +334,7 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
<Modal
open={state.view !== null}
onClose={() => { props.closeView() }}
title={state.view === null ? '' : `${t('view')} · ${state.view.title}`}
title={state.view === null ? '' : `${t('view')} · ${viewedTitle}`}
closeLabel={t('close')}
description={t('composition')}
className={css.dialog as string}

View File

@@ -11,6 +11,7 @@
import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
import type { AgentPresetOption } from './settings-store.ts'
import { presetDisplayText, type AgentPresetSettingsKey } from './locales.ts'
/** What one surface passes to the shared picker. */
export interface PresetMenuProps {
@@ -20,8 +21,8 @@ export interface PresetMenuProps {
selectedId: string
/** Text on the button; the surfaces word a pending roster differently. */
label: string
/** Suffix marking a locally authored preset in the menu. */
userTrustLabel: string
/** Active Web locale lookup. */
t: (key: AgentPresetSettingsKey) => string
/** Class for the trigger button, owned by the calling surface. */
buttonClassName: string | undefined
/** Class for the chevron, owned by the calling surface. */
@@ -42,22 +43,22 @@ export interface PresetMenuProps {
* @returns the menu and its trigger.
*/
export function PresetMenu({
options, selectedId, label, userTrustLabel, buttonClassName, chevronClassName,
options, selectedId, label, t, buttonClassName, chevronClassName,
disabled, open, onOpenChange, onSelect,
}: PresetMenuProps) {
return (
<Menu
open={open}
onClose={() => { onOpenChange(false) }}
items={options.map(option => ({
id: option.id,
// The metadata name is what every surface shows; the id is addressing,
// not a label. A preset that names itself nothing falls back to its id,
// which is then all there is to say about it.
label: option.trust === 'user'
? `${option.name ?? option.id} · ${userTrustLabel}`
: option.name ?? option.id,
}))}
items={options.map((option) => {
const name = presetDisplayText(option, t).name
return {
id: option.id,
// All preset surfaces resolve copy the same way; the id is addressing,
// not a label, except where no display name exists.
label: option.trust === 'user' ? `${name} · ${t('userTrust')}` : name,
}
})}
selectedId={selectedId}
onSelect={(id) => {
onOpenChange(false)

View File

@@ -157,7 +157,8 @@ export function apply(ctx: ClientContext): void {
const label = scope.slots.register({
name: 'conversation.session.header.actions',
id: 'agent-preset',
order: 20,
// Static session context occupies the header's leading negative-order band.
order: -10,
locale: 'settings.agentPreset',
inject: labelInjected,
}, AgentPresetLabel)

View File

@@ -4,6 +4,10 @@
export type AgentPresetSettingsKey =
| 'title' | 'description' | 'loading' | 'error' | 'userTrust' | 'seatHint' | 'headerHint'
| 'nav' | 'sectionIntro' | 'builtIn' | 'setDefault' | 'view'
| 'presetStandardName' | 'presetStandardDescription'
| 'presetCodeName' | 'presetCodeDescription'
| 'presetMinimalName' | 'presetMinimalDescription'
| 'presetCordisName' | 'presetCordisDescription'
| 'duplicate' | 'duplicateUnavailable' | 'delete' | 'presetId' | 'presetIdPlaceholder' | 'copyOf'
| 'displayName' | 'displayNamePlaceholder'
| 'inUse' | 'noDescription' | 'builtInGroup' | 'customGroup'
@@ -30,6 +34,18 @@ export const en: Record<AgentPresetSettingsKey, string> = {
builtIn: 'Built-in',
setDefault: 'Set as default',
view: 'View',
presetStandardName: 'Standard mode',
presetStandardDescription:
'Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.',
presetCodeName: 'Code mode',
presetCodeDescription:
'All Standard mode capabilities, with tools exposed through the Code Mode SDK so the model can combine multi-step operations in one TypeScript program.',
presetMinimalName: 'Minimal mode',
presetMinimalDescription:
'Two-tool coding agent with only bash and str_replace_editor, for benchmarks and minimal reproductions.',
presetCordisName: 'Creator mode',
presetCordisDescription:
'Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance.',
duplicate: 'Duplicate',
duplicateUnavailable: 'This deployment has no writable preset directory',
delete: 'Delete',
@@ -82,6 +98,14 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
builtIn: '内置',
setDefault: '设为默认',
view: '查看',
presetStandardName: '标准模式',
presetStandardDescription: '功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。',
presetCodeName: '代码模式',
presetCodeDescription: '具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。',
presetMinimalName: '极简模式',
presetMinimalDescription: '仅提供 bash 与 str_replace_editor 的双工具编码 Agent,用于基准测试和最小复现。',
presetCordisName: '创造模式',
presetCordisDescription: '用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。',
duplicate: '复制',
duplicateUnavailable: '此部署未配置可写的预设目录',
delete: '删除',
@@ -116,3 +140,53 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
deleteConfirm: '删除',
deleting: '正在删除…',
}
/** Preset roster fields needed to resolve Web display copy. */
export interface PresetDisplaySource {
/** Stable preset id. */
readonly id: string
/** Whether the deployment ships the preset or the user owns it. */
readonly trust: 'system' | 'user'
/** Unlocalized name published by the preset. */
readonly name?: string
/** Unlocalized description published by the preset. */
readonly description?: string
}
/** Display copy resolved for the active Web locale. */
export interface PresetDisplayText {
/** Localized built-in name or the preset's own fallback name. */
readonly name: string
/** Localized built-in description or the preset's own description. */
readonly description?: string
}
interface PresetLocaleKeys {
readonly name: AgentPresetSettingsKey
readonly description: AgentPresetSettingsKey
}
const BUILT_IN_PRESET_KEYS: Readonly<Partial<Record<string, PresetLocaleKeys>>> = {
standard: { name: 'presetStandardName', description: 'presetStandardDescription' },
code: { name: 'presetCodeName', description: 'presetCodeDescription' },
minimal: { name: 'presetMinimalName', description: 'presetMinimalDescription' },
cordis: { name: 'presetCordisName', description: 'presetCordisDescription' },
}
/**
* Resolve preset display copy without making user-authored metadata translatable.
* @param preset - roster row whose copy is being rendered.
* @param t - active Web locale lookup.
* @returns localized copy for a known shipped preset, otherwise file metadata.
*/
export function presetDisplayText(
preset: PresetDisplaySource,
t: (key: AgentPresetSettingsKey) => string,
): PresetDisplayText {
const keys = preset.trust === 'system' ? BUILT_IN_PRESET_KEYS[preset.id] : undefined
if (keys !== undefined) return { name: t(keys.name), description: t(keys.description) }
return {
name: preset.name ?? preset.id,
...preset.description === undefined ? {} : { description: preset.description },
}
}

View File

@@ -2,7 +2,7 @@
* Agent-preset surface plugin, node half. The empty apply exists so the plugin
* appears in the host cordis.yml / Loader; the browser half ships the
* General-settings row through exports["./client"], discovered from the
* package.json dshClient declaration.
* package.json dsh.client declaration.
*/
/** Host plugin body — no host-side behavior for this surface plugin. */

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-agent-preset'

View File

@@ -5,7 +5,7 @@
* that are already showing, so a default set from one converges the other.
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
@@ -304,7 +304,7 @@ describe('ui-agent-preset apply', () => {
expect(chip.component).toBe(AgentPresetSeat)
const label = slots.entries('conversation.session.header.actions')[0]!
expect(label.component).toBe(AgentPresetLabel)
expect(label.options).toMatchObject({ id: 'agent-preset', order: 20 })
expect(label.options).toMatchObject({ id: 'agent-preset', order: -10 })
await fiber.dispose()
expect(slots.entries('conversation.hero.agentPreset')).toHaveLength(0)
expect(slots.entries('conversation.session.header.actions')).toHaveLength(0)

View File

@@ -90,7 +90,7 @@ describe('the General-settings row', () => {
const actions = renderRow()
await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
expect(screen.getByRole('button').textContent).toContain('标准模式')
expect(screen.getByRole('button').textContent).toContain(en.presetStandardName)
})
it('marks a locally authored option as local', () => {
@@ -102,7 +102,7 @@ describe('the General-settings row', () => {
// list says which rows are local rather than presenting all as vetted.
expect(screen.getByText(`mine · ${en.userTrust}`)).toBeTruthy()
// The shipped one carries no marker; only local rows are called out.
expect(screen.getAllByText('标准模式')).toHaveLength(2)
expect(screen.getAllByText(en.presetStandardName)).toHaveLength(2)
})
it('falls back to the id for a preset that published no name', () => {
@@ -128,6 +128,12 @@ describe('the General-settings row', () => {
expect(screen.getByText('bare')).toBeTruthy()
})
it('shows the selected id until a stale roster contains it', () => {
renderRow({ currentValue: 'arriving', options: [] })
expect(screen.getByRole('button').textContent).toContain('arriving')
})
it('writes the picked preset and closes the menu', () => {
const actions = renderRow()
fireEvent.click(screen.getByRole('button'))
@@ -194,7 +200,7 @@ describe('the new-session chip', () => {
const actions = renderSeat()
await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
expect(screen.getByRole('button').textContent).toContain('标准模式')
expect(screen.getByRole('button').textContent).toContain(en.presetStandardName)
expect(screen.getByRole('button').getAttribute('title')).toBe(en.seatHint)
})
@@ -205,7 +211,7 @@ describe('the new-session chip', () => {
// The id alone never said what a preset does; the description is the
// whole reason a preset can publish metadata at all.
expect(screen.getByText('完整的编码 agent。')).toBeTruthy()
expect(screen.getByText(en.presetStandardDescription)).toBeTruthy()
// A preset that published none still reads as a row, with its id standing
// in for the name.
expect(screen.getByText(en.noDescription)).toBeTruthy()
@@ -218,6 +224,12 @@ describe('the new-session chip', () => {
expect(screen.getByRole('button').textContent).toContain('mine')
})
it('shows the staged id until a stale roster contains it', () => {
renderSeat({ current: 'arriving' })
expect(screen.getByRole('button').textContent).toContain('arriving')
})
it('stages the picked preset and closes the menu', () => {
const actions = renderSeat()
fireEvent.click(screen.getByRole('button'))
@@ -267,7 +279,7 @@ describe('the session-header label', () => {
await waitFor(() => { expect(load).toHaveBeenCalledTimes(1) })
// A control here would promise a switch the host refuses outright.
expect(screen.queryByRole('button')).toBeNull()
expect(screen.getByTitle('完整的编码 agent。').textContent).toBe('标准模式')
expect(screen.getByTitle(en.presetStandardDescription).textContent).toBe(en.presetStandardName)
})
it('falls back to the id, and to the generic hint, when metadata is absent', () => {

View File

@@ -1,7 +1,7 @@
/** The package's node half: an empty host body and an explained empty invariant companion. */
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as AgentPresetInvariant from '@deepseek-ai/dsh-client-ui-agent-preset/invariant'

View File

@@ -0,0 +1,33 @@
/** Web-localized copy for the four shipped presets and file copy for every other row. */
import { describe, expect, it } from 'vitest'
import { en, presetDisplayText, zh } from '../src/client/locales.ts'
const translate = (bundle: typeof en) => (key: keyof typeof en): string => bundle[key]
describe('preset display copy', () => {
it.each([
['standard', 'presetStandardName', 'presetStandardDescription'],
['code', 'presetCodeName', 'presetCodeDescription'],
['minimal', 'presetMinimalName', 'presetMinimalDescription'],
['cordis', 'presetCordisName', 'presetCordisDescription'],
] as const)('localizes the shipped %s preset in English and Chinese', (id, nameKey, descriptionKey) => {
const preset = { id, trust: 'system' as const, name: 'file name', description: 'file description' }
expect(presetDisplayText(preset, translate(en)))
.toEqual({ name: en[nameKey], description: en[descriptionKey] })
expect(presetDisplayText(preset, translate(zh)))
.toEqual({ name: zh[nameKey], description: zh[descriptionKey] })
})
it('keeps file metadata for user and unknown system presets', () => {
const fileCopy = { name: '我的标准', description: '团队自己的 preset。' }
expect(presetDisplayText({ id: 'standard', trust: 'user', ...fileCopy }, translate(en)))
.toEqual(fileCopy)
expect(presetDisplayText({ id: 'deployment-extra', trust: 'system', ...fileCopy }, translate(en)))
.toEqual(fileCopy)
expect(presetDisplayText({ id: 'bare', trust: 'user' }, translate(en)))
.toEqual({ name: 'bare' })
})
})

View File

@@ -85,13 +85,13 @@ describe('the preset list', () => {
await waitFor(() => { expect(actions.load).toHaveBeenCalledTimes(1) })
})
it('shows the published name and description, falling back to the id', () => {
it('shows resolved copy for built-ins and falls back to custom ids', () => {
renderSection()
// The name is what a picker reads; the id stays visible as the key the
// Display copy is what a picker reads; the id stays visible as the key the
// composition and the session header actually carry.
expect(screen.getByText('标准模式')).toBeTruthy()
expect(screen.getByText('完整的编码 agent。')).toBeTruthy()
expect(screen.getByText(en.presetStandardName)).toBeTruthy()
expect(screen.getByText(en.presetStandardDescription)).toBeTruthy()
const mine = rowFor('mine')
expect(within(mine).getAllByText('mine').length).toBeGreaterThan(0)
expect(within(mine).getByText(en.noDescription)).toBeTruthy()
@@ -134,7 +134,7 @@ describe('the preset list', () => {
it('picks a preset by clicking its card, and the one in use is inert', () => {
const actions = renderSection()
const inUse = within(rowFor('standard')).getByRole('button', { name: `${en.inUse}: 标准模式` })
const inUse = within(rowFor('standard')).getByRole('button', { name: `${en.inUse}: ${en.presetStandardName}` })
expect(inUse).toHaveProperty('disabled', true)
fireEvent.click(inUse)
@@ -150,8 +150,8 @@ describe('the preset list', () => {
// the point. A custom preset is edited in its files, so its row leads
// there instead; there is no editor for either.
const standard = rowFor('standard')
expect(within(standard).getByRole('button', { name: `${en.view}: 标准模式` })).toBeTruthy()
expect(within(standard).queryByRole('button', { name: `${en.openLocation}: 标准模式` })).toBeNull()
expect(within(standard).getByRole('button', { name: `${en.view}: ${en.presetStandardName}` })).toBeTruthy()
expect(within(standard).queryByRole('button', { name: `${en.openLocation}: ${en.presetStandardName}` })).toBeNull()
const mine = rowFor('mine')
expect(within(mine).getByRole('button', { name: `${en.openLocation}: mine` })).toBeTruthy()
expect(within(mine).queryByRole('button', { name: `${en.view}: mine` })).toBeNull()
@@ -161,13 +161,13 @@ describe('the preset list', () => {
renderSection()
expect(within(rowFor('mine')).getByRole('button', { name: `${en.delete}: mine` })).toBeTruthy()
expect(within(rowFor('standard')).queryByRole('button', { name: `${en.delete}: 标准模式` })).toBeNull()
expect(within(rowFor('standard')).queryByRole('button', { name: `${en.delete}: ${en.presetStandardName}` })).toBeNull()
})
it('disables duplication when nothing is writable, and says why', () => {
renderSection({ authorable: false })
const duplicate = within(rowFor('standard')).getByRole('button', { name: `${en.duplicate}: 标准模式` })
const duplicate = within(rowFor('standard')).getByRole('button', { name: `${en.duplicate}: ${en.presetStandardName}` })
expect(duplicate).toHaveProperty('disabled', true)
expect(duplicate.getAttribute('data-tip')).toBe(en.duplicateUnavailable)
})
@@ -205,7 +205,7 @@ describe('the preset list', () => {
// There is no readable composition to offer; the reason on the card is
// the whole story a shipped row can tell.
const standard = rowFor('standard')
expect(within(standard).queryByRole('button', { name: `${en.view}: 标准模式` })).toBeNull()
expect(within(standard).queryByRole('button', { name: `${en.view}: ${en.presetStandardName}` })).toBeNull()
expect(within(standard).getByRole('alert').textContent).toContain('not valid YAML')
})
@@ -232,7 +232,7 @@ describe('the preset list', () => {
fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.setDefault}: mine` }))
fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.openLocation}: mine` }))
fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.duplicate}: mine` }))
fireEvent.click(within(rowFor('standard')).getByRole('button', { name: `${en.view}: 标准模式` }))
fireEvent.click(within(rowFor('standard')).getByRole('button', { name: `${en.view}: ${en.presetStandardName}` }))
expect(actions.makeDefault).toHaveBeenCalledWith('mine')
expect(actions.openLocation).toHaveBeenCalledWith('mine')
@@ -311,7 +311,7 @@ describe('the copy dialog', () => {
const actions = renderSection({ copy: draft })
const dialog = screen.getByRole('dialog')
expect(dialog.getAttribute('aria-label')).toBe(`${en.copyTitle} · ${en.copyOf} 标准模式`)
expect(dialog.getAttribute('aria-label')).toBe(`${en.copyTitle} · ${en.copyOf} ${en.presetStandardName}`)
expect(within(dialog).getByText(en.copyIntro)).toBeTruthy()
fireEvent.change(within(dialog).getByPlaceholderText(en.presetIdPlaceholder), { target: { value: 'my-agent' } })
fireEvent.change(within(dialog).getByPlaceholderText(en.displayNamePlaceholder), { target: { value: '我的模式' } })
@@ -374,11 +374,17 @@ describe('the read-only viewer', () => {
renderSection({ view: { id: 'standard', title: '标准模式', content: '- id: tool-bash\n' } })
const dialog = screen.getByRole('dialog')
expect(dialog.getAttribute('aria-label')).toBe(`${en.view} · 标准模式`)
expect(dialog.getAttribute('aria-label')).toBe(`${en.view} · ${en.presetStandardName}`)
expect(within(dialog).getByText(en.composition)).toBeTruthy()
expect(within(dialog).getByText(/tool-bash/).textContent).toBe('- id: tool-bash\n')
})
it('keeps the loaded title when the viewed row leaves the roster', () => {
renderSection({ view: { id: 'retired', title: 'Retired mode', content: '- id: tool-bash\n' } })
expect(screen.getByRole('dialog').getAttribute('aria-label')).toBe(`${en.view} · Retired mode`)
})
it('closes through the controller', () => {
const actions = renderSection({ view: { id: 'standard', title: '标准模式', content: '- id: x\n' } })

View File

@@ -22,14 +22,16 @@
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-slash",
"@deepseek-ai/dsh-client-ui-conversation"
],
"platform": "web"
"dsh": {
"client": {
"inject": [
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-slash",
"@deepseek-ai/dsh-client-ui-conversation"
],
"platform": "web"
}
},
"scripts": {
"bundle": "tsdown",
@@ -48,7 +50,7 @@
"@deepseek-ai/dsh-client-ui-slash": "^0.0.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"devDependencies": {
@@ -62,7 +64,7 @@
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@types/react": "~18.3.1",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"files": [

View File

@@ -28,7 +28,7 @@ export type {
} from './contract.ts'
export type { CommandKey } from './locales.ts'
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
command: CommandService
}

View File

@@ -7,8 +7,8 @@
* addresses the session's agent by sessionId — sessions are always
* agent-backed.
*/
import { Service } from 'cordis'
import type { Context } from 'cordis'
import { Service } from '@deepseek-ai/cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { ConnectionHandle, SessionId } from '@deepseek-ai/dsh-client-connection/client'
import type { ClientContext, ISessions } from '@deepseek-ai/dsh-client-runtime/client'
import type {

View File

@@ -1,7 +1,7 @@
/**
* Command UI plugin, node half. Pure UI plugin: the empty apply exists so
* the plugin appears in the host cordis.yml / Loader; the browser half ships
* via exports["./client"], discovered through the package.json dshClient
* via exports["./client"], discovered through the package.json dsh.client
* declaration. The host command registry itself mounts separately
* (bootHost + CommandService).
*/

View File

@@ -4,7 +4,7 @@
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-command'

View File

@@ -6,7 +6,7 @@
* scope → popupFor; unknown id fails loud), both fold up on fiber disposal
* (HMR safety), and the service satisfies the frozen CommandServiceContract.
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import { createScope, scopeOf, SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'

View File

@@ -7,7 +7,7 @@
* payload, the scoped consume-token dispatch, per-session popupFor
* lifecycle, and the directory invalidation event subscriptions.
*/
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import { createScope, scopeOf } from '@deepseek-ai/dsh-client-runtime/client'
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'

View File

@@ -22,14 +22,16 @@
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-layout"
],
"platform": "web"
"dsh": {
"client": {
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-layout"
],
"platform": "web"
}
},
"scripts": {
"bundle": "tsdown",
@@ -39,7 +41,7 @@
"dependencies": {
"@deepseek-ai/dsh-settings": "workspace:^",
"clsx": "^2.0.0",
"schemastery": "^3.18.0"
"@deepseek-ai/schemastery": "^3.18.0"
},
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
@@ -57,7 +59,7 @@
"@deepseek-ai/dsh-llm-retry": "^0.0.1",
"@deepseek-ai/dsh-token-meter": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"devDependencies": {
@@ -84,7 +86,7 @@
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-tool-todo": "workspace:^",
"@types/react": "~18.3.1",
"cordis": "^4.0.0-rc.7",
"@deepseek-ai/cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"files": [

View File

@@ -1,5 +1,5 @@
/** Registers the conversation components, shared store, and service callbacks. */
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { resolveSlotLabel, type BoundActions } from '@deepseek-ai/dsh-client-ui-slots'
import {
bindSettingsScope, resolveWorkspacePath, type ISessions, type SessionId,

View File

@@ -1,4 +1,4 @@
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { NS } from '../locales.ts'
import { AssistantNodeView } from './AssistantNodeView.tsx'
import { CommandNodeView, ManualCompactionNodeView } from './CommandNodeView.tsx'

View File

@@ -38,7 +38,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
'conversation.session': { kind: 'single'; scope: 'session' }
/** Strict-session header above the resident conversation scrollport. */
'conversation.session.header': { kind: 'single'; scope: 'session' }
/** Session-header actions contributed by feature plugins. */
/**
* Session-header actions contributed by feature plugins. Entries render
* by ascending `order`; negative values are reserved for static session
* context that precedes interactive actions.
*/
'conversation.session.header.actions': { kind: 'list'; scope: 'session'; owner: ConversationHeaderActionOwnerProps }
/**
* The conversation view ring: one list entry per view tab (chat here;
@@ -224,7 +228,7 @@ export interface ChatFileMentions {
forClosing(owner: TurnTailOwnerProps): MarkdownFileMentions | undefined
}
declare module 'cordis' {
declare module '@deepseek-ai/cordis' {
interface Context {
/** Prose file-mention provider (ui-deliverables); reach via ctx.get — optional. */
chatFileMentions: ChatFileMentions

View File

@@ -1,4 +1,4 @@
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type {
AssistantBlock, AssistantMessageNode, ConversationLocation, ConversationMatch,
ConversationNodeContext, ConversationNodeDefinition,

View File

@@ -1,4 +1,4 @@
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import type {
ChatConversationViewNode, ChatLocationNodeIndex, ChatNodeStore, ChatSnapshot,
ConversationLocation, ConversationNode, ConversationTimelineSnapshot,

Some files were not shown because too many files have changed in this diff Show More