diff --git a/AGENTS.md b/AGENTS.md index 4b0a421..e56d917 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,8 +30,7 @@ src/usage-remote.ts Host half of the usage Remote: `commandcodeUsage` src/client/index.ts Browser client entry: registers the "Command Code" settings page (settings.section, id `commandcode`) and the Models-page provider card - (settings.models.provider-card, key `llm-commandcode`), - and installs the friendly image-gate error wrapper. + (settings.models.provider-card, key `llm-commandcode`). src/client/settings.ts Settings-page controller (scope + credentials + staged form; React-free so node tests can drive it). src/client/usage.ts Account-card controller (Remote fetch lifecycle + @@ -56,7 +55,6 @@ src/client/locales.ts zh/en copy + LocaleNamespaceMap augmentation. tests/adapter.test.ts Core adapter unit tests (node:test + tsx). tests/accounts.test.ts Account-pool rotation tests. tests/commands.test.ts getUsage + command tests (stubbed fetch, no network). -tests/client.test.ts sessions-wrapper tests. tests/settings.test.ts settings-page controller tests. tests/card.test.ts Models-page provider-card tests (posture logic, key write path, login affordance parity). @@ -80,10 +78,20 @@ tsdown.config.ts Build config (tsdown -> lib/, ESM, .d.ts + client.js). `react/jsx-runtime`, `@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-ui-slots`, `@deepseek-ai/dsh-client-web-react`, `@deepseek-ai/dsh-client-ui-primitives`, `@deepseek-ai/dsh-client-schema-form`, `@deepseek-ai/dsh-client-ui-attachment`) - and host-shipped client bundles resolvable from the loader's module table - (e.g. `@deepseek-ai/dsh-client-runtime/client`). The settings page binds the + and host-shipped platform modules resolvable from the loader's module table + (e.g. `@deepseek-ai/dsh-client-ui-primitives`, which is published on npm). + The 0.1.2 adapter originally imported `createSnapshotStore` from the + `@deepseek-ai/dsh-client-store` platform module, but that package was never + published and the host's module table does not materialize it — so any + `require("@deepseek-ai/dsh-client-store")` in `lib/client.js` makes the dsh + loader reject the whole plugin at import time. The store is therefore + **vendored** at `src/client/snapshot-store.ts` (a tiny + `getSnapshot`/`subscribe`/`set` triple); the client bundle has no + `dsh-client-store` require, so it loads on any host. Flip back to importing + it from the platform module only once upstream actually ships the package. + The settings page binds the `llm-commandcode` namespace through `ctx.settingsScope` and writes the API - key through `connection.api.credentials` under the `COMMANDCODE_API_KEY` + key through `ctx.remote.credentials` under the `COMMANDCODE_API_KEY` reference — never through the settings section, so the key literal cannot leak into a settings document. `tests/settings.test.ts` pins this contract. - **Models-page provider card (`settings.models.provider-card`)**: a keyed diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a4ae0c..2f75f23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +### Changed + +- **Adapted the provider to DeepSeek Harness 0.1.2-alpha.1.** Tool-call IDs now use the current branded chunk vocabulary, credential operations use Typert Remotes, the browser client consumes the new settings/models surfaces, and package metadata no longer references the removed `dsh-client-runtime` package. + +### Fixed + +- **Restored plugin loadability after the 0.1.2-alpha.1 adaptation.** The adaptation pointed the browser client at the new `@deepseek-ai/dsh-client-store` platform module for its snapshot store, but that package was never published to the registry and the host's client-module table does not materialize it — so every `require("@deepseek-ai/dsh-client-store")` in the built client bundle made the dsh loader reject the plugin at import time ("missed the module table"). The snapshot store is now a tiny vendored utility (`getSnapshot`/`subscribe`/`set`) inlined into the client bundle, so `lib/client.js` no longer references `dsh-client-store` at all and the plugin loads on any host. No `0.9.1` retraction is needed: the published `0.9.1` never referenced `dsh-client-store` (it still used `dsh-client-runtime`); only the unreleased adaptation did. +- **Prevented the legacy friendly image-session error wrapper from blocking 0.1.2 client startup.** The pre-0.1.2 wrapper assumed `connection.api.sessions`; 0.1.2 replaces that façade with `remote.session`, so the optional copy rewrite now safely skips itself when the legacy sessions face is absent instead of failing the whole plugin with `Cannot read properties of undefined (reading 'sessions')`. Legacy hosts still receive the localized rewrite. + ## [0.9.1] - 2026-08-28 ### Added diff --git a/package-lock.json b/package-lock.json index 5644d01..8de3aa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6", - "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6", @@ -24,6 +23,7 @@ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6", "@deepseek-ai/dsh-settings": "^0.1.0-rc.6", "@deepseek-ai/dsh-timeout": "^0.1.0-rc.6", + "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6", "@deepseek-ai/schemastery": "^3.18.1", "@types/node": "^22.0.0", "@types/react": "^18.3.1", @@ -36,22 +36,28 @@ }, "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", - "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6", - "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6", - "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6", - "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6", - "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6", - "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6", - "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6", - "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6", - "@deepseek-ai/dsh-commands": "^0.1.0-rc.6", - "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6", - "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6", - "@deepseek-ai/dsh-llm": "^0.1.0-rc.6", - "@deepseek-ai/dsh-settings": "^0.1.0-rc.6", - "@deepseek-ai/dsh-timeout": "^0.1.0-rc.6", + "@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-attachment": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-commands": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-credentials": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-launch-environment": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-llm": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-settings": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.1", "@deepseek-ai/schemastery": "^3.18.1", "react": "^18.2.0" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-client-ui-renderer": { + "optional": true + } } }, "node_modules/@deepseek-ai/cordis": { @@ -311,6 +317,7 @@ "integrity": "sha512-qprJMsVPLK2CyJmmCHwECJZlbM7XNG5yKr2Zf/yKYRyz0+QNJfHlIt+4C32tkjFFIq+Dx3gU5YaUQaLXxsR4/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@deepseek-ai/dsh-agent": "^0.1.0-rc.6", "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6", @@ -662,6 +669,7 @@ "integrity": "sha512-HdjBWcQ6spwA8B5dScRhndLQNiKoQTkq36O+kmJ80B9V4z08K5XrJwAInH9cCxJ91CqNzx/sxCdDds0oZDH1Pw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@deepseek-ai/schemastery": "^3.18.1" }, @@ -2766,6 +2774,7 @@ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -4262,6 +4271,7 @@ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } @@ -4406,6 +4416,7 @@ "integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "use-sync-external-store": "1.2.0" }, diff --git a/package.json b/package.json index d19cfa3..d559e35 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,11 @@ "client": { "inject": [ "@deepseek-ai/dsh-client-connection", - "@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-locale", + "@deepseek-ai/dsh-client-ui-renderer", "@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-api-remotes" ], - "external": [ - "@deepseek-ai/dsh-client-runtime/client" - ], "platform": "web" } }, @@ -78,31 +75,35 @@ "license": "MIT", "peerDependencies": { "@deepseek-ai/cordis": "^4.0.1", - "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-commands": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-timeout": "^0.1.0-rc.6 || ^0.1.1-rc.1", - "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6 || ^0.1.1-rc.1", + "@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-attachment": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-commands": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-credentials": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-launch-environment": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-llm": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-settings": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-timeout": "^0.1.2-alpha.1", + "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.1", "@deepseek-ai/schemastery": "^3.18.1", "react": "^18.2.0" }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-client-ui-renderer": { + "optional": true + } + }, "devDependencies": { "@deepseek-ai/cordis": "^4.0.1", "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6", "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6", - "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6", "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6", diff --git a/src/adapter.ts b/src/adapter.ts index 970ac86..a120192 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -30,7 +30,6 @@ import type { AttachmentStore, ImageAttachmentRef } from '@deepseek-ai/dsh-attac import { attributionHeaders, - CallId, LlmAdapter, LlmError, ReasoningEffortId, @@ -49,6 +48,11 @@ import { } from '@deepseek-ai/dsh-llm' import { RETRY_MAX_DELAY_MS } from './accounts.ts' +/** Brand a provider-issued tool-call id through the active harness chunk vocabulary. */ +function toolCallId(id: string): Extract['id'] { + return id as Extract['id'] +} + // --------------------------------------------------------------------------- // Static capability snapshot (from the official command-code@1.37.0 bundled // model catalog, dist/cli.mjs). The Provider API does not expose reasoning @@ -1782,11 +1786,11 @@ export class CommandCodeAdapter ctx.locale.getLocale().active) } @@ -188,13 +188,22 @@ export function apply(ctx: Context): void { // `slots.inject` waits for the declaration). ctx.effect(() => ctx.locale.register('settings.commandcode', { zh, en }), 'dsh-commandcode-provider: page copy') - const api = ctx.get('connection').api - const hostDescription = ctx.get('connection').hostDescription const scope = ctx.settingsScope.bind>({ namespace: COMMANDCODE_NS }) - const controller = new CommandCodeSettingsController(scope, { credentials: api.credentials }, hostDescription) + const credentialRemote = (ctx.remote as unknown as { credentials: SettingsPageApi['credentials'] }).credentials + // Preserve hostDescription (cwd placeholder) when the Host still exposes it; + // 0.1.2 may no longer provide it, so the field is optional and the page + // degrades to no placeholder. + const hostDescription = (ctx.get('connection') as unknown as { hostDescription?: { getSnapshot(): { cwd?: string } | undefined; subscribe(fn: () => void): () => void } } | undefined)?.hostDescription + const controller = new CommandCodeSettingsController(scope, { credentials: credentialRemote }, hostDescription) ctx.effect(() => () => controller.dispose(), 'dsh-commandcode-provider: settings controller') const store = createSnapshotStore(controller.state()) controller.subscribe(() => store.set(controller.state())) + ctx.effect( + () => (ctx.remote as unknown as { + $on(event: 'credentials/reference-updated', listener: (ref: string) => void): () => void + }).$on('credentials/reference-updated', () => { controller.refreshCredentials() }), + 'dsh-commandcode-provider: credential invalidations', + ) // The account-usage card + login panel: mount the shared Remote contribution // (one mount carries every endpoint this plugin serves — report and login — @@ -355,5 +364,6 @@ export const inject: readonly string[] = [ 'locale', 'connection', 'remote', + 'remote.credentials', 'settingsScope', ] diff --git a/src/client/sessions.ts b/src/client/sessions.ts index 0754e59..316b9c4 100644 --- a/src/client/sessions.ts +++ b/src/client/sessions.ts @@ -110,15 +110,30 @@ export function withFriendlyImageError( } } -/** The connection handle shape we read `api.sessions` from. */ +/** + * The pre-0.1.2 connection handle that exposed the shared session API under + * `connection.api.sessions`. In 0.1.2 the connection service became a + * transport/generation handle and model selection moved to `remote.session`, + * so this legacy field is deliberately optional. + */ export interface ConnectionLike { - api: { sessions: SessionsLike } + api?: { sessions?: SessionsLike } } -/** Install the wrapper on a connection's shared sessions face. */ +/** + * Install the friendly-error wrapper when a legacy sessions face is present. + * + * @returns whether the wrapper was installed. The rewrite is only UX polish; + * a 0.1.2 connection has no `api.sessions`, and its absence must never block + * the plugin from mounting its settings, credential, or usage surfaces. + */ export function installFriendlyImageError( connection: ConnectionLike, getLocale: () => LocaleId, -): void { - connection.api.sessions = withFriendlyImageError(connection.api.sessions, getLocale) +): boolean { + const api = connection.api + const sessions = api?.sessions + if (api === undefined || sessions === undefined || typeof sessions.selectModel !== 'function') return false + api.sessions = withFriendlyImageError(sessions, getLocale) + return true } diff --git a/src/client/settings.ts b/src/client/settings.ts index dfb3bb9..f6c1e10 100644 --- a/src/client/settings.ts +++ b/src/client/settings.ts @@ -26,22 +26,50 @@ * the React component renders. */ -import type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client' - /** The settings namespace the plugin registers (host half, src/index.ts). */ export const COMMANDCODE_NS = 'llm-commandcode' /** Default credential reference the plugin resolves when none is named. */ export const DEFAULT_API_KEY_REF = 'COMMANDCODE_API_KEY' +/** The settings-scope snapshot fields consumed by this controller. */ +export interface SettingsScopeSnapshot { + status: 'loading' | 'ready' | 'unavailable' + value: T | undefined + base: unknown + user: unknown + revision: number | undefined + writable: boolean + mode: 'host' | 'memory' +} + +/** Current settings-scope service face used without importing a browser plugin value. */ +export interface SettingsScope { + getSnapshot(): SettingsScopeSnapshot + subscribe(listener: () => void): () => void + set(field: string, value: unknown): Promise + unset(field: string): Promise +} + +/** Result envelope returned by one current Typert Remote call. */ +interface RemoteResult { + ok: boolean + value?: T + error?: { message: string } +} + +/** Credential facts returned without exposing the credential value. */ +interface CredentialInfo { + configured: boolean + writable: boolean +} + /** The narrow slice of the wire face this controller needs. */ export interface SettingsPageApi { + /** Credential methods exposed by the current Typert Remote namespace. */ credentials: { - describe(request: { refs: string[] }): Promise<{ - result: { ok: true; value: { credentials: Record } } | { ok: false; error: { message: string } } - }> - set(request: { ref: string; value: string }): Promise<{ result: { ok: true; value?: unknown } | { ok: false; error: { message: string } } }> - /** Remove one stored credential entirely (the Host's `credentials.unset`). */ - unset(request: { ref: string }): Promise<{ result: { ok: true; value?: unknown } | { ok: false; error: { message: string } } }> + describe(refs: string[]): Promise>> + set(ref: string, value: string): Promise> + unset(ref: string): Promise> } } @@ -664,8 +692,8 @@ export class CommandCodeSettingsController { /** Write one account's key, then re-read the Host's credential states. */ private async writeKeyTo(ref: string, value: string): Promise { try { - const response = await this.api.credentials.set({ ref, value }) - if (!response.result.ok) return false + const response = await this.api.credentials.set(ref, value) + if (!response.ok) return false } catch { return false } @@ -682,14 +710,14 @@ export class CommandCodeSettingsController { ] let response: Awaited> try { - response = await this.api.credentials.describe({ refs }) + response = await this.api.credentials.describe(refs) } catch { return } - if (!response.result.ok) return + if (!response.ok) return let changed = false for (const ref of refs) { - const view = response.result.value.credentials[ref] + const view = response.value?.[ref] const next = { configured: view?.configured ?? false, writable: view?.writable ?? true, @@ -780,8 +808,8 @@ export class CommandCodeSettingsController { /** Unset one stored credential, then re-read the Host's credential states. */ private async unsetKey(ref: string): Promise { try { - const response = await this.api.credentials.unset({ ref }) - if (!response.result.ok) return false + const response = await this.api.credentials.unset(ref) + if (!response.ok) return false } catch { return false } diff --git a/src/client/snapshot-store.ts b/src/client/snapshot-store.ts new file mode 100644 index 0000000..cef6820 --- /dev/null +++ b/src/client/snapshot-store.ts @@ -0,0 +1,45 @@ +/** + * A tiny observable snapshot store — the `getSnapshot` / `subscribe` / `set` + * triple React's `useSyncExternalStore` consumes through the harness slot kit + * (the host builds each slot's `useFoo(selector)` hook from one of these). + * + * Vendored on purpose. The 0.1.2 adapter used to import this from the + * `@deepseek-ai/dsh-client-store` platform module, but that package was never + * published to the registry and the host's client-module table does not + * materialize it — so any `require("@deepseek-ai/dsh-client-store")` in the + * built client bundle makes the dsh loader reject the whole plugin at import + * time. The utility is small enough (and behaviorally identical to the + * platform one — notify-on-set, stable-while-unchanged) that inlining it + * removes a hard dependency on a not-yet-shipped module without changing the + * slot-hook contract. When the platform module lands upstream we can flip + * back to importing it; until then this keeps the plugin loadable. + */ + +/** Mutable observable snapshot consumed by slot hooks. */ +export interface SnapshotStore { + getSnapshot(): T + subscribe(listener: () => void): () => void + set(value: T): void +} + +/** Create one observable snapshot store. */ +export function createSnapshotStore(initial: T): SnapshotStore { + let snapshot = initial + const listeners = new Set<() => void>() + return { + getSnapshot: () => snapshot, + subscribe(listener: () => void) { + listeners.add(listener) + return () => { + listeners.delete(listener) + } + }, + set(value: T) { + if (Object.is(value, snapshot)) return + snapshot = value + for (const listener of listeners) { + listener() + } + }, + } +} diff --git a/src/usage-wire.ts b/src/usage-wire.ts index 3ea445a..424f3f7 100644 --- a/src/usage-wire.ts +++ b/src/usage-wire.ts @@ -219,6 +219,11 @@ export const USAGE_HOST_CONTRIBUTION = { package: USAGE_REMOTE_PACKAGE, face: 'host' as const, schemas: [], + // 0.1.2's Typert registry requires every Host contribution to carry its + // reflection model. This hand-written Remote deliberately has no generated + // reflection exports, so use the official empty-model form rather than a + // cast that leaves registry inspection with `model: undefined`. + model: { services: [], events: [], objects: [] }, invocations: [USAGE_REPORT_DESCRIPTOR], } diff --git a/tests/card.test.ts b/tests/card.test.ts index 8121005..3d1daca 100644 --- a/tests/card.test.ts +++ b/tests/card.test.ts @@ -78,20 +78,20 @@ function makeScope(init: { function makeApi(init: { store?: Map }) { const store = init.store ?? new Map() const credentials = { - describe: async ({ refs }: { refs: string[] }) => { + describe: async (refs: string[]) => { const map: Record = {} for (const ref of refs) { map[ref] = { configured: store.has(ref), writable: true } } - return { result: { ok: true as const, value: { credentials: map } } } + return { ok: true as const, value: map } }, - set: async ({ ref, value }: { ref: string; value: string }) => { + set: async (ref: string, value: string) => { store.set(ref, value) - return { result: { ok: true as const, value: {} } } + return { ok: true as const, value: undefined } }, - unset: async ({ ref }: { ref: string }) => { + unset: async (ref: string) => { store.delete(ref) - return { result: { ok: true as const, value: {} } } + return { ok: true as const, value: undefined } }, } return { credentials, store } diff --git a/tests/client.test.ts b/tests/client.test.ts index 860ebad..019efe0 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -15,7 +15,7 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' -import { withFriendlyImageError, isImageSessionRejection } from '../src/client/sessions.ts' +import { installFriendlyImageError, withFriendlyImageError, isImageSessionRejection } from '../src/client/sessions.ts' import { PLUGIN_VERSION } from '../src/client/version.ts' // --------------------------------------------------------------------------- @@ -133,6 +133,22 @@ test('withFriendlyImageError() preserves success results', async () => { assert.deepEqual(result, original) }) +test('installFriendlyImageError() skips the 0.1.2 connection shape without throwing', () => { + // dsh 0.1.2 replaces the legacy `connection.api.sessions` façade with a + // transport/generation handle; selectModel now lives on `remote.session`. + // The optional copy-rewrite must not keep this plugin fiber pending or fail. + assert.equal(installFriendlyImageError({} as never, () => 'zh'), false) + assert.equal(installFriendlyImageError({ api: {} } as never, () => 'zh'), false) +}) + +test('installFriendlyImageError() still wraps a legacy connection sessions face', async () => { + const connection = { api: { sessions: sessionsReturning(imageGateError('x')) } } + assert.equal(installFriendlyImageError(connection as never, () => 'en'), true) + const result = await connection.api.sessions.selectModel({ sessionId: 's', provider: 'commandcode', model: 'x' }) + assert.equal(result.result.ok, false) + assert.match(result.result.error.message, /session already contains images/i) +}) + // --------------------------------------------------------------------------- // PLUGIN_VERSION (settings-page footer) // --------------------------------------------------------------------------- diff --git a/tests/settings.test.ts b/tests/settings.test.ts index 0a32d7b..c2daa53 100644 --- a/tests/settings.test.ts +++ b/tests/settings.test.ts @@ -78,7 +78,7 @@ function makeApi(init: { configured?: boolean; writable?: boolean; store?: Map { + describe: async (refs: string[]) => { const credentialsMap: Record = {} for (const ref of refs) { credentialsMap[ref] = { @@ -86,17 +86,17 @@ function makeApi(init: { configured?: boolean; writable?: boolean; store?: Map { - if (init.failSet === true) return { result: { ok: false as const, error: { message: 'write refused' } } } + set: async (ref: string, value: string) => { + if (init.failSet === true) return { ok: false as const, error: { message: 'write refused' } } store.set(ref, value) - return { result: { ok: true as const, value: {} } } + return { ok: true as const, value: undefined } }, - unset: async ({ ref }: { ref: string }) => { - if (init.failUnset === true) return { result: { ok: false as const, error: { message: 'unset refused' } } } + unset: async (ref: string) => { + if (init.failUnset === true) return { ok: false as const, error: { message: 'unset refused' } } store.delete(ref) - return { result: { ok: true as const, value: {} } } + return { ok: true as const, value: undefined } }, } return { credential, credentials, store } @@ -593,16 +593,12 @@ test('a failed key write aborts the save before the accounts list lands', async const store = new Map() const api = { credentials: { - describe: async ({ refs }: { refs: string[] }) => ({ - result: { - ok: true as const, - value: { - credentials: Object.fromEntries(refs.map((ref) => [ref, { configured: store.has(ref), writable: true }])), - }, - }, + describe: async (refs: string[]) => ({ + ok: true as const, + value: Object.fromEntries(refs.map((ref) => [ref, { configured: store.has(ref), writable: true }])), }), // The credentials domain rejects every write. - set: async () => ({ result: { ok: false as const, error: { message: 'read-only' } } }), + set: async () => ({ ok: false as const, error: { message: 'read-only' } }), }, } const { controller } = makeController({ scope, api: api as unknown as ReturnType }) diff --git a/tests/usage-wire.test.ts b/tests/usage-wire.test.ts index 287d0ef..2f41251 100644 --- a/tests/usage-wire.test.ts +++ b/tests/usage-wire.test.ts @@ -189,6 +189,7 @@ test('the shared descriptor targets the commandcodeUsage service endpoint', () = test('host and client contributions carry the same descriptor object', () => { assert.equal(USAGE_HOST_CONTRIBUTION.face, 'host') + assert.deepEqual(USAGE_HOST_CONTRIBUTION.model, { services: [], events: [], objects: [] }) assert.equal(USAGE_HOST_CONTRIBUTION.invocations[0], USAGE_REPORT_DESCRIPTOR) assert.equal(USAGE_REMOTE_CONTRIBUTION.descriptors[0], USAGE_REPORT_DESCRIPTOR) assert.equal(USAGE_HOST_CONTRIBUTION.package, USAGE_REMOTE_CONTRIBUTION.package) diff --git a/tsdown.config.ts b/tsdown.config.ts index 1db527a..816a8a3 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -50,7 +50,6 @@ const client = defineConfig({ '@deepseek-ai/cordis', 'react', 'react/jsx-runtime', - '@deepseek-ai/dsh-client-runtime/client', '@deepseek-ai/dsh-client-ui-primitives', ], outputOptions: {