From a988c9a8fbe04125d01615132dc0b2c8a5dc9729 Mon Sep 17 00:00:00 2001 From: bowenliang123 Date: Wed, 7 Oct 2026 23:37:52 +0800 Subject: [PATCH] feat(compat): drop dsh 0.1.5-rc.1, floor the support matrix at 0.1.7-rc.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The supported set is now the two V4-generation lines (0.1.7-rc.2+ and 0.2.0-rc.2+): the manifest's dshReleases, the peer floors (>=0.1.7-rc.2, cordis ^4.0.4 — both lines vendor 4.0.4), the devDependency pins, the baseline matrix, the release workflow's tag fetch, and the compatibility docs. With every supported line on session format V4, the V3-only paths leave: the fold reads the tool result's first-class message alone (source.callId pairing, message.isError — the admission validation refuses any row whose data.error outlives it); the settings.register host face module is gone (the Config-form entry schema serves the preferences); the client drops the settingsScope/settings.plugin.item card seat, the retired sessions.open jump fallback, the legacy icon spellings, and the settings-dialog leg of the settings jump; the history page's tool/result mapper reads the V4 message (its V3 wrapper reads were dropping result content on supported lines); the cache probe rides the 2-arg cachedSnapshot face. The baseline gate stays: a below-floor harness composes the fallback units and the upgrade prompt, and an undetectable version still fails open. Verified: pnpm run lint:fix && pnpm run test && pnpm run build green (1918 tests, 100% per-file coverage); the compat matrix passes against the real dsh sources at both baseline tags; live on dsh 0.2.0-rc.2 the plugin composes, the Plugins-page preference card renders and round-trips a write, the insights warm-up fills the heatmap, and the session jump and Context tab work end to end. --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/workflows/release.yml | 7 +- README.md | 11 +- docs/compatibility.md | 72 +++--- docs/settings.png | Bin 28332 -> 135972 bytes package.json | 21 +- pnpm-lock.yaml | 240 +++++++++--------- pnpm-workspace.yaml | 24 +- src/client/components/fileCard.tsx | 4 +- src/client/components/richText.tsx | 2 +- src/client/components/settingsCard.tsx | 71 +----- src/client/dockMeasure.ts | 10 +- src/client/historyPage.ts | 13 +- src/client/i18n.ts | 8 - src/client/index.ts | 40 +-- src/client/insightPage.ts | 7 +- src/client/primitives.ts | 63 ++--- src/client/revealScroll.ts | 5 +- src/client/services.ts | 54 ++-- src/client/settings.ts | 13 +- src/client/settingsJump.ts | 108 ++------ src/client/sidebar.ts | 25 +- src/client/styles/base.css | 21 +- src/client/styles/settings.css | 15 +- src/client/viewFocus.ts | 8 +- src/host/backfill.ts | 34 +-- src/host/balance.ts | 52 ++-- src/host/compat.ts | 6 +- src/host/config.ts | 12 +- src/host/fold.ts | 81 +++--- src/host/index.ts | 2 - src/host/logShapes.ts | 16 +- src/host/pricing.ts | 11 +- src/host/settings.ts | 57 ----- src/host/timeline.ts | 12 +- src/shared/fileOps.ts | 2 +- src/shared/types.ts | 11 +- src/shared/version.ts | 2 +- tests/baselines.ts | 202 ++++----------- tests/client/compat/clientFaces.spec.ts | 6 +- tests/client/components/agentGraph.spec.ts | 8 +- tests/client/components/browser.spec.ts | 10 +- tests/client/components/contextView.spec.ts | 4 +- tests/client/components/events.spec.ts | 4 +- tests/client/components/images.spec.ts | 2 +- tests/client/components/overviewPanel.spec.ts | 2 +- tests/client/components/settingsCard.spec.ts | 193 ++------------ tests/client/components/upgradeGate.spec.ts | 14 +- tests/client/dockMeasure.spec.ts | 4 +- tests/client/helpers/harness.ts | 2 +- tests/client/historyPage.spec.ts | 24 +- tests/client/index.spec.ts | 174 +++---------- tests/client/primitives.spec.ts | 38 ++- tests/client/services.spec.ts | 32 +-- tests/client/settings.spec.ts | 6 +- tests/client/settingsJump.spec.ts | 200 ++++----------- tests/compat/bundle.spec.ts | 2 +- tests/compat/matrix.spec.ts | 49 ++-- tests/compat/staging.ts | 47 ++-- tests/host/backfill.spec.ts | 26 +- tests/host/balance.spec.ts | 56 +--- tests/host/compat/registry.spec.ts | 13 - tests/host/compat/registryDriver.ts | 1 - .../@deepseek-ai/dsh/package.json | 2 +- .../dsh-session-projection/package.json | 2 +- .../@deepseek-ai/dsh-session/package.json | 2 +- .../@deepseek-ai/dsh/package.json | 2 +- .../{fold-v3.spec.ts => fold-dialect.spec.ts} | 4 +- tests/host/fold-events.spec.ts | 14 +- tests/host/fold-ops.spec.ts | 15 +- tests/host/fold-surface.spec.ts | 64 +++-- tests/host/fold-timing.spec.ts | 15 +- tests/host/helpers/events.ts | 29 +-- tests/host/index.spec.ts | 5 +- tests/host/pricing.spec.ts | 11 +- tests/host/settings.spec.ts | 161 ------------ tests/host/version.spec.ts | 18 +- tests/shared/estimate.spec.ts | 6 +- tests/shared/version.spec.ts | 19 +- 79 files changed, 809 insertions(+), 1821 deletions(-) delete mode 100644 src/host/settings.ts rename tests/host/{fold-v3.spec.ts => fold-dialect.spec.ts} (99%) delete mode 100644 tests/host/settings.spec.ts diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 91c0034..0affbb5 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -28,7 +28,7 @@ body: attributes: label: DeepSeek Harness version / DeepSeek Harness 版本 description: The `@deepseek-ai/dsh` version in use, if relevant. / 如相关,请填写正在使用的 `@deepseek-ai/dsh` 版本。 - placeholder: e.g. 0.1.5-rc.1 + placeholder: e.g. 0.2.0-rc.2 validations: required: false - type: textarea diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 968c66e..6716c9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,11 +21,7 @@ jobs: - name: Check the DeeSeek Harness plugin # `next` = the newest published generation (0.2.0-rc.2), whose npm # dependency closure is complete; `latest` still serves the other - # supported line (0.1.7-rc.2). The 0.1.5-rc line is transitively broken - # on npm right now: web-app 0.1.5-rc.3 was published without the - # dsh-client-ui-sidebar-documentpreview rc.3 it requests, and every - # ^0.1.5-rc range resolves to it. Revisit when upstream republishes a - # complete stable closure. + # supported line (0.1.7-rc.2). uses: bowenliang123/dsh-plugin-checker@v1 # Reads the pinned pnpm version from `packageManager` in package.json. @@ -61,7 +57,6 @@ jobs: git init -q git remote add origin https://github.com/deepseek-ai/deepseek-harness.git git fetch --depth 1 -q origin \ - refs/tags/dsh-v0.1.5-rc.1:refs/tags/dsh-v0.1.5-rc.1 \ refs/tags/dsh-v0.1.7-rc.2:refs/tags/dsh-v0.1.7-rc.2 \ refs/tags/dsh-v0.2.0-rc.2:refs/tags/dsh-v0.2.0-rc.2 diff --git a/README.md b/README.md index 5774c88..86b1a47 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [`dsh-context`](https://www.npmjs.com/package/dsh-context) provides full context lifecycle management features. - **Context Insights page** — the cross-session overview as a first-level sidebar panel (beside Plugins and Automation tasks): KPI band with a 7-day token/cost usage chart, activity heatmap, aggregate composition ring, and filterable session cards that jump straight into any session. - **Context tab** — an UI context dashboard for DeepSeek Harness's context stats, composition, trend, events, and messages. -- **Context panel** — the same dashboard as a right-sidebar tab (dsh 0.1.5-rc.1+): pick **Context** on the sidebar's guide page and the panel opens beside the chat. +- **Context panel** — the same dashboard as a right-sidebar tab: pick **Context** on the sidebar's guide page and the panel opens beside the chat. - **`/context` command** — the slash command shows the context model for current context composition and recent context evolution. ## Install / Update @@ -154,7 +154,7 @@ One row per touched file — read, written, or searched — aggregated up to whi - **Every mode counts** — native tools, the Minimal preset's `str_replace_editor`, and the nested calls inside PTC `run_code` programs are all folded into per-tool rows. - **Searches land on real files** — matched files get their own ops rows with hit counts. - **Click a row** to expand its full operation log — every op jumps straight to the exact tool result in the Context browser. -- **Click a file name** to open its preview in the right Sidebar (dsh 0.1.5-rc.1+), exactly as the built-in Files sidebar does — the same viewer, the same tab-per-file behavior. On a harness without that column the name opens on your system as before. +- **Click a file name** to open its preview in the right Sidebar, exactly as the built-in Files sidebar does — the same viewer, the same tab-per-file behavior. On a harness without that column the name opens on your system as before. ### 🕸 Agent Network — the family portrait @@ -174,10 +174,7 @@ A centered dialog opens with the **Current Composition** card and the **Context ## ⚙️ Settings -The **Context** preferences card holds this plugin's per-user settings — default placement, trend granularity (Step/Turn), trend mode (Total/Delta), the trend duration curve (shown by default), tool and File Activity sort, and the sidebar insights entry. Where it lives depends on your dsh release: - -- **dsh 0.1.7+** — the sidebar **Plugins** entry → the **dsh-context** bundle's page → its **Configuration** section (the card is served by the entry's live Config form). -- **older releases** — **Settings → Plugins → Plugin configuration** → the **Context** card. +The **Context** preferences card holds this plugin's per-user settings — default placement, trend granularity (Step/Turn), trend mode (Total/Delta), the trend duration curve (shown by default), tool and File Activity sort, and the sidebar insights entry. It lives on the **Plugins** page: the sidebar **Plugins** entry → the **dsh-context** bundle's page → its **Configuration** section (the card is served by the entry's live Config form). In-chart and in-card toggles stay per-view and never overwrite the stored preference. @@ -186,7 +183,7 @@ In-chart and in-card toggles stay per-view and never overwrite the stored prefer ## Good to know - **Estimates vs actuals** — category figures use dsh's own fixed-density heuristic (the same one as its built-in token meter); the pinned trend details show provider-reported actuals next to them, and the Token card pairs its ≈-estimated composition shares with the provider-exact billed total. -- **Compatibility** — works on `@deepseek-ai/dsh` **0.1.5-rc.1+** (the `0.1.5` line from rc.1, the `0.1.7` line from rc.2, the `0.2.0` line from rc.2), across the V3 (`0.1.5-alpha.x+`) and V4 (`0.1.6/0.1.7/0.2.0+`) session-log generations, from one shape-driven code path. The per-release matrix and how it is verified: [docs/compatibility.md](docs/compatibility.md). +- **Compatibility** — works on `@deepseek-ai/dsh` **0.1.7-rc.2+** (the `0.1.7` line from rc.2 and the `0.2.0` line from rc.2), the V4 session-log generation, from one shape-driven code path. The per-release matrix and how it is verified: [docs/compatibility.md](docs/compatibility.md). - **I18n** — UI in English and 简体中文. ## Like it? diff --git a/docs/compatibility.md b/docs/compatibility.md index ce17af1..0c92fbe 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -2,27 +2,26 @@ dsh-context declares per-release compatibility with `@deepseek-ai/dsh` in its package manifest (`dsh.compatibility.dshReleases`). This page records what is actually verified for each declared release, and how. -Last verified: **2026-09-29** (plugin `dsh-context@0.60.0` source tree, dsh `0.2.0-rc.2`). +Last verified: **2026-10-07** (plugin `dsh-context@0.65.0` source tree, dsh `0.2.0-rc.2`). ## Supported dsh releases | dsh release | Session log | Declared | Automated seam matrix | Disposable-profile install / uninstall | | --- | --- | --- | --- | --- | -| `0.1.5-rc.1` | V3 | compatible | ✅ baseline `v0.1.5-rc.1` | ✅ install OK → 1 composed row → uninstall OK → 0 rows (verified 2026-09-10) | | `0.1.7-rc.2` | V4 | compatible | ✅ baseline `v0.1.7-rc.2` | ✅ install OK → uninstall OK (verified manually 2026-09-28) | | `0.2.0-rc.2` | V4 | compatible | ✅ baseline `v0.2.0-rc.2` | ✅ install OK → 1 composed row → uninstall OK → 0 rows (`dsh-context@0.60.0`, verified manually 2026-09-29) | -The automated seam matrix runs for every row on every `pnpm test`. The disposable-profile column is a manual, per-release check: each release's CLI was installed from npm into a temporary `DSH_HOME` (the real `~/.dsh` is never touched) — `0.1.5-rc.1` on 2026-09-10 (a stale mirror can 404 the harness's own dependency closure before the plugin is even considered), then `0.1.7-rc.2` and `0.2.0-rc.2` on 2026-09-28 and 2026-09-29. The check pins the plugin build it installs: a bare-name install resolves through the profile's package manager, whose release-age policy can lag a just-published build (on `0.2.0-rc.2` the bare name resolved to `0.59.1`, so the row records the explicit `dsh-context@0.60.0`). The only pnpm complaints there are peers the harness supplies from its own runtime module tree — none version-incompatible, and the install is never refused. +The automated seam matrix runs for every row on every `pnpm test`. The disposable-profile column is a manual, per-release check: each release's CLI was installed from npm into a temporary `DSH_HOME` (the real `~/.dsh` is never touched) — `0.1.7-rc.2` and `0.2.0-rc.2` on 2026-09-28 and 2026-09-29. The check pins the plugin build it installs: a bare-name install resolves through the profile's package manager, whose release-age policy can lag a just-published build (on `0.2.0-rc.2` the bare name resolved to `0.59.1`, so the row records the explicit `dsh-context@0.60.0`). The only pnpm complaints there are peers the harness supplies from its own runtime module tree — none version-incompatible, and the install is never refused. -The supported range is the three lines above: the `0.1.5` line from `rc.1`, the `0.1.7` line from `rc.2` (the first release of that line with a complete npm dependency closure; `rc.2` adds `startsSeries` to the first `request/header` of a resumed series and the `developer/message` tool-registry events; `startsSeries` is inert to the fold, while registry messages contribute to injected context), and the `0.2.0` line from `rc.2` — the continuation of the V4 Config-form generation, every seam the plugin rides spelled as at `0.1.7-rc.2` (cordis `4.0.4`, the same projection-registry contract, platform module table, client seats, settings transport, and durable-event vocabulary; still the V4 log generation, with no `v4-to-v5` migration in the session-format tree). Everything older — the `0.1.2`/`0.1.3` generations, the `0.1.1` line, and the alpha previews — was supported through `dsh-context@0.55.x` and is no longer in the support matrix; on such a harness the plugin's baseline gate composes the fallback units and shows the upgrade prompt instead of folding. +The supported range is the two lines above: the `0.1.7` line from `rc.2` (the first release of that line with a complete npm dependency closure; `rc.2` adds `startsSeries` to the first `request/header` of a resumed series and the `developer/message` tool-registry events; `startsSeries` is inert to the fold, while registry messages contribute to injected context), and the `0.2.0` line from `rc.2` — the continuation of the V4 Config-form generation, every seam the plugin rides spelled as at `0.1.7-rc.2` (cordis `4.0.4`, the same projection-registry contract, platform module table, client seats, settings transport, and durable-event vocabulary; still the V4 log generation, with no `v4-to-v5` migration in the session-format tree). Everything older — the `0.1.5` line (supported through `dsh-context@0.65.x`), the `0.1.2`/`0.1.3` generations, the `0.1.1` line, and the alpha previews — is no longer in the support matrix; on such a harness the plugin's baseline gate composes the fallback units and shows the upgrade prompt instead of folding. ## Harness-side compatibility admission (0.1.7+) -From `0.1.7` the harness enforces a plugin's own `peerDependencies` at startup and install: a bundle whose `@deepseek-ai/dsh*` peer ranges do not satisfy the running version (semver, `includePrerelease: true`) is skipped at composition, refused at install, and only loads with an exact-version exemption (`dsh plugin allow-version`). This plugin's dsh peers are all `>=0.1.5-rc.1`, which every supported release — all three prerelease lines included — satisfies, so the plugin composes normally with no exemption. The `0.2.0` line keeps the same gate. +Every supported line enforces a plugin's own `peerDependencies` at startup and install: a bundle whose `@deepseek-ai/dsh*` peer ranges do not satisfy the running version (semver, `includePrerelease: true`) is skipped at composition, refused at install, and only loads with an exact-version exemption (`dsh plugin allow-version`). This plugin's dsh peers are all `>=0.1.7-rc.2`, which both supported lines satisfy, so the plugin composes normally with no exemption. The gate predates them (`0.1.7` introduced it), so an older harness without it (the `0.1.5`/`0.1.6` lines) still loads the plugin — and the plugin's own baseline gate below is what composes the fallback units there. ## Baseline gate -The declared floor is enforced at runtime, not only documented. At startup the host resolves the running harness's version — first from the module tree the plugin is bound to, then from the `$DSH_HOME/profiles/node_modules` mirror — and compares it against `0.1.5-rc.1` (channel order: release > rc > beta > alpha): +The declared floor is enforced at runtime, not only documented. At startup the host resolves the running harness's version — first from the module tree the plugin is bound to, then from the `$DSH_HOME/profiles/node_modules` mirror — and compares it against `0.1.7-rc.2` (channel order: release > rc > beta > alpha): - **Below the floor** — fallback projection units that fold nothing and serve zeroed data: the Context tab keeps its (empty) cards while a modal names both versions and urges the update. - **At or above the floor** — the real projection units. @@ -30,33 +29,30 @@ The declared floor is enforced at runtime, not only documented. At startup the h The mirror reflects whichever installation last booted a CLI profile, which need not be the running one (a packaged desktop client never refreshes it), so it never outranks the running module tree. Implementation: `src/host/version.ts`, `src/host/fallback.ts`, `src/shared/version.ts`. -## Session-log generations +## Session-log generation -The supported range spans three baselines and two durable-log generations, and the plugin folds both generations from one shape-driven code path: the spellings meet only in `src/host/logShapes.ts` (`firstTokenTimeOfStream`, `replaceRangeOf`, `isTokenChunk`), and `applyTimeline` runs a single code path over all of them: +Every supported line is the V4 durable-log generation, and the plugin folds it from one shape-driven code path (`src/host/logShapes.ts` holds the vocabulary readers — `firstTokenTimeOfStream`, `replaceRangeOf`, `isTokenChunk` — and `applyTimeline` runs a single code path over them): -| Seam | V3 (`0.1.5-rc.1+`) | V4 (`0.1.6/0.1.7+`, `0.2.0+`) | -| --- | --- | --- | -| System prompt | `system/message` surface node (a `request/header` carrying `header.system` is rejected outright) | same as V3 | -| First token | embedded `assistant/message.data.stream` (also `assistant/attempt.data.stream`) — packed delta runs plus raw `chunk` records | same as V3 | -| Replacement endpoints | `{ startSeq, endSeq }` | same as V3 | -| Nested PTC dispatch | `tool/ptc-dispatch` | same as V3 | -| Tool result | `tool-result` wrapper block, `role: 'user'` | `role: 'tool'`, lifted `message.toolCallId`/`message.isError`, direct content (no wrapper block) | +| Seam | V4 (`0.1.7-rc.2+`, `0.2.0-rc.2+`) | +| --- | --- | +| System prompt | `system/message` surface node (a `request/header` carrying `header.system` is rejected outright) | +| First token | embedded `assistant/message.data.stream` (also `assistant/attempt.data.stream`) — packed delta runs plus raw `chunk` records | +| Replacement endpoints | `{ startSeq, endSeq }` | +| Nested PTC dispatch | `tool/ptc-dispatch` | +| Tool result | first-class `role: 'tool'` message with lifted `message.toolCallId`/`message.isError`, direct content | +| Developer messages | `developer/message` tool-registry changes, nested under `data.message` | -**Legacy logs never reach the fold.** The running harness migrates every historical log to its own generation on EVERY read path the plugin rides — live open, persistence read handles, `sessionQuery.observeSession` (the detail/backfill cold reads) — in memory on read opens and as a published successor file on write opens; the fork-inherited prefix migrates with the rest. A log the migration chain cannot convert is refused entirely (`SessionFormatUnsupportedError`): the session is unavailable, never served raw. So on any supported harness the events the fold sees are V3/V4-shaped by construction, and the pre-V3 spellings (`header.system` envelopes, `assistant/chunk` floods, `{ start, end }` endpoints, `tool/code-dispatch`) exist nowhere in the code. +**Legacy logs never reach the fold.** The running harness migrates every historical log to V4 on EVERY read path the plugin rides — live open, persistence read handles, `sessionQuery.observeSession` (the detail/backfill cold reads) — in memory on read opens and as a published successor file on write opens; the fork-inherited prefix migrates with the rest (the V3→V4 stage lifts the old `tool-result` wrapper into the first-class message and rewrites sources). A log the migration chain cannot convert is refused entirely (`SessionFormatUnsupportedError`): the session is unavailable, never served raw. So on any supported harness the events the fold sees are V4-shaped by construction, and the pre-V4 spellings (the `tool-result` wrapper block and its `isError`, the event-level `data.error` mark, and further back the `header.system` envelopes, `assistant/chunk` floods, `{ start, end }` endpoints, `tool/code-dispatch`) exist nowhere in the code. The fold never branches on a detected harness version. The version probe is best-effort — it reports the module tree the embedding shell resolves plugin imports to, which a packaged client may redirect — so it decides which units register and nothing more. -### Known V4 deltas +### Deltas of the current generation -- **Developer messages**: V4 tool-registry changes are nested under `data.message` and fold into injected context, including request composition, live surface, and removed-node archives. They never count as human inputs; empty-content events keep their surface position at zero tokens, matching the harness's message projection. -- **Tool-result error mark**: V4 made the event-level `data.error` identity optional while lifting `isError` onto the message, so the fold reads the flag from BOTH spellings (`src/host/fold.ts`); a V4 result carrying only `message.isError` still flags its surface node and file-op row. +- **Developer messages**: `developer/message` tool-registry changes are nested under `data.message` and fold into injected context, including request composition, live surface, and removed-node archives. They never count as human inputs; empty-content events keep their surface position at zero tokens, matching the harness's message projection (its `deriveEventMessage` projects an empty message to null — the fold's structural read keeps the node). +- **Tool-result error mark**: the event-level `data.error` metadata is admitted only alongside the message mark (the generation's own validation refuses a row whose `data.error` outlives `message.isError`), so the fold reads `message.isError` alone; a hostile row carrying only the envelope field degrades to unflagged, never to a throw. - **Image offload accounting (deferred)**: when a route rejects with `IMAGE_OFFLOAD_REQUIRED`, the harness offloads images out of the context through a message projection. The plugin's fold reads raw durable events, so an offloaded message keeps its pre-offload image count and price until compaction removes it — an over-count on a rare recovery path, not a crash. Revisit alongside the harness's own token-meter treatment if it shows up in practice. -- **User preferences (shipped)**: the V4 line (first shipped in `0.1.6-alpha.2`) retired the `settings.register` host face and the `settings.plugin.item` browser slot — plugin configuration moved to the Plugins page, derived from each loader entry's own Config schema. The plugin serves both generations from one entry schema: - - The entry `Config` (`src/host/config.ts`) is schemastery. Cordis still validates the `config:` block through its Standard Schema face on every supported line; on V4+ the SAME schema is what the settings `describe` projects — the namespace is the entry id (`dsh-context`) and the eight preference fields (`.volatile()`-marked) are the served, live-editable form; the fold bounds stay ordinary entry config (their edit remounts the entry, a preference edit commits volatile-only and never remounts). - - The browser half registers its card on the seat the running line declares, by service presence: `settingsScope` + `settings.plugin.item` on V3, `configForms` + the Plugins page's keyed `plugins.bundle.config` on V4+ (`ctx.configForms.whileServed` keeps the card alive only while the Host serves the namespace). Both seats render the same eight preference rows. - - The `.volatile()` modifier exists only on the Config-form generations' schemastery; the schema builder feature-detects it, so the same bundle serves both generations. - - Deliberate relaxations against the replaced zod schema: schemastery objects merge unknown keys through instead of failing the load, and only whole-value edits are rejected by range/step checks (bounds remain `min 1, step 1`). - - Preference values persisted on the V3 line live in the settings document (`settings.yaml` section `dsh-context`); the V4 harness's own legacy import moves that section into the profile entry of the same name on first boot, so existing values carry into the new surface. +- **User preferences**: plugin configuration lives on the Plugins page, derived from the loader entry's own Config schema (`src/host/config.ts`): cordis validates the `config:` block through the schema's Standard Schema face, and the SAME schema is what the settings `describe` projects — the namespace is the entry id (`dsh-context`) and the eight preference fields (`.volatile()`-marked) are the served, live-editable form; the fold bounds stay ordinary entry config (their edit remounts the entry, a preference edit commits volatile-only and never remounts). The browser half registers its card on the Plugins page's keyed `plugins.bundle.config` seat over the `configForms` transport (`ctx.configForms.whileServed` keeps the card alive only while the Host serves the namespace). The `.volatile()` modifier is feature-detected at schema build time (`volatileField`), so the same bundle still loads — preferences inert — on a below-floor harness whose schemastery predates it (the gate's fallback composition). + - Preference values persisted on a retired line (the `settings.yaml` section `dsh-context`) are moved into the profile entry of the same name by the harness's own legacy import on first boot, so existing values carry into the current surface. ## Parsing resilience [IMPORTANT!] @@ -76,27 +72,27 @@ The plugin's low-level logic mirrors the newest supported dsh implementation: to ## Web client seams -The browser half rides generation-specific seats, each reached through an optional seam so a deployment without the service simply goes without the capability. Capabilities only newer lines serve are reached through a deferred `ctx.inject` — never a hard module `inject` — so an older harness composes fully with the capability absent (no pending fiber, no throw): the service shape is re-proved before use, the registration is guarded, and the module is declared in `dsh.client.inject`. Every supported line ships the right Sidebar and the keyed `main` conversation panel; the plugin contributes to whichever face the running line serves: +The browser half rides generation-specific seats, each reached through an optional seam so a deployment without the service simply goes without the capability. Capabilities only newer lines serve are reached through a deferred `ctx.inject` — never a hard module `inject` — so a composition without the service composes fully with the capability absent (no pending fiber, no throw): the service shape is re-proved before use, the registration is guarded, and the module is declared in `dsh.client.inject`. Every supported line ships the right Sidebar and the keyed `main` conversation panel; the plugin contributes to whichever face the running line serves: -| Seam | V3 (`0.1.5-rc.1`) | V4 (`0.1.7-rc.2`, `0.2.0-rc.2`) | -| --- | --- | --- | -| Conversation panel root | keyed `main` panel's `main.conversation` (the plugin's `conversation.view` seat hangs under it) | same as V3 | -| Insight page (panel + entry) | the layout's keyed `main` slot under the `dsh-context` panel key, with the sidebar entry on the `sidebar.panellist` list under the same id — the shell owns the entry row and panel switching; both spellings probed per baseline (the `insightPage` probe) | same as V3 | -| Right Sidebar tab + guide entry | `sidebarRightTabs` registry, `sidebar.right.pane.tab`(`.title`) seats, guide capsule with `order`/`title`/`description`/`icon` | same as V3 (`0.1.7-rc.2`+ adds an optional `commandId` to guide entries and `bindCommands` to tab actions — additive, unused by the plugin) | -| Preferences card seat | `settings.plugin.item` over the `settingsScope` transport | `plugins.bundle.config` over the `configForms` transport | -| Session jump | `uiWorkspace.openSession` — the sidebar row click's own verb; the retired `sessions.open(id)` spelling is still served as a fallback | `uiWorkspace.openSession(target: SessionTarget)` — same verb, parameter re-spelled by the 0.1.6 selection refactor; the sessions service carries no selection verb any more | +| Seam | V4 (`0.1.7-rc.2`, `0.2.0-rc.2`) | +| --- | --- | +| Conversation panel root | keyed `main` panel's `main.conversation` (the plugin's `conversation.view` seat hangs under it) | +| Insight page (panel + entry) | the layout's keyed `main` slot under the `dsh-context` panel key, with the sidebar entry on the `sidebar.panellist` list under the same id — the shell owns the entry row and panel switching; both spellings probed per baseline (the `insightPage` probe) | +| Right Sidebar tab + guide entry | `sidebarRightTabs` registry, `sidebar.right.pane.tab`(`.title`) seats, guide capsule with `order`/`title`/`description`/`icon` (an optional `commandId` on guide entries and `bindCommands` on tab actions are additive and unused by the plugin) | +| Preferences card seat | `plugins.bundle.config` over the `configForms` transport | +| Session jump | `uiWorkspace.openSession(target: SessionTarget)` — the sidebar row click's own verb, its parameter re-spelled by the 0.1.6 selection refactor; the sessions service carries no selection verb any more | -The guide capsule carries `order`, `title`, `description`, and `icon`; `0.1.5-rc.1+` renders the description line (the `guideEntry` probe in `tests/baselines.ts` pins the fields of both supported generations). +The guide capsule carries `order`, `title`, `description`, and `icon` (the `guideEntry` probe in `tests/baselines.ts` pins the fields of the supported generation). -The File Activity card's file-name affordance is an optional-column story: on a line with the right Sidebar it opens the file's preview tab through `ctx.sidebarRight.openResource` (the built-in Files sidebar's own path), building the `dsh-resource://file/…` address with the harness's browser-safe `fileAddressFor` (inlined by the client bundle, exactly as the Sidebar's own file types inline it). Where the preview face is absent (or refuses the address), the name keeps its original system-open behavior; the `sidebar.nav` probe pins the navigation face's spelling. +The File Activity card's file-name affordance is an optional-column story: the plugin opens the file's preview tab through `ctx.sidebarRight.openResource` (the built-in Files sidebar's own path), building the `dsh-resource://file/…` address with the harness's browser-safe `fileAddressFor` (inlined by the client bundle, exactly as the Sidebar's own file types inline it). Where the preview face is absent (or refuses the address), the name keeps its original system-open behavior; the `sidebar.nav` probe pins the navigation face's spelling. -The session jump — a Context Insights session card or an Agent network node bringing its session forward — rides the harness's own selection verb (`openSessionVia` in `src/client/services.ts`): `uiWorkspace.openSession`, the verb the built-in sidebar's row click uses on every supported line, with the sessions service's retired `open(id)` spelling kept as the degradation path for a composition without the workspace module. Both faces are re-proved per call and a deployment serving neither (or a hostile face) swallows silently — the verb's own navigation returns the center column to the conversation (`selectPanel(null)`), so a dead jump simply stays on the page. Issue #90 was exactly the retired-spelling trap: the plugin called `sessions.open` alone, which the 0.1.6 selection refactor removed from the service (the click then did nothing on `0.1.7-rc.2`), so the jump now follows the view owner first and the per-baseline `sessionNav` probe pins both spellings presence-as-declared. +The session jump — a Context Insights session card or an Agent network node bringing its session forward — rides the harness's own selection verb (`openSessionVia` in `src/client/services.ts`): `uiWorkspace.openSession`, the verb the built-in sidebar's row click uses on every supported line. The face is re-proved per call and a deployment serving none (or a hostile face) swallows silently — the verb's own navigation returns the center column to the conversation (`selectPanel(null)`), so a dead jump simply stays on the page. Issue #90 was exactly the retired-spelling trap: the plugin once called the sessions service's own `open(id)`, which the 0.1.6 selection refactor removed from the service (the click then did nothing on `0.1.7-rc.2`), so the jump follows the view owner and the per-baseline `sessionNav` probe pins the verb's signature and the retired spelling's absence. -The Context Insights page's DeepSeek balance capsule is an all-optional stack, so it arms nowhere it cannot: the host reaches the `settings` and `credentials` services only inside a deferred `ctx.inject` (and re-reads them per request, so a key added while the host runs is picked up on the next open), mounts `/api/dsh-context/balance` through the same Connection exact-route registry as the detail and backfill routes, and the outbound `/user/balance` read carries the key only inside the host process. The provider's settings row is read through whichever face the running line serves: the `get(ns)` registered-section read through V3 (the `llm-deepseek` namespace), the Config-form `describe()` projection on V4+ — there the row is the entry whose served value declares the top-level `apiKeyEnv` credential ref, preferred under the settings ids the generations have served it as (`llm-deepseek` on product profiles, `llm-deepseek-api-key` elsewhere). The capsule appears only on a live, well-formed answer; a deployment whose DeepSeek API-key provider is not composed (no settings row on either face), whose credentials service cannot resolve the key, whose connection service lacks the exact-route registry, or whose platform read fails (network, non-2xx, malformed payload, a third-party endpoint that does not serve the balance path) answers a typed `null` and the browser renders nothing — no pill, no spinner, no error state. The `balance` probe pins the settings face and the provider row's sources per baseline. +The Context Insights page's DeepSeek balance capsule is an all-optional stack, so it arms nowhere it cannot: the host reaches the `settings` and `credentials` services only inside a deferred `ctx.inject` (and re-reads them per request, so a key added while the host runs is picked up on the next open), mounts `/api/dsh-context/balance` through the same Connection exact-route registry as the detail and backfill routes, and the outbound `/user/balance` read carries the key only inside the host process. The provider's settings row is read off the Config-form `describe()` projection — the row is the entry whose served value declares the top-level `apiKeyEnv` credential ref, preferred under the settings ids the generations have served it as (`llm-deepseek` on product profiles, `llm-deepseek-api-key` elsewhere). The capsule appears only on a live, well-formed answer; a deployment whose DeepSeek API-key provider is not composed (no settings row), whose credentials service cannot resolve the key, whose connection service lacks the exact-route registry, or whose platform read fails (network, non-2xx, malformed payload, a third-party endpoint that does not serve the balance path) answers a typed `null` and the browser renders nothing — no pill, no spinner, no error state. The `balance` probe pins the settings face and the provider row's sources per baseline. ## What each check means -- **Automated seam matrix** — part of this repository's `pnpm test` (the `compat` vitest project). For every baseline tag it stages the harness's REAL sources at that tag, boots the plugin's built host entry into that tag's actual `SessionProjectionRegistry` on the cordis release the line vendors, and probes the tag's client seams (slots, finalized-nodes seat, image loader, history face/envelope, markdown chrome, platform module table, that generation's durable-event vocabulary, the right Sidebar tab seam, its guide-entry contract and its resource-navigation face, the session-jump seam, the preferences card seat and settings transport). Definitions live in `tests/baselines.ts` — the one source of truth for the supported baselines and their per-version seam faces; the release workflow fetches the pinned baseline tags before testing. Preferences seats and transports are asserted BOTH ways: each slot exists on exactly one supported generation, so the plugin's deferred registrations pick their side and never pend. To add a future dsh release: add one `Baseline` entry (tag, vendored cordis, faces) to `tests/baselines.ts` and its tag to the release workflow's fetch step, then run `pnpm test` and re-fit whichever seam a failing probe names. The `compat` project boots the BUILT plugin (`pnpm run build` first) against the ACTUAL harness sources at each baseline tag and needs a dsh checkout (env `DSH_REPO`, default `~/dev/deepseek-harness`). +- **Automated seam matrix** — part of this repository's `pnpm test` (the `compat` vitest project). For every baseline tag it stages the harness's REAL sources at that tag, boots the plugin's built host entry into that tag's actual `SessionProjectionRegistry` on the cordis release the line vendors, and probes the tag's client seams (slots, finalized-nodes seat, image loader, history face/envelope, markdown chrome, platform module table, that generation's durable-event vocabulary, the right Sidebar tab seam, its guide-entry contract and its resource-navigation face, the session-jump seam, the preferences card seat and settings transport). Definitions live in `tests/baselines.ts` — the one source of truth for the supported baselines and their per-version seam faces; the release workflow fetches the pinned baseline tags before testing. Retired seams are asserted ABSENT (`settings.register`, the settings `get(ns)` read, the sessions service's `open(id)`), so a resurrection names the seam instead of silently shadowing the current face. To add a future dsh release: add one `Baseline` entry (tag, vendored cordis, faces) to `tests/baselines.ts` and its tag to the release workflow's fetch step, then run `pnpm test` and re-fit whichever seam a failing probe names. The `compat` project boots the BUILT plugin (`pnpm run build` first) against the ACTUAL harness sources at each baseline tag and needs a dsh checkout (env `DSH_REPO`, default `~/dev/deepseek-harness`). - **Statistics against the harness's own folds** — the plugin's figures were verified differentially against the harness's OWN projection values (`sessionStats`, `contextBreakdown`, `contextPressure`, `tokenUsage`) over real session logs: system/tools/message tokens, per-request counts, turns/steps, TTFT, generation, tool time, and every billed cost bucket match exactly. - **Disposable-profile install / uninstall** — for each release, that exact `dsh` CLI version was installed from npm into a temporary `DSH_HOME` (the real `~/.dsh` is never touched), then: 1. `dsh plugin --profile add dsh-context` — install OK; diff --git a/docs/settings.png b/docs/settings.png index 4cc3ff015d98b058923cc1383663f0da830ead5c..adec10696582e3fa2391d26bbc47151a8e74b254 100644 GIT binary patch literal 135972 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 literal 28332 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 diff --git a/package.json b/package.json index a7817f6..ecbbdb7 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ }, "compatibility": { "dshReleases": { - "0.1.5-rc.1": "compatible", "0.1.7-rc.2": "compatible", "0.2.0-rc.2": "compatible" } @@ -88,10 +87,9 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@deepseek-ai/cordis": "^4.0.2", - "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.5-rc.1", - "@deepseek-ai/dsh-session": ">=0.1.5-rc.1", - "@deepseek-ai/dsh-settings": ">=0.1.5-rc.1", + "@deepseek-ai/cordis": "^4.0.4", + "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.2", + "@deepseek-ai/dsh-session": ">=0.1.7-rc.2", "@deepseek-ai/schemastery": "^3.18.2", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -108,12 +106,13 @@ } }, "devDependencies": { - "@deepseek-ai/cordis": "^4.0.2", - "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.1", - "@deepseek-ai/dsh-session": "0.1.5-rc.1", - "@deepseek-ai/dsh-session-projection": "0.1.5-rc.1", - "@deepseek-ai/dsh-settings": "0.1.5-rc.1", - "@deepseek-ai/dsh-util-workspace-path": "0.1.5-rc.1", + "@deepseek-ai/cordis": "^4.0.4", + "@deepseek-ai/dsh-client-store": "0.1.7-rc.2", + "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.2", + "@deepseek-ai/dsh-session": "0.1.7-rc.2", + "@deepseek-ai/dsh-session-projection": "0.1.7-rc.2", + "@deepseek-ai/dsh-util-code-language": "0.1.7-rc.2", + "@deepseek-ai/dsh-util-workspace-path": "0.1.7-rc.2", "@deepseek-ai/schemastery": "^3.18.4", "@opencode-ai/models": "^0.0.87", "@shikijs/langs": "^4.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0ac76b..0b64ed9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,23 +13,26 @@ importers: version: 4.6.5 devDependencies: '@deepseek-ai/cordis': - specifier: ^4.0.2 - version: 4.0.2 + specifier: ^4.0.4 + version: 4.0.4 + '@deepseek-ai/dsh-client-store': + specifier: 0.1.7-rc.2 + version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) '@deepseek-ai/dsh-client-ui-primitives': - specifier: 0.1.5-rc.1 - version: 0.1.5-rc.1(@deepseek-ai/cordis@4.0.2) + specifier: 0.1.7-rc.2 + version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) '@deepseek-ai/dsh-session': - specifier: 0.1.5-rc.1 - version: 0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))) + specifier: 0.1.7-rc.2 + version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-scope@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-invariants@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4))) '@deepseek-ai/dsh-session-projection': - specifier: 0.1.5-rc.1 - version: 0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)))) - '@deepseek-ai/dsh-settings': - specifier: 0.1.5-rc.1 - version: 0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.4) + specifier: 0.1.7-rc.2 + version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-session@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-scope@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-invariants@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)))) + '@deepseek-ai/dsh-util-code-language': + specifier: 0.1.7-rc.2 + version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) '@deepseek-ai/dsh-util-workspace-path': - specifier: 0.1.5-rc.1 - version: 0.1.5-rc.1(@deepseek-ai/cordis@4.0.2) + specifier: 0.1.7-rc.2 + version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) '@deepseek-ai/schemastery': specifier: ^3.18.4 version: 3.18.4 @@ -225,12 +228,12 @@ packages: resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} engines: {node: '>=20.19.0'} - '@deepseek-ai/cordis@4.0.2': - resolution: {integrity: sha512-asOnXP1TzFSFQlHb1iegDZp0z/8WD1c7YNrwJR/Tx2bzNuMXfcekE/I67Iv6SQXeLB4csxqCngzQKANP7gdw0g==} + '@deepseek-ai/cordis@4.0.4': + resolution: {integrity: sha512-obgyxqWAmFn3Re8kvsuUnyW+ihrz6eJCnJO4fh1cQzDtmPYz/zzVeUkH9R94I0OwSVOocK67Kgakm04j/oQXzg==} hasBin: true peerDependencies: - '@deepseek-ai/cordis-plugin-include': ^1.0.7 - '@deepseek-ai/cordis-plugin-loader': ^1.0.3 + '@deepseek-ai/cordis-plugin-include': ~1.0.9 + '@deepseek-ai/cordis-plugin-loader': ~1.0.5 peerDependenciesMeta: '@deepseek-ai/cordis-plugin-include': optional: true @@ -240,80 +243,78 @@ packages: '@deepseek-ai/cosmokit@1.8.5': resolution: {integrity: sha512-LXsrlem9z8dq4sLflj2yuCuYX7KqhdzF5hZly3eZyuTo8d6oDSOXuEgC5DbQWKW+lksm6zmOutQLsP/ma6wR6A==} - '@deepseek-ai/dsh-brand@0.1.5-rc.3': - resolution: {integrity: sha512-pzbPtyEwnFMCrUYLwFio8GmUxtTqqAkhW5IheWH1tSgUnVIAu4OwVbxwTNiDGMRbgTRAzT9xZ7bukUejpyob4w==} + '@deepseek-ai/dsh-brand@0.1.7-rc.2': + resolution: {integrity: sha512-cvcvlNRuiFqZNNqyxdu7wcpiBjd9xa/0MpuhRvee43zBp9uED2p5JolZr5/2EKJf57gYx9k9bedb+Zbyiqnpkw==} peerDependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': ~4.0.4 - '@deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.1': - resolution: {integrity: sha512-C6mJOcwg41gD5Yy57QGjZhX/8p8U1kSR4px7tOfLmLIKKXyeHbDytJEKjPKHfh8TkOvpqQ4wA02gQsVe06ithQ==} + '@deepseek-ai/dsh-client-store@0.1.7-rc.2': + resolution: {integrity: sha512-8nF4gqy/3yvSN3KsuHX61DVP1Z2YjC93wkB2uGVS5xg4iyD1KgCqWB2S+fVAdQ7END9PDzWhZTk71AYnkSh7LA==} peerDependencies: - '@deepseek-ai/cordis': ^4.0.2 + '@deepseek-ai/cordis': ~4.0.4 - '@deepseek-ai/dsh-invariants@0.1.5-rc.3': - resolution: {integrity: sha512-1iPfLsu8zFAsfIRYE958uMhB3oTO/LmN3ArTifxWFlRvCmBRd9Ca6l7DZh/daLnJsDSdtLQtVzPqbgTV9tzNlQ==} + '@deepseek-ai/dsh-client-ui-primitives@0.1.7-rc.2': + resolution: {integrity: sha512-ymIUil9ivHcIDgaAmF88lOygC7dJ0UsUgdbIvXT5bymPZDzvDJIoorOKhlMsCqPEG1ZawCMVhUatnIPi4xdlMQ==} peerDependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': ~4.0.4 - '@deepseek-ai/dsh-llm@0.1.5-rc.3': - resolution: {integrity: sha512-c6Z7S1SlRAgLKQ/MZKp0mCMrBz06X9XEddLFxhK5Flq0PM18dsJgIbRgNpobILy2AJyYx0vnHbepjmITW6BXWA==} + '@deepseek-ai/dsh-invariants@0.1.7-rc.2': + resolution: {integrity: sha512-GY2lY2TUUH2Qk1ZnrB3Q5+lXSh4webn1kbt5nQfhojVmNbist/f3dJC8JJa7XXOUNvqAgqRN9IZ4egbDZ9LLxQ==} peerDependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': ~4.0.4 - '@deepseek-ai/dsh-scope@0.1.5-rc.3': - resolution: {integrity: sha512-2B45GShJQwPs+Ad4MdI2kkbxkogagZopAY2lbYowAz6Oris3jGxZJ4v3/ZyJw7c9bhTJUagHXCpXQYFSIXumLA==} + '@deepseek-ai/dsh-llm@0.1.7-rc.2': + resolution: {integrity: sha512-hNT3ovFnbNDQ8g544bh2Q9ORX40aI6Y+jhjer1lETeHS4g0jTjqUUVfARCTA7uAFvHucSBg2rFgbisIktmJCxw==} peerDependencies: - '@deepseek-ai/cordis': 4.0.2 - '@deepseek-ai/dsh-invariants': ^0.1.5-rc.3 + '@deepseek-ai/cordis': ~4.0.4 - '@deepseek-ai/dsh-session-projection@0.1.5-rc.1': - resolution: {integrity: sha512-dwDS3ite9I1oOAPneA87ieHBd+szFNrTeMNF4XHRxexMbBWxNZUv62h714UMNlk2niGKsAKE56PzwlnWHmGwGg==} + '@deepseek-ai/dsh-scope@0.1.7-rc.2': + resolution: {integrity: sha512-rk/OM5ej6/V8pCNczEqQ3BkKx3DuPeFGzNUsEZo8hF+tnkF/EhsxMWSXC/yP/uLKeNkSsVHzjFpaQR0qr0J3gQ==} peerDependencies: - '@deepseek-ai/cordis': ^4.0.2 - '@deepseek-ai/dsh-session': ^0.1.5-rc.1 + '@deepseek-ai/cordis': ~4.0.4 + '@deepseek-ai/dsh-invariants': 0.1.7-rc.2 - '@deepseek-ai/dsh-session@0.1.5-rc.1': - resolution: {integrity: sha512-0YBBrzkCbVEJolS/OpD0DZMSozmYxUTZopiG76MXInjOFBW9J4ca1a8WUjJjqelP1nJTmWGKXp92HcvNEny0Dg==} + '@deepseek-ai/dsh-session-projection@0.1.7-rc.2': + resolution: {integrity: sha512-s5T8oHBEh+GeOTJ+wehD4CGZkTiWgczZZA+NDbfbqUO0k5L+dk0kk3fN+/srAg77rhR7UeeDSR8ZNcpYx/VUdw==} peerDependencies: - '@deepseek-ai/cordis': ^4.0.2 - '@deepseek-ai/dsh-scope': ^0.1.5-rc.1 + '@deepseek-ai/cordis': ~4.0.4 + '@deepseek-ai/dsh-session': 0.1.7-rc.2 - '@deepseek-ai/dsh-settings@0.1.5-rc.1': - resolution: {integrity: sha512-9t6JlHwnMu7qTHpMVVRLyzfi7yWKlDozmh7Xbjjd5mmzY+dSdKTPsoRGSdGgTLvBMQBi0X2O8Kj2moDp30XKGg==} + '@deepseek-ai/dsh-session@0.1.7-rc.2': + resolution: {integrity: sha512-0dXv1t4Zox/yHkZ4PfPNSBWJfIc72PDqOvjz43EMoHpbmoKTVpit2UpUI+yTtX01e4MVBbDMxBZ95oJW/Oydnw==} peerDependencies: - '@deepseek-ai/cordis': ^4.0.2 - '@deepseek-ai/dsh-brand': ^0.1.5-rc.1 - '@deepseek-ai/dsh-invariants': ^0.1.5-rc.1 - '@deepseek-ai/dsh-session': ^0.1.5-rc.1 - '@deepseek-ai/schemastery': ^3.18.2 + '@deepseek-ai/cordis': ~4.0.4 + '@deepseek-ai/dsh-scope': 0.1.7-rc.2 - '@deepseek-ai/dsh-timeout@0.1.5-rc.3': - resolution: {integrity: sha512-Z7HyqvGaO/k4XLp/EykvIuA0i67CvWb/R0c7Lt/3/8dwmDvqKWgWvh/EiNip7UMXMpQBJzI+Xe4OuzXV/A90Nw==} + '@deepseek-ai/dsh-timeout@0.1.7-rc.2': + resolution: {integrity: sha512-YunLoLZSF8Ld9eYwyvKoBnxgy3GZrIQpw98HfFS15ZfZlqcFiZcchFhYmStLtUbtX+gjhoeXc8Xz913YbVifNA==} peerDependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': ~4.0.4 - '@deepseek-ai/dsh-typert-protocol@0.1.5-rc.3': - resolution: {integrity: sha512-lcQGIQK4Z5dBguLQFzoDiHQBT387egSnFYJOakEjqIMM9jkAfMI/HqlsVSRSzHW7SQL9f6SxJMHCD7pIkJzsWA==} + '@deepseek-ai/dsh-typert-protocol@0.1.7-rc.2': + resolution: {integrity: sha512-oJm03zqMQlvt16Ejw7VWy3/gqlJGrBCZj3Z4KL8ydLJeeTToKIR49WVCxMbJTlH0F9QFljruLtHguA5B99AeUQ==} peerDependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': ~4.0.4 - '@deepseek-ai/dsh-util-crypto@0.1.5-rc.3': - resolution: {integrity: sha512-gQAFF2TLLAV5ejHAklCplePVdL2TV4CH1Xt3A0bqaLxZ2zoOxXoVUieUzPrHo+X1NKfsmdcWvOx9zdlDkXY+sQ==} + '@deepseek-ai/dsh-util-code-language@0.1.7-rc.2': + resolution: {integrity: sha512-/NcCcgFXbTfV4sse5HoYO6SyKfcn9GFHeCP/bPkFsACZSv1dhosFivnz8jkkS1esbQe97jiki+UQcZBd+O6Usw==} peerDependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': ~4.0.4 - '@deepseek-ai/dsh-util-values@0.1.5-rc.3': - resolution: {integrity: sha512-K0qEm9XmgcxCMuJBK7skNE3v35JTOurN24IwrZ4t0xoaSlZSF4wR0L/R7ipj9IQSlB7sGdQLdegTNsZmSlmw4Q==} + '@deepseek-ai/dsh-util-crypto@0.1.7-rc.2': + resolution: {integrity: sha512-BGc55/bSj/1LcDTQ5fpok4U24X9SFyN+TB23/hBlbe2TBUKVuwO8+X5FFKKje2MLO8PWYEDkXv/LrV6jRVKZEg==} peerDependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': ~4.0.4 - '@deepseek-ai/dsh-util-workspace-path@0.1.5-rc.1': - resolution: {integrity: sha512-yGU2meCr+8Ru+4WbMtKLlROxtuj9zGjJ6N6bJs20kRxIT0JNlZ6uwqnDQONgN8u44WdyKEaIwbv5IGjxIftLRw==} + '@deepseek-ai/dsh-util-values@0.1.7-rc.2': + resolution: {integrity: sha512-UX8nh5SbtZcPVVEvjd+9PCPcbybA6OpVzJGgFxYn59ao/7QQGyYbByS8KXr+THU5JtqmDDVb9xGUSJaF2IYmeQ==} peerDependencies: - '@deepseek-ai/cordis': ^4.0.2 + '@deepseek-ai/cordis': ~4.0.4 - '@deepseek-ai/schemastery@3.18.2': - resolution: {integrity: sha512-njDtZsznjYxok7KLLlHOPyuv2efdWVbSflAHgztSfbMsg+CVraEoRe2DjOCgClYv3ZCSm7WXoaUkbB/+RY7tWQ==} + '@deepseek-ai/dsh-util-workspace-path@0.1.7-rc.2': + resolution: {integrity: sha512-opp6TTeRvZHPi2bsXNK66TwUBh4uNECZJp/qK35ffLQRNa3L+RoPsGgOJZuRjkXdNygytM26P/CWjaBX65oKXg==} + peerDependencies: + '@deepseek-ai/cordis': ~4.0.4 '@deepseek-ai/schemastery@3.18.4': resolution: {integrity: sha512-SSXO6tYuyrIqKVbmOnIq0s+riUywYzouFMcnBluHF9n4KMo2G8HvEzhCYj6pj2617/glOjbJuVInJ9hfONsjkg==} @@ -2429,89 +2430,84 @@ snapshots: '@csstools/css-tokenizer@4.0.2': {} - '@deepseek-ai/cordis@4.0.2': + '@deepseek-ai/cordis@4.0.4': dependencies: '@deepseek-ai/cosmokit': 1.8.5 '@standard-schema/spec': 1.1.0 '@deepseek-ai/cosmokit@1.8.5': {} - '@deepseek-ai/dsh-brand@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)': + '@deepseek-ai/dsh-brand@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': dependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': 4.0.4 - '@deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)': + '@deepseek-ai/dsh-client-store@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': dependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': 4.0.4 - '@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)': + '@deepseek-ai/dsh-client-ui-primitives@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': dependencies: - '@deepseek-ai/cordis': 4.0.2 - '@deepseek-ai/schemastery': 3.18.2 + '@deepseek-ai/cordis': 4.0.4 - '@deepseek-ai/dsh-llm@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)': + '@deepseek-ai/dsh-invariants@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': dependencies: - '@deepseek-ai/cordis': 4.0.2 - '@deepseek-ai/dsh-brand': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - '@deepseek-ai/dsh-timeout': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - '@deepseek-ai/dsh-typert-protocol': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - '@deepseek-ai/dsh-util-crypto': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - '@deepseek-ai/dsh-util-values': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - '@deepseek-ai/schemastery': 3.18.2 - zod: 4.6.5 - - '@deepseek-ai/dsh-scope@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))': - dependencies: - '@deepseek-ai/cordis': 4.0.2 - '@deepseek-ai/dsh-invariants': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - - '@deepseek-ai/dsh-session-projection@0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-session@0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))))': - dependencies: - '@deepseek-ai/cordis': 4.0.2 - '@deepseek-ai/dsh-session': 0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))) - zod: 4.6.5 - - '@deepseek-ai/dsh-session@0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)))': - dependencies: - '@deepseek-ai/cordis': 4.0.2 - '@deepseek-ai/dsh-brand': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - '@deepseek-ai/dsh-llm': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - '@deepseek-ai/dsh-scope': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)) - '@deepseek-ai/dsh-util-values': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - - '@deepseek-ai/dsh-settings@0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-brand@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))(@deepseek-ai/dsh-session@0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))))(@deepseek-ai/schemastery@3.18.4)': - dependencies: - '@deepseek-ai/cordis': 4.0.2 - '@deepseek-ai/dsh-brand': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - '@deepseek-ai/dsh-invariants': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) - '@deepseek-ai/dsh-session': 0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-scope@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)(@deepseek-ai/dsh-invariants@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2))) - '@deepseek-ai/dsh-util-values': 0.1.5-rc.3(@deepseek-ai/cordis@4.0.2) + '@deepseek-ai/cordis': 4.0.4 '@deepseek-ai/schemastery': 3.18.4 - '@deepseek-ai/dsh-timeout@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)': + '@deepseek-ai/dsh-llm@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': dependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': 4.0.4 + '@deepseek-ai/dsh-brand': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) + '@deepseek-ai/dsh-timeout': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) + '@deepseek-ai/dsh-typert-protocol': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) + '@deepseek-ai/dsh-util-crypto': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) + '@deepseek-ai/dsh-util-values': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) + '@deepseek-ai/schemastery': 3.18.4 + zod: 4.6.5 - '@deepseek-ai/dsh-typert-protocol@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)': + '@deepseek-ai/dsh-scope@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-invariants@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4))': dependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': 4.0.4 + '@deepseek-ai/dsh-invariants': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) - '@deepseek-ai/dsh-util-crypto@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)': + '@deepseek-ai/dsh-session-projection@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-session@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-scope@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-invariants@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4))))': dependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': 4.0.4 + '@deepseek-ai/dsh-session': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-scope@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-invariants@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4))) + zod: 4.6.5 - '@deepseek-ai/dsh-util-values@0.1.5-rc.3(@deepseek-ai/cordis@4.0.2)': + '@deepseek-ai/dsh-session@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-scope@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-invariants@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)))': dependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': 4.0.4 + '@deepseek-ai/dsh-brand': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) + '@deepseek-ai/dsh-llm': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) + '@deepseek-ai/dsh-scope': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-invariants@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)) + '@deepseek-ai/dsh-util-values': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) - '@deepseek-ai/dsh-util-workspace-path@0.1.5-rc.1(@deepseek-ai/cordis@4.0.2)': + '@deepseek-ai/dsh-timeout@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': dependencies: - '@deepseek-ai/cordis': 4.0.2 + '@deepseek-ai/cordis': 4.0.4 - '@deepseek-ai/schemastery@3.18.2': + '@deepseek-ai/dsh-typert-protocol@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': dependencies: - '@deepseek-ai/cosmokit': 1.8.5 - '@standard-schema/spec': 1.1.0 + '@deepseek-ai/cordis': 4.0.4 + '@deepseek-ai/dsh-brand': 0.1.7-rc.2(@deepseek-ai/cordis@4.0.4) + + '@deepseek-ai/dsh-util-code-language@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': + dependencies: + '@deepseek-ai/cordis': 4.0.4 + + '@deepseek-ai/dsh-util-crypto@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': + dependencies: + '@deepseek-ai/cordis': 4.0.4 + + '@deepseek-ai/dsh-util-values@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': + dependencies: + '@deepseek-ai/cordis': 4.0.4 + + '@deepseek-ai/dsh-util-workspace-path@0.1.7-rc.2(@deepseek-ai/cordis@4.0.4)': + dependencies: + '@deepseek-ai/cordis': 4.0.4 '@deepseek-ai/schemastery@3.18.4': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4771129..5ca7c78 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,16 +8,20 @@ allowBuilds: esbuild: true minimumReleaseAgeExclude: - - '@deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.1' - - '@deepseek-ai/dsh-session@0.1.5-rc.1' - - '@deepseek-ai/dsh-session-projection@0.1.5-rc.1' - - '@deepseek-ai/dsh-settings@0.1.5-rc.1' + - '@deepseek-ai/dsh-client-store@0.1.7-rc.2' + - '@deepseek-ai/dsh-client-ui-primitives@0.1.7-rc.2' + - '@deepseek-ai/dsh-session@0.1.7-rc.2' + - '@deepseek-ai/dsh-session-projection@0.1.7-rc.2' + - '@deepseek-ai/dsh-util-code-language@0.1.7-rc.2' + - '@deepseek-ai/dsh-util-workspace-path@0.1.7-rc.2' - '@deepseek-ai/cosmokit@1.8.3 || 1.8.5' - - '@deepseek-ai/dsh-brand@0.1.5-rc.3' - - '@deepseek-ai/dsh-llm@0.1.5-rc.3' - - '@deepseek-ai/dsh-timeout@0.1.5-rc.3' - - '@deepseek-ai/dsh-typert-protocol@0.1.5-rc.3' - - '@deepseek-ai/dsh-util-crypto@0.1.5-rc.3' - - '@deepseek-ai/dsh-util-values@0.1.5-rc.3' + - '@deepseek-ai/dsh-brand@0.1.7-rc.2' + - '@deepseek-ai/dsh-invariants@0.1.7-rc.2' + - '@deepseek-ai/dsh-llm@0.1.7-rc.2' + - '@deepseek-ai/dsh-scope@0.1.7-rc.2' + - '@deepseek-ai/dsh-timeout@0.1.7-rc.2' + - '@deepseek-ai/dsh-typert-protocol@0.1.7-rc.2' + - '@deepseek-ai/dsh-util-crypto@0.1.7-rc.2' + - '@deepseek-ai/dsh-util-values@0.1.7-rc.2' - '@deepseek-ai/schemastery@3.18.2 || 3.18.4' - '@opencode-ai/models@0.0.72 || 0.0.87' diff --git a/src/client/components/fileCard.tsx b/src/client/components/fileCard.tsx index c338607..32c6f1c 100644 --- a/src/client/components/fileCard.tsx +++ b/src/client/components/fileCard.tsx @@ -10,8 +10,8 @@ * expands the file's own operation log; each operation is itself the click * target that jumps to (and reveals) the exact tool result in the browser. * The file name opens the file's right-Sidebar preview where that column - * exists (dsh 0.1.5-rc.1+, the shipped files-sidebar idiom), and the system - * opener where it does not. + * exists (the shipped files-sidebar idiom), and the system opener where it + * does not. */ import { memo, useState, type ChangeEvent, type ComponentType, type MouseEvent, type ReactElement } from 'react' diff --git a/src/client/components/richText.tsx b/src/client/components/richText.tsx index 9d96f3b..544790f 100644 --- a/src/client/components/richText.tsx +++ b/src/client/components/richText.tsx @@ -184,7 +184,7 @@ export function makeRichText(kit: ViewKit): RichKit { function RichText(props: { text: string; mode: RichMode }): ReactElement { // Reference-stable per locale: a fresh object identity would discard the - // renderer's cached elements on every render (0.1.2+ faces). + // renderer's cached elements on every render. const mdLabels = useMemo(() => ({ code: { copyLabel: t('rich.md.copy'), copiedLabel: t('rich.md.copied') }, footnotes: t('rich.md.footnotes'), diff --git a/src/client/components/settingsCard.tsx b/src/client/components/settingsCard.tsx index 60eeb88..9b91d6c 100644 --- a/src/client/components/settingsCard.tsx +++ b/src/client/components/settingsCard.tsx @@ -1,21 +1,15 @@ /** - * The dsh-context preference cards — two seats over the same eight rows. The - * settings-section card (`settings.plugin.item`, the older harness lines) - * renders a collapsible list item in Settings → Plugins → Plugin - * configuration; the Plugins-page card (`plugins.bundle.config`, the - * Config-form generation) renders the rows flat inside the section chrome the - * Plugins page draws for the bundle. Both are keyed on the Host-served - * `dsh-context` namespace and render nothing while it is unavailable (a - * deployment without the Host half, or a remote browser, shows no trace). - * The settings-section card mounts expanded when the Plugin Info card's - * "Open plugin settings" jump left a fresh expand request (settingsJump.ts), - * scrolling itself into view. + * The dsh-context preference card: the eight preference rows on the + * Plugins-page seat (`plugins.bundle.config`, the Config-form generation), + * rendered flat inside the section chrome the Plugins page draws for the + * bundle. The card is keyed on the Host-served `dsh-context` namespace and + * renders nothing while it is unavailable (a deployment without the Host + * half, or a remote browser, shows no trace). */ -import { useEffect, useRef, useState, type ReactElement } from 'react' +import { useState, type ReactElement } from 'react' import { Menu } from '@deepseek-ai/dsh-client-ui-primitives' import { IconChevronDown } from '../primitives' -import { consumeCardExpand } from '../settingsJump' import type { SettingsField, SettingsState } from '../settings' import type { ViewKit } from '../viewkit' @@ -67,7 +61,7 @@ function PrefRow(props: PrefRowProps): ReactElement { /** Translate over the plugin's dictionary, as the view kit binds it. */ type Translate = ViewKit['t'] -/** The seven preference rows shared by both seats. */ +/** The eight preference rows. */ function PreferenceRows(props: { t: Translate; state: SettingsState; set?: SettingsCardProps['set'] }): ReactElement { const { t, state, set } = props const disabled = state.status !== 'ready' || !state.writable @@ -160,57 +154,10 @@ function PreferenceRows(props: { t: Translate; state: SettingsState; set?: Setti ) } -export function makeSettingsCard(kit: ViewKit): (props: SettingsCardProps) => ReactElement | null { - const { t } = kit - return function SettingsCard(props: SettingsCardProps): ReactElement | null { - const [open, setOpen] = useState(false) - const itemRef = useRef(null) - // "Open plugin settings" jump: consume its fresh expand request once on - // mount and land open; every guard stays local so no host quirk can surface. - useEffect(() => { - if (!consumeCardExpand()) return - setOpen(true) - try { - itemRef.current?.scrollIntoView({ block: 'nearest' }) - } catch { /* hosts without scrollIntoView: expanded but unscrolled */ } - }, []) - const state = typeof props.useContextSettings === 'function' ? props.useContextSettings(s => s) : undefined - if (state === undefined || state.status === 'unavailable') return null - return ( -
  • - - {open - ? ( -
    - {!state.writable && state.status === 'ready' - ?

    {t('settings.readOnly')}

    - : null} - -
    - ) - : null} -
  • - ) - } -} - /** * The Plugins-page card (the Config-form generation's `plugins.bundle.config` * seat, `view: 'page'`): the page owns the bundle's page and section chrome, - * so the rows render flat. Same unserved/absent degradation as the - * settings-section card. + * so the rows render flat. Unserved/absent degrades to nothing. */ export function makePluginConfigCard(kit: ViewKit): (props: SettingsCardProps) => ReactElement | null { const { t } = kit diff --git a/src/client/dockMeasure.ts b/src/client/dockMeasure.ts index 6c76a8b..f3cdaff 100644 --- a/src/client/dockMeasure.ts +++ b/src/client/dockMeasure.ts @@ -1,9 +1,9 @@ /** * The /context modal's dock insets: the shell's sidebar tracks, so the fixed backdrop spans exactly the main column and centers the dialog * over it instead of the whole viewport. The only host anchor is the app frame's inline grid template (`px ` — - * the leading px track is the left sidebar, the trailing px track the right panel's bound: the contract every supported baseline shares; - * the centre track's spelling differs per generation) found by walking up from the backdrop. On the minmax generations the trailing bound - * is NOT the solved width: the frame lets the grid squeeze (the centre's declared minimum wins, the right track takes the remainder up to + * the leading px track is the left sidebar, the trailing px track the right panel's bound: the contract every supported baseline shares) + * found by walking up from the backdrop. The trailing bound is NOT the solved width: the frame lets the grid + * squeeze (the centre's declared minimum wins, the right track takes the remainder up to * its bound — the frame's own columns solve), so the parsed bound is clamped to the space the centre minimum leaves. Anything unresolved — * no frame, unparsable template, a hostile node in the chain — degrades to 0 insets, the full-viewport mask. */ @@ -19,9 +19,9 @@ export interface DockMeasure { } const LEADING_PX_TRACK = /^(\d+(?:\.\d+)?)px/ -// The centre track's declared minimum (`minmax(0, 1fr)`, `minmax(400px, 1fr)` — spelling varies per generation, only the number is read). +// The centre track's declared minimum (`minmax(0, 1fr)`, `minmax(400px, 1fr)` — spelling varies per shell layout, only the number is read). const CENTRE_MIN_TRACK = /^\s*minmax\(\s*([\d.]+)(?:px)?\s*,/ -// The V4 spelling's trailing track closes with a paren after the px. +// The minmax spelling's trailing track closes with a paren after the px. const TRAILING_PX_TRACK = /(\d+(?:\.\d+)?)px\)?$/ /** Measure the sidebar tracks from the backdrop's ancestor chain, or resolve to the full-viewport mask. */ diff --git a/src/client/historyPage.ts b/src/client/historyPage.ts index 61e2387..81d0e93 100644 --- a/src/client/historyPage.ts +++ b/src/client/historyPage.ts @@ -161,20 +161,21 @@ export function pageNodesOf(entries: readonly unknown[]): Map : null const source = message?.source !== null && typeof message?.source === 'object' ? message.source as Record : null - const first = Array.isArray(message?.content) ? (message.content as unknown[])[0] : undefined - const block = first !== null && typeof first === 'object' ? first as Record : null - const callId = typeof source?.callId === 'string' ? source.callId - : typeof block?.toolCallId === 'string' ? block.toolCallId + const callId = typeof message?.toolCallId === 'string' ? message.toolCallId + : typeof source?.callId === 'string' ? source.callId : null const call = callId !== null ? calls.get(callId) ?? null : null nodes.set(seq, { kind: 'tool-result', seq, call, - content: block !== null && Array.isArray(block.content) ? block.content as readonly unknown[] : [], - isError: block?.isError === true || data.error === true, + content: Array.isArray(message?.content) ? message.content as readonly unknown[] : [], + isError: message?.isError === true, }) continue } diff --git a/src/client/i18n.ts b/src/client/i18n.ts index 74f59d7..c05f912 100644 --- a/src/client/i18n.ts +++ b/src/client/i18n.ts @@ -78,8 +78,6 @@ export const DICT_ZH: Record = { 'trend.title': '上下文趋势', 'gran.step': '步骤', 'gran.turn': '轮次', - 'settings.title': '上下文', - 'settings.desc': 'dsh-context 插件中上下文面板的偏好设置', 'settings.placement': '入口位置', 'placement.tab': '标签页', 'placement.sidebar': '侧边栏', @@ -93,8 +91,6 @@ export const DICT_ZH: Record = { 'settings.deltaBase': '上下文浏览器 · 对比基线', 'settings.toolSort': '工具定义 · 默认排序', 'settings.fileSort': '文件活动 · 默认排序', - 'settings.expand': '展开', - 'settings.collapse': '收起', 'settings.readOnly': '当前环境的设置为只读', 'gran.total': '全量', 'gran.delta': '增量', @@ -495,8 +491,6 @@ export const DICT_EN: Record = { 'trend.title': 'Context Trend', 'gran.step': 'Step', 'gran.turn': 'Turn', - 'settings.title': 'Context', - 'settings.desc': 'Preferences for Context panel in dsh-context plugin', 'settings.placement': 'Entry Options', 'placement.tab': 'Tab', 'placement.sidebar': 'Sidebar', @@ -510,8 +504,6 @@ export const DICT_EN: Record = { 'settings.deltaBase': 'Context Browser · Diff baseline', 'settings.toolSort': 'Tool Schemas · Default sort', 'settings.fileSort': 'File Activity · Default sort', - 'settings.expand': 'Expand', - 'settings.collapse': 'Collapse', 'settings.readOnly': 'Settings are read-only in this environment', 'gran.total': 'Total', 'gran.delta': 'Delta', diff --git a/src/client/index.ts b/src/client/index.ts index 83e6721..9280d62 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -27,11 +27,11 @@ import { DICT_EN, DICT_ZH } from './i18n' import { registerContextCommand } from './command' import { makeContextModal } from './components/contextModal' import { makeOverviewPanel } from './components/overviewPanel' -import { makeSettingsCard, makePluginConfigCard } from './components/settingsCard' +import { makePluginConfigCard } from './components/settingsCard' import { watchInsightPage } from './insightPage' import { modalStoreOf } from './modalStore' import type { ClientCtx } from './services' -import { createContextSettings, type ConfigFormsFace, type SettingsField, type SettingsScopeBinderFace } from './settings' +import { createContextSettings, type ConfigFormsFace, type SettingsField } from './settings' import { makeContextView } from './components/contextView' import { makeContextJumpButton } from './components/contextJump' import { watchHistoryFaces } from './historyPage' @@ -87,7 +87,7 @@ function apply(ctx: ClientCtx): void { // Placement: the per-user `defaultPlacement` preference picks which // registration carries the view — the conversation tab, the right Sidebar - // (dsh 0.1.5-rc.1+, optional by contract), or both (the default). Each + // (every supported line, optional by contract), or both (the default). Each // mount owns its disposer so the watcher takes down exactly what a // preference flip drops (see placement.ts). ctx.effect(() => watchPlacement(settings, { @@ -148,7 +148,7 @@ function apply(ctx: ClientCtx): void { NS, ), 'dsh-context: insight page') - /** The injected face both preference cards ride: the settings store as the + /** The injected face the preference card rides: the settings store as the * framework's hooks-compartment `useContextSettings` seat, plus the set verb. */ const cardFace = (): { hooks: { contextSettings: typeof settings.store } @@ -158,31 +158,13 @@ function apply(ctx: ClientCtx): void { set: (field, value) => { settings.set(field, value) }, }) - // Per-user display preferences. Two generations, two transports and seats, - // switched by SERVICE PRESENCE (each deferred inject fires on exactly one - // side; a host without its service never runs the callback): - // - the settingsScope generation registers the card on the keyed - // `settings.plugin.item` slot (Settings → Plugins → Plugin - // configuration), binding the Host-served `dsh-context` namespace; - // - the Config-form generation (dsh 0.1.7+) retired that pair — there the - // card rides the configForms transport and the Plugins page's keyed - // `plugins.bundle.config` seat, alive only while the Host serves the - // namespace (the entry Config's volatile preference fields). - ctx.inject(['settingsScope'], (raw) => { - const c = raw as ClientCtx & { settingsScope?: SettingsScopeBinderFace } - const binder = c.settingsScope - if (binder === undefined) return - c.effect(() => settings.attach(binder.bind({ namespace: NS })), 'dsh-context: settings scope') - const SettingsCard = makeSettingsCard(kit) - c.slots.inject('settings.plugin.item', () => { - return c.slots.register( - { name: 'settings.plugin.item', key: NS, locale: NS, inject: cardFace }, - // Root-scope keyed slot: no sessionId on these props — the face - // (hooks + set) arrives through the registration's inject. - props => h(SettingsCard, props as unknown as Parameters[0]), - ) - }) - }) + // Per-user display preferences on the Config-form generation's transport + // and seat (the only ones the supported lines serve): the card rides the + // configForms service and the Plugins page's keyed `plugins.bundle.config` + // slot, alive only while the Host serves the namespace (the entry Config's + // volatile preference fields). The deferred inject fires only where the + // service exists — a host without it never runs the callback (no pending + // fiber, no throw), and the preferences simply hold their schema defaults. ctx.inject(['configForms'], (raw) => { const c = raw as ClientCtx & { configForms?: ConfigFormsFace } const forms = c.configForms diff --git a/src/client/insightPage.ts b/src/client/insightPage.ts index 19c75f7..19d8194 100644 --- a/src/client/insightPage.ts +++ b/src/client/insightPage.ts @@ -12,10 +12,9 @@ * click that selects the panel), the plugin contributes only the glyph * and the localized label thunk. * - * Both seats ship on every supported line (the `main` conversation panel and - * the panellist contract are present since 0.1.5-rc.1 — the compat matrix - * pins both spellings per baseline), so the registrations ride plain - * `slots.inject` declaration injects, like the conversation tab's. + * Both seats ship on every supported line (the compat matrix pins both + * spellings per baseline), so the registrations ride plain `slots.inject` + * declaration injects, like the conversation tab's. * * The per-user `insightsEntry` preference gates the pair: 'show' mounts both * registrations, 'hide' unwinds them (a hidden entry leaves the page no diff --git a/src/client/primitives.ts b/src/client/primitives.ts index d8c7665..adfaaeb 100644 --- a/src/client/primitives.ts +++ b/src/client/primitives.ts @@ -1,13 +1,13 @@ /** - * Generation-proof named imports from the ui-primitives platform module. + * Named imports from the ui-primitives platform module, resolved defensively. * - * The icon vocabulary renamed across the supported range — `Icon*Outline16` / - * `Icon*Outline14` through 0.1.5, `Icon*OutlineRegular` / `Icon*OutlineMedium` - * from 0.1.6 — and a renamed export arrives as `undefined` at runtime (the - * pinned devDep's types cannot see the other generation), which rendered as - * React error #130 inside the Context tab. Each icon here resolves through - * BOTH spellings at module load, modern first; a render-nothing fallback keeps - * a future rename a missing glyph instead of a crashed tab. + * Every supported line ships the `Icon*OutlineRegular` / `Icon*OutlineMedium` + * vocabulary, but the import arrives through the shell's module table at + * runtime: a renamed or absent export reads as `undefined`, which renders as + * React error #130 inside the Context tab. Each icon here resolves by name + * through a bounded read; a missing or non-component export (a future rename, + * a partial module mock, a hostile namespace) degrades to a render-nothing + * fallback — a missing glyph, never a crashed tab. */ import type { ReactElement } from 'react' @@ -17,39 +17,28 @@ import type { IconProps } from '@deepseek-ai/dsh-client-ui-primitives' /** One platform icon component, as the call sites render it. */ export type IconComponent = (props: IconProps) => ReactElement | null -/** The platform module's export surface, read by name (the two vocabularies meet here). */ +/** The platform module's export surface, read by name. */ const ns = primitives as unknown as Record /** - * Read one export through a bounded catch: partial module mocks (and any - * hostile namespace) may throw on the property access itself, which must - * degrade to "not this spelling", never crash the importer. - */ -function read(source: Record, key: string): unknown { - try { - return source[key] - } catch { - return undefined - } -} - -/** - * Resolve one icon across the range's two spellings: the modern - * `…OutlineRegular`/`…OutlineMedium` name first, then the legacy - * `…Outline16`/`…Outline14` one; neither present (or not a component) + * Resolve one icon by its export name; absent or not a component (or a + * namespace whose property READ throws — partial mocks, hostile modules) * renders nothing. `source` overrides the platform namespace for tests. */ -export function resolveIcon(modern: string, legacy: string, source: Record = ns): IconComponent { - const found = read(source, modern) ?? read(source, legacy) - return typeof found === 'function' ? found as IconComponent : () => null +export function resolveIcon(name: string, source: Record = ns): IconComponent { + try { + const found = source[name] + if (typeof found === 'function') return found as IconComponent + } catch { /* a hostile namespace: the fallback below */ } + return () => null } -export const IconBranch = resolveIcon('IconBranchOutlineRegular', 'IconBranchOutline16') -export const IconPlus = resolveIcon('IconPlusOutlineRegular', 'IconPlusOutline16') -export const IconCheck = resolveIcon('IconCheckOutlineRegular', 'IconCheckOutline16') -export const IconCopy = resolveIcon('IconCopyOutlineRegular', 'IconCopyOutline16') -export const IconClose = resolveIcon('IconCloseOutlineRegular', 'IconCloseOutline16') -export const IconSettings = resolveIcon('IconSettingsOutlineMedium', 'IconSettingsOutline14') -export const IconChevronDown = resolveIcon('IconChevronDownOutlineMedium', 'IconChevronDownOutline14') -export const IconChevronUp = resolveIcon('IconChevronUpOutlineMedium', 'IconChevronUpOutline14') -export const IconSearch = resolveIcon('IconSearchOutlineRegular', 'IconSearchOutline16') +export const IconBranch = resolveIcon('IconBranchOutlineRegular') +export const IconPlus = resolveIcon('IconPlusOutlineRegular') +export const IconCheck = resolveIcon('IconCheckOutlineRegular') +export const IconCopy = resolveIcon('IconCopyOutlineRegular') +export const IconClose = resolveIcon('IconCloseOutlineRegular') +export const IconSettings = resolveIcon('IconSettingsOutlineMedium') +export const IconChevronDown = resolveIcon('IconChevronDownOutlineMedium') +export const IconChevronUp = resolveIcon('IconChevronUpOutlineMedium') +export const IconSearch = resolveIcon('IconSearchOutlineRegular') diff --git a/src/client/revealScroll.ts b/src/client/revealScroll.ts index 56a5d05..d2c5b78 100644 --- a/src/client/revealScroll.ts +++ b/src/client/revealScroll.ts @@ -2,9 +2,8 @@ * Scroll an element's nearest scrollable ancestor so the element tops that * scrollport. The jump's landing reveal needs this host-agnostic: the * conversation tab scrolls the shared `[data-conversation-scroll]` container - * while the right Sidebar's scroller differs per generation (dockkit's pane - * body — a CSS-module class with no stable attribute — on the 0.1.5 line, the - * tab root itself on the 0.1.7 line), so the walk takes whichever ancestor + * while the right Sidebar's scroller differs per shell layout (the tab root + * itself on the supported lines), so the walk takes whichever ancestor * actually scrolls — overflowing style alone is not enough, a grow-with- * content box reports `auto` yet never moves — and the rect delta lands the * anchor flush regardless of the container's current position. diff --git a/src/client/services.ts b/src/client/services.ts index 30e5336..72fa2d1 100644 --- a/src/client/services.ts +++ b/src/client/services.ts @@ -30,7 +30,7 @@ export interface SlotRegistration { /** List slots dispatch on id + order. */ id?: string order?: number - /** Keyed slots (e.g. settings.plugin.item) dispatch on the entry key. */ + /** Keyed slots (e.g. plugins.bundle.config) dispatch on the entry key. */ key?: string /** optional dictionary namespace; the framework then synthesizes the `t` seat. */ locale?: string @@ -49,7 +49,7 @@ export interface SlotsService { /** * One guide-page capsule a right-Sidebar tab type contributes (dsh - * 0.1.5-rc.1+): the glyph, the title, and the optional one-line description, + * 0.1.7-rc.2+): the glyph, the title, and the optional one-line description, * exactly the fields `SidebarRightGuideEntry` carries. */ export interface SidebarGuideEntryLike { @@ -79,10 +79,10 @@ export interface SidebarTabDefinitionLike { /** * The right Sidebar's tab-type registry (`ctx.sidebarRightTabs`), as far as - * this plugin consumes it. OPTIONAL by contract: the service ships only on the - * 0.1.5 line (0.1.5-rc.1+ supported), so the plugin reaches it through a - * deferred inject and stays fully functional (no pending fiber, no throw) - * without it. + * this plugin consumes it. OPTIONAL by contract: a deployment may strip the + * service (and a below-baseline host, gated onto the fallback units, serves + * none of this half's seats), so the plugin reaches it through a deferred + * inject and stays fully functional (no pending fiber, no throw) without it. */ export interface SidebarTabsFace { register(definition: SidebarTabDefinitionLike): () => void @@ -815,13 +815,6 @@ export interface SessionScopeFace { export interface SessionsFace { scope(id: string): SessionScopeFace | undefined - /** - * Select a listed session as current — the retired selection spelling, - * still served through the 0.1.5 line and gone since the 0.1.6 selection - * refactor (issue #90: the verb's home is now the view owner). Re-proved at - * the call site via {@link openSessionVia}, which prefers the newer face. - */ - open?(id: string): void /** * Re-pull the session-list baseline (the rows' projection column included). * The overview rides it on open so host-side backfill rows (backfill.ts) @@ -956,12 +949,12 @@ export function openPathVia(ctx: ClientCtx): ((path: string) => void) | undefine /** * The right Sidebar's resource opener over `ctx.sidebarRight`, or undefined - * when this harness serves no such column (every line older than 0.1.5-rc.1) or - * the face is hostile — the caller then keeps its system-open degradation. - * Synchronous, and it reports whether the column took the address: `openResource` - * throws for a no-type-claims address or with no session surface mounted, and an - * unwired preview must fall back rather than become an inert click. The face is - * re-proved at call time (the service can land or be revoked across an HMR + * when this harness serves no such column (a stripped deployment) or the face + * is hostile — the caller then keeps its system-open degradation. Synchronous, + * and it reports whether the column took the address: `openResource` throws + * for a no-type-claims address or with no session surface mounted, and an + * unwired preview must fall back rather than become an inert click. The face + * is re-proved at call time (the service can land or be revoked across an HMR * reload), so the returned closure reads it per open. */ export function openResourceVia(ctx: ClientCtx): ((address: string) => boolean) | undefined { @@ -995,26 +988,19 @@ export function openResourceVia(ctx: ClientCtx): ((address: string) => boolean) /** * Jump to one session — the harness's own session-selection verb (issue #90). * Every supported line selects through the view owner (`uiWorkspace` - * .openSession, the sidebar row click's own verb); the sessions service's own - * `open` is the retired spelling (still served through the 0.1.5 line, gone - * since the 0.1.6 selection refactor) and stays as the degradation path for a - * composition without the workspace module. Both faces are re-proved per call - * (a service can land or be revoked across an HMR reload) and the verb is - * invoked bound (the service instance reads its own state); a generation - * serving neither, or a hostile face, swallows — the jump is best-effort by - * nature. + * .openSession, the sidebar row click's own verb, re-spelled + * `openSession(target: SessionTarget)` by the 0.1.6 selection refactor; a + * plain session id satisfies the target union). The face is re-proved per + * call (a service can land or be revoked across an HMR reload) and the verb + * is invoked bound (the service instance reads its own state); a composition + * without the workspace module, or a hostile face, swallows — the jump is + * best-effort by nature. */ export function openSessionVia(ctx: ClientCtx, id: string): void { try { const workspace = asRecord(ctx.get('uiWorkspace')) const open = workspace?.openSession - if (typeof open === 'function') { - open.call(workspace, id) - return - } - const sessions = asRecord(ctx.get('sessions')) - const legacy = sessions?.open - if (typeof legacy === 'function') legacy.call(sessions, id) + if (typeof open === 'function') open.call(workspace, id) } catch { /* absent or hostile face — the jump is best-effort */ } } diff --git a/src/client/settings.ts b/src/client/settings.ts index 600dbab..a82ff4d 100644 --- a/src/client/settings.ts +++ b/src/client/settings.ts @@ -1,9 +1,9 @@ /** * The plugin's user-settings binding (browser half). The Host-served * `dsh-context` namespace carries per-user display preferences; the Context - * tab reads them at mount, and the Plugin configuration card (Settings → - * Plugins) writes them through the settings scope. Both degrade to the - * schema defaults when the settings surface is absent (older host) or + * tab reads them at mount, and the Plugins page's configuration card writes + * them through the bound form. Both degrade to the schema defaults when the + * settings surface is absent (a host without the Config-form transport) or * read-only (remote browser in memory mode). * * The scope faces are minimally re-typed here (the services.ts discipline): @@ -18,18 +18,13 @@ import type { DefaultDeltaBase, DefaultFileSort, DefaultGranularity, DefaultDura // here so client-side consumers keep their canonical import path. export type { DefaultDeltaBase, DefaultFileSort, DefaultGranularity, DefaultDurationCurve, DefaultPlacement, DefaultToolSort, DefaultTrendMode, InsightsEntry, SettingsField } from '../shared/types' -/** The bound settings scope (ctx.settingsScope.bind result), as consumed. */ +/** The bound settings form (the ctx.configForms.get result), as consumed. */ export interface SettingsScopeLike { getSnapshot(): { status: string; value: unknown; writable: boolean } subscribe(listener: () => void): () => void set(field: string, value: unknown): Promise } -/** The ctx.settingsScope binder face, as consumed. */ -export interface SettingsScopeBinderFace { - bind(spec: { namespace: string }): SettingsScopeLike -} - /** * The ctx.configForms service face (the Config-form generation's settings * transport), as consumed: forms bind per namespace, and `whileServed` keeps diff --git a/src/client/settingsJump.ts b/src/client/settingsJump.ts index 633b037..f349b49 100644 --- a/src/client/settingsJump.ts +++ b/src/client/settingsJump.ts @@ -1,45 +1,22 @@ /** - * Best-effort jump to this plugin's preferences. Two generations of chrome: - * - dsh 0.1.7+ (Config-form generation): the preferences live on the - * bundle's page of the Plugins main panel — the jump clicks the panel's - * sidebar entry (its shipped aria-label), then follows the page with a - * bounded poll for the bundle card's open control, found by the shipped, - * locale-independent `data-plugin-package` list identity: the card lists - * once the page's package load settles, and inside it the one button - * that is not the enable switch is the title that opens the page. A - * poll that never finds the card (still loading, plugin absent, or the - * page already open on the detail view — the list unmounts there) - * degrades to the card list, and every step stays silent. - * - older lines: Settings → Plugins → Plugin configuration — the settings - * panel keeps its open state inside the shell component (no plugin-facing - * open face exists), so the jump drives the real chrome: click the - * sidebar's settings trigger, then the Plugins section nav row, both - * found by their shipped attributes and labels. - * The last leg — the card itself — is ours: the module also carries a - * short-lived in-bundle expand request that the settings-section card - * consumes on mount, so that jump lands on the configuration already open. - * EVERY step is individually guarded: a shell that doesn't match (different - * layout, missing sidebar) degrades the whole jump to a silent no-op — this - * path must never throw into the caller's render. + * Best-effort jump to this plugin's preferences on the Config-form + * generation (every supported line): the preferences live on the bundle's + * page of the Plugins main panel — the jump clicks the panel's sidebar entry + * (its shipped aria-label), then follows the page with a bounded poll for the + * bundle card's open control, found by the shipped, locale-independent + * `data-plugin-package` list identity: the card lists once the page's package + * load settles, and inside it the one button that is not the enable switch is + * the title that opens the page. A poll that never finds the card (still + * loading, plugin absent, or the page already open on the detail view — the + * list unmounts there) degrades to the card list, and every step stays + * silent: a shell that doesn't match (different layout, missing sidebar) + * degrades the whole jump to a silent no-op — this path must never throw + * into the caller's render. */ -let requestedAt = 0 - -/** Raise a short-lived request for the plugin's settings card to mount expanded. */ -export function requestCardExpand(now: number = Date.now()): void { - requestedAt = now -} - -/** Consume the request once; true only while it is still fresh (younger than maxAgeMs). */ -export function consumeCardExpand(now: number = Date.now(), maxAgeMs = 5000): boolean { - const fresh = requestedAt > 0 && now - requestedAt < maxAgeMs - requestedAt = 0 - return fresh -} - /** * The shipped Plugins label in both locales — the main-panel entry's - * aria-label and the settings section's nav row text are the same word. + * aria-label. */ const PLUGIN_LABELS = new Set(['Plugins', '插件']) @@ -71,55 +48,26 @@ function findCardOpenControl(doc: Document): HTMLButtonElement | undefined { return open.length === 1 ? open[0] : undefined } -/** The sidebar's settings triggers: dialog semantics plus an expanded flag. */ -function findTriggers(doc: Document): HTMLButtonElement[] { - return buttonsOf(doc) - .filter(b => b.getAttribute('aria-haspopup') === 'dialog' && b.hasAttribute('aria-expanded')) -} - -/** The Plugins section's nav row, matched by its shipped label text. */ -function findSectionRow(doc: Document): HTMLButtonElement | undefined { - return buttonsOf(doc).find(b => PLUGIN_LABELS.has(b.textContent.trim())) -} - export function openPluginSettings( doc: Document = document, schedule: (run: () => void, ms: number) => void = (run, ms) => { window.setTimeout(run, ms) }, ): void { try { - // Config-form generation first: the panel entry is the one seat both - // locales ship. The follow-up poll rides the same scheduler contract as - // the legacy leg, so tests drive every tick. + // The panel entry is the one seat both locales ship; without it this + // shell has no Plugins page and the jump is a no-op. const panel = findPanelEntry(doc) - if (panel !== undefined) { - panel.click() - const poll = (left: number): void => { - try { - const open = findCardOpenControl(doc) - if (open !== undefined) { - open.click() - return - } - } catch { /* a hostile card: stay on the card list */ } - if (left > 0) schedule(() => { poll(left - 1) }, 100) - } - schedule(() => { poll(POLL_TICKS) }, 200) - return - } - const triggers = findTriggers(doc) - const open = triggers.find(b => b.getAttribute('aria-expanded') === 'true') - // No dialog trigger at all: this shell has no settings panel — no-op. - if (open === undefined && triggers.length === 0) return - // A collapsed trigger opens the panel; an already-open one is left alone. - triggers.find(b => b.getAttribute('aria-expanded') !== 'true')?.click() - // Only a chrome with a settings dialog raises the expand request, and - // only once the trigger click survived — a degraded jump leaves no - // stale signal behind. - requestCardExpand() - schedule(() => { + if (panel === undefined) return + panel.click() + const poll = (left: number): void => { try { - findSectionRow(doc)?.click() - } catch { /* no Plugins row on this host: stay on the opened section */ } - }, 80) + const open = findCardOpenControl(doc) + if (open !== undefined) { + open.click() + return + } + } catch { /* a hostile card: stay on the card list */ } + if (left > 0) schedule(() => { poll(left - 1) }, 100) + } + schedule(() => { poll(POLL_TICKS) }, 200) } catch { /* the settings surface doesn't match: silent no-op */ } } diff --git a/src/client/sidebar.ts b/src/client/sidebar.ts index 2003d0b..624c1b7 100644 --- a/src/client/sidebar.ts +++ b/src/client/sidebar.ts @@ -1,5 +1,5 @@ /** - * The right Sidebar's Context tab (dsh 0.1.5-rc.1+). + * The right Sidebar's Context tab (dsh 0.1.7-rc.2+). * * The tab reuses the Context conversation-view component VERBATIM: the * `sidebar.right.pane.tab` seat is session-scoped and delivers the same @@ -11,14 +11,15 @@ * glyph, title, and description line, plus the chip-title seat that puts the * same glyph beside the label once the tab is open (`icon.tsx`). * - * OPTIONAL BY CONTRACT. `ctx.sidebarRightTabs` and the seat ship only on the - * 0.1.5 line (0.1.5-rc.1+ supported); the registration therefore rides a - * DEFERRED inject (the plugin's hard injects stay `slots` + `locale`), so on - * every older supported line the callback never fires, the plugin fiber never - * pends, and nothing is registered. The registry is re-proved structurally and - * the whole registration is guarded: a foreign or hostile registry (a throwing - * `register`, a taken id/kind) leaves the sidebar without the tab instead of - * taking the browser down. + * OPTIONAL BY CONTRACT. `ctx.sidebarRightTabs` and the seats ship on every + * supported line, but a deployment may strip them (and a below-baseline host, + * gated onto the fallback units, serves none of this half's seats); the + * registration therefore rides a DEFERRED inject (the plugin's hard injects + * stay `slots` + `locale`), so a composition without the service never fires + * the callback, the plugin fiber never pends, and nothing is registered. The + * registry is re-proved structurally and the whole registration is guarded: a + * foreign or hostile registry (a throwing `register`, a taken id/kind) leaves + * the sidebar without the tab instead of taking the browser down. * * @module dsh-context/client/sidebar */ @@ -87,9 +88,9 @@ export function watchSidebarContextTab( { name: 'sidebar.right.pane.tab', key: SIDEBAR_CONTEXT_ID, locale: ns }, (props: { sessionId?: string } & Record) => view({ ...props, host: 'sidebar' }), ))) - // The chip title seat (0.1.5-rc.1+): the emblem beside the label, the - // files type's own idiom. Registered under the type id so the kit - // dispatches it for this type's tabs only. + // The chip title seat: the emblem beside the label, the files type's + // own idiom. Registered under the type id so the kit dispatches it for + // this type's tabs only. own(injected.slots.inject('sidebar.right.pane.tab.title', () => injected.slots.register( { name: 'sidebar.right.pane.tab.title', key: SIDEBAR_CONTEXT_ID }, makeContextTabTitle(t), diff --git a/src/client/styles/base.css b/src/client/styles/base.css index 501b96d..ae51a80 100644 --- a/src/client/styles/base.css +++ b/src/client/styles/base.css @@ -8,18 +8,17 @@ */ .lc-root { padding: 16px 20px 32px; overflow-y: auto; height: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 13px; } /* - Right Sidebar panel (dsh 0.1.5-rc.1+): the body draws NO padding of its own - (dockkit leaves each tab its own insets), so the root keeps a 12px frame on - all four sides — the harness's own body inset — instead of the wide shared - chrome. The root KEEPS its own scroller: the 0.1.5 line renders the tab - straight into the scrolling dockkit pane body, while the 0.1.7 line wraps it - in a definite-height `.tabBody` whose `overflow: hidden` would clip a grown - root out of reach — `height: 100%` resolves against whichever definite-height - host the running generation provides (and degrades to the root growing under - an auto-height one, where the pane scrolls instead: one scroller either way). + Right Sidebar panel: the body draws NO padding of its own (dockkit leaves + each tab its own insets), so the root keeps a 12px frame on all four sides — + the harness's own body inset — instead of the wide shared chrome. The root + KEEPS its own scroller: the supported lines wrap the tab in a + definite-height `.tabBody` whose `overflow: hidden` would clip a grown root + out of reach — `height: 100%` resolves against whichever definite-height + host the running shell provides (and degrades to the root growing under an + auto-height one, where the pane scrolls instead: one scroller either way). Scoped by the panel's own markers, which exist only inside the right column - (the open panel and the 0.1.5 float host that carries a tab dragged out of - it); the conversation tab and the /context modal are unaffected. + (the open panel and the float host that carries a tab dragged out of it); + the conversation tab and the /context modal are unaffected. */ [data-sidebar-right-open] .lc-root, [data-sidebar-right-float-host] .lc-root { padding: 12px; } diff --git a/src/client/styles/settings.css b/src/client/styles/settings.css index f02b991..c9d5b5d 100644 --- a/src/client/styles/settings.css +++ b/src/client/styles/settings.css @@ -1,16 +1,5 @@ -/* The settings-entry card at the foot of the Context tab (settingsCard.tsx). */ -.lc-settings-card { background: var(--dsw-alias-bg-layer-3); border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; transition: border-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); } -.lc-settings-card:hover { border-color: var(--dsw-alias-label-dimmed); } -.lc-settings-open, .lc-settings-open:hover { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-label-dimmed); } -.lc-settings-head { width: 100%; appearance: none; border: 0; background: none; font: inherit; color: inherit; text-align: left; cursor: pointer; display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-radius: 12px; } -.lc-settings-headtext { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; } -.lc-settings-name { font-size: 15px; font-weight: 600; line-height: 1.4; color: var(--dsw-alias-label-primary); } -.lc-settings-desc { font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); } -.lc-settings-chevron { flex: none; color: var(--dsw-alias-label-tertiary); transition: transform var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); } -.lc-settings-open .lc-settings-chevron { transform: rotate(180deg); } -.lc-settings-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding: 4px 0 12px; } -/* The Plugins-page card (Config-form generation): the page draws the section - chrome, so the rows render flat inside their own block. */ +/* The preference card on the Plugins page (settingsCard.tsx): the page draws + the section chrome, so the rows render flat inside their own block. */ .lc-settings-prefs { padding: 4px 0; } .lc-settings-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; } .lc-settings-label { flex: 1; min-width: 0; font-size: 14px; color: var(--dsw-alias-label-primary); } diff --git a/src/client/viewFocus.ts b/src/client/viewFocus.ts index a0119ae..1257edf 100644 --- a/src/client/viewFocus.ts +++ b/src/client/viewFocus.ts @@ -45,10 +45,10 @@ export function takeContextFocus(sessionId: string): number | null { /** * Open the Context tab on the right Sidebar over `ctx.sidebarRight.openTab` * (the column expands in the same step). OPTIONAL seam, re-proved at call - * time: a harness without the service (every line older than 0.1.5-rc.1), a - * placement that registered no tab type, no mounted session surface, or a - * hostile face all report false so the caller keeps its conversation-tab - * fallback. + * time: a harness without the service (a stripped deployment, a below-baseline + * gated host), a placement that registered no tab type, no mounted session + * surface, or a hostile face all report false so the caller keeps its + * conversation-tab fallback. */ export function openContextSidebar(ctx: ClientCtx): boolean { try { diff --git a/src/host/backfill.ts b/src/host/backfill.ts index ad98bdb..ccfcdde 100644 --- a/src/host/backfill.ts +++ b/src/host/backfill.ts @@ -44,7 +44,7 @@ */ import type { Context } from '@deepseek-ai/cordis' -import { interruptedTurnClosers, SessionLogOffset } from '@deepseek-ai/dsh-session' +import { interruptedTurnClosers } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' import { type ColdReadGate, makeColdReadGate } from './coldRead' @@ -58,13 +58,12 @@ interface SessionQueryLike { /** * The cache's two cold-path verbs, as consumed (re-proved at runtime). The - * cached-rows probe's face differs across the supported range — dsh - * 0.1.5-rc.1 takes `(meta, inheritedEventCount, keys)` while dsh 0.1.7-rc.2 - * dropped the offset (`(meta, keys)`, the lifecycle identity riding the - * header alone) — so {@link servesRows} re-proves the shape per call. + * cached-rows probe rides the current face — `(meta, keys?)`, the lifecycle + * identity riding the header alone (the offset parameter belonged to the + * dropped 0.1.5 line and never existed on a supported one). */ interface ProjectionCacheLike { - cachedSnapshot(meta: SessionHeader, offsetOrKeys: unknown, keys?: readonly string[]): unknown + cachedSnapshot(meta: SessionHeader, keys?: readonly string[]): unknown coldSnapshot(header: SessionHeader, inheritedEventCount: unknown, events: readonly SessionEvent[]): unknown } @@ -125,24 +124,6 @@ function headerOf(record: unknown): SessionHeader | null { /** The probe keys: the projection keys the cache must already serve. */ const PROBE_KEYS = ['contextActivity', 'contextTimeline'] -/** - * Probe the cache through the face the running generation serves. The older - * spelling is tried first and the newer one answers the fallback: on a - * 0.1.7-rc.2 cache the 3-arg call binds the branded offset onto `keys`, - * where `new Set(0)` throws as soon as a served record is reached — while a - * record-less session returns `undefined` without throwing, which is the - * correct "not served" either way. On 0.1.5-rc.1 the 3-arg face answers - * directly; if it ever throws there, the 2-arg retry degrades to the same - * "not served" verdict through the caller's catch. - */ -function cachedSnapshotOf(cache: ProjectionCacheLike, header: SessionHeader): unknown { - try { - return cache.cachedSnapshot(header, SessionLogOffset(0), PROBE_KEYS) - } catch { - return cache.cachedSnapshot(header, PROBE_KEYS) - } -} - /** Whether the cache already serves BOTH projection rows for this header (nothing to backfill). */ function servesRows(cache: ProjectionCacheLike, header: SessionHeader): boolean { try { @@ -151,8 +132,9 @@ function servesRows(cache: ProjectionCacheLike, header: SessionHeader): boolean // misses and the session takes the cold-read path — correct either way. // Both keys must be served: a version-stale row (the timeline's head // gained `lastUser` at stateVersion 20) reads as absent here, so the - // session's stale rows get their one cold refold at startup. - const block = asRecord(cachedSnapshotOf(cache, header)) + // session's stale rows get their one cold refold at startup. A hostile + // face's throw reads as "not served" the same way. + const block = asRecord(cache.cachedSnapshot(header, PROBE_KEYS)) const values = asRecord(block?.values) return values !== null && values.contextActivity !== undefined diff --git a/src/host/balance.ts b/src/host/balance.ts index 5af5128..ee71f23 100644 --- a/src/host/balance.ts +++ b/src/host/balance.ts @@ -7,11 +7,11 @@ * so the HOST reads it and serves the redacted figures. The connection facts * resolve per request, exactly as the DeepSeek provider serves its own * requests: its settings row carries the credential ref and the optional - * endpoint override, read through whichever face the running line serves - * (deepseekSectionOf), and the credentials service resolves the ref to the - * key. Any missing fact — the provider absent, no settings service, no key — - * answers a typed `null`, as does a failed or malformed platform read: the - * capsule renders nothing rather than a stale figure. + * endpoint override (read off the Config-form projection, deepseekSectionOf), + * and the credentials service resolves the ref to the key. Any missing fact — + * the provider absent, no settings service, no key — answers a typed `null`, + * as does a failed or malformed platform read: the capsule renders nothing + * rather than a stale figure. * * The transport is Connection's fetch-route registry (the same authenticated * `/api` fence host/detail.ts mounts), registered through a deferred inject @@ -29,13 +29,10 @@ import type { PlatformBalance, PlatformBalanceEntry } from '../shared/types' export const BALANCE_ROUTE = '/api/dsh-context/balance' /** The settings id the DeepSeek API-key provider has served its connection - * section under: the V3 registered namespace, kept as the entry id on V4+ - * product profiles. */ + * section under: the pre-Config-form registered namespace, kept as the entry + * id on product profiles. */ const DEEPSEEK_SETTINGS_NS = 'llm-deepseek' -/** llm-deepseek's default credential ref (the env-var name its section resolves). */ -const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY' - /** The platform's public API root (llm-deepseek's PUBLIC_BASE_URL default). */ const PUBLIC_BASE_URL = 'https://api.deepseek.com' @@ -43,13 +40,12 @@ const PUBLIC_BASE_URL = 'https://api.deepseek.com' const FETCH_TIMEOUT_MS = 10_000 /** - * The harness `settings` service, as far as the route consumes it. The face - * is generation-specific: V3 reads a registered section back (`get`), while - * V4+ retired that face and projects every configurable entry instead - * (`describe`) — the route folds over whichever the running line serves. + * The harness `settings` service, as far as the route consumes it: the + * Config-form generation's `describe()` projection of every configurable + * entry (the only face the supported lines serve — the registered-section + * `get(ns)` read retired with them). */ interface SettingsHostFace { - get?(ns: string): unknown describe?(): unknown } @@ -87,21 +83,17 @@ function amountOf(value: unknown): number | null { interface DeepSeekFacts { baseUrl: string; apiKey: string } /** - * The DeepSeek API-key provider's settings section, read through whichever - * face the running line serves. V3's `get` reads the registered - * `llm-deepseek` section directly; V4+ retired that face, so its `describe()` - * projection is folded instead — the provider's row is the one whose served - * value declares the top-level `apiKeyEnv` credential ref (the volatile shape - * only that provider declares at the section root), preferred under the entry - * ids the generations have served it as (`llm-deepseek` on product profiles, - * `llm-deepseek-api-key` elsewhere). Rows fold in isolation — a hostile row - * drops whole, valid siblings keep serving — and a describe that throws is no - * section at all. + * The DeepSeek API-key provider's settings section, folded from the + * Config-form `describe()` projection: the provider's row is the one whose + * served value declares the top-level `apiKeyEnv` credential ref (the + * volatile shape only that provider declares at the section root), preferred + * under the entry ids the generations have served it as (`llm-deepseek` on + * product profiles, `llm-deepseek-api-key` elsewhere). Rows fold in + * isolation — a hostile row drops whole, valid siblings keep serving — and a + * describe that throws is no section at all. */ function deepseekSectionOf(ctx: Context): Record | null { const settings = ctx.get('settings') as SettingsHostFace | undefined - const section = typeof settings?.get === 'function' ? asRecord(settings.get(DEEPSEEK_SETTINGS_NS)) : null - if (section !== null) return section if (typeof settings?.describe !== 'function') return null try { const rows = settings.describe() @@ -134,9 +126,9 @@ function deepseekSectionOf(ctx: Context): Record | null { async function resolveFacts(ctx: Context): Promise { const section = deepseekSectionOf(ctx) if (section === null) return null - const apiKeyEnv = typeof section.apiKeyEnv === 'string' && section.apiKeyEnv !== '' - ? section.apiKeyEnv - : DEFAULT_API_KEY_ENV + // The matched row's apiKeyEnv is a proved non-empty string (the match rule), + // and the provider's schema defaults it anyway. + const apiKeyEnv = section.apiKeyEnv as string const baseUrl = typeof section.baseURL === 'string' && section.baseURL !== '' ? section.baseURL : PUBLIC_BASE_URL diff --git a/src/host/compat.ts b/src/host/compat.ts index a1bcb75..f707700 100644 --- a/src/host/compat.ts +++ b/src/host/compat.ts @@ -2,7 +2,7 @@ * Session-projection unit contract compatibility layer. * * Supported harness baseline (see docs/compatibility.md): dsh - * 0.1.5-rc.1+. On it, the session-projection registry drives the unit + * 0.1.7-rc.2+. On it, the session-projection registry drives the unit * contract (introduced in dsh 0.1.1-rc.1, replacing the pre-0.1.1 * `{ schema, view }` shape): the real projection units emit only the modern * shape, while the below-baseline fallback units (fallback.ts) still carry @@ -24,7 +24,7 @@ * that contract as-is (both arguments go unobserved). * * The installed devDependency types pin the oldest supported surface - * (0.1.5-rc.1), whose `wire?` is optional; this plugin's units are always + * (0.1.7-rc.2), whose `wire?` is optional; this plugin's units are always * client-visible, and the registry's wired-register overload demands `wire` * PRESENT — which the dts's optional `wire?` fails. The contract is * therefore mirrored here as {@link ProjectionDefinition} to keep both @@ -35,7 +35,7 @@ import type { z } from 'zod' import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types' import type { SessionEvent } from '@deepseek-ai/dsh-session' -/** The session-projection unit contract served by dsh 0.1.5-rc.1+ (local mirror). */ +/** The session-projection unit contract served by dsh 0.1.7-rc.2+ (local mirror). */ export interface ProjectionDefinition { key: K stateSchema: z.ZodType diff --git a/src/host/config.ts b/src/host/config.ts index 0824c53..f2091bc 100644 --- a/src/host/config.ts +++ b/src/host/config.ts @@ -9,11 +9,9 @@ * live form from this same schema — there the namespace is the entry id * (`dsh-context`) and only the `.volatile()` preference fields are served and * editable. Bounds edits remount the entry (the folds read them at apply); - * preference edits commit volatile-only and never remount. On the older lines - * the preferences are inert here — their surface is the registered settings - * namespace (settings.ts). The `Config` type describes the RESOLVED shape - * cordis hands to `apply`; the raw patch values are partial and the schema - * fills every default. + * preference edits commit volatile-only and never remount. The `Config` type + * describes the RESOLVED shape cordis hands to `apply`; the raw patch values + * are partial and the schema fills every default. */ import z from '@deepseek-ai/schemastery' @@ -73,8 +71,8 @@ export const DEFAULT_BOUNDS: FoldBounds = { /** * Mark a field live-editable where the harness's schemastery ships the * `.volatile()` modifier (the Config-form generation reads the mark to serve - * the field on the Plugins page); a plain field on the older lines whose - * schemastery predates the modifier. + * the field on the Plugins page); a plain field where the modifier is absent + * (the feature-detect keeps one schema serving every harness the gate admits). */ export function volatileField(field: S): S { const volatile = (field as unknown as { volatile?: () => S }).volatile diff --git a/src/host/fold.ts b/src/host/fold.ts index 4b7cff9..e3d288e 100644 --- a/src/host/fold.ts +++ b/src/host/fold.ts @@ -502,20 +502,21 @@ interface SurfaceEventLike { interface MessageLike { content?: ContentBlock[] source?: MessageSource - error?: boolean /** - * The V4 tool-result error mark (`tool/result.message.isError`): V3 carried - * it inside the `tool-result` content wrapper block, V4 lifted it onto the - * message and made the event-level `data.error` identity optional — so the - * flag is read from BOTH spellings. + * The tool-result error mark (`tool/result.message.isError`): the V4 log + * generation's one spelling — the event-level `error` metadata (when + * present at all) is admitted only alongside it, and the harness's own + * admission validation refuses a row whose mark disagrees. */ isError?: unknown } /** * A message nested under data.message. A structural read keeps malformed - * payloads total and handles developer messages absent from the V3 - * dependency's deriveEventMessage. User messages instead live in data itself. + * payloads total, and — unlike the dependency's deriveEventMessage, which + * projects an empty-content message to null — keeps an empty developer + * message's surface position at zero tokens (the harness's message-projection + * rule the fold's surface mirrors). User messages instead live in data itself. */ function messageOf(data: Record | undefined): MessageLike | null { const message = data?.message @@ -523,21 +524,16 @@ function messageOf(data: Record | undefined): MessageLike | nul } /** - * The first full text block, recursing through nested content blocks (a tool - * result wraps its text in a `tool-result` block). Unlike `firstText` this - * must NOT truncate/normalize: the skill name is matched off the raw + * The first full text block of a tool result's content. Unlike `firstText` + * this must NOT truncate/normalize: the skill name is matched off the raw * `` wrapper. */ -function nestedText(blocks: unknown): string { +function firstFullText(blocks: unknown): string { if (!Array.isArray(blocks)) return '' for (const item of blocks) { if (item === null || typeof item !== 'object') continue const block = item as ContentBlock if (block.type === 'text' && typeof block.text === 'string' && block.text !== '') return block.text - if (block.content !== undefined) { - const nested = nestedText(block.content) - if (nested !== '') return nested - } } return '' } @@ -548,7 +544,7 @@ function nestedText(blocks: unknown): string { * is recovered from the content rather than trusted from the call envelope. */ function skillNameOf(msg: MessageLike | null | undefined): string { - const text = nestedText(msg?.content) + const text = firstFullText(msg?.content) const match = text.match(/ = {} for (const k in st.callNames) { - if (k !== srcId && k !== blockId) kept[k] = st.callNames[k] + if (k !== srcId) kept[k] = st.callNames[k] } st.callNames = kept } - if (data?.error || message?.isError === true) node.err = true + if (message?.isError === true) node.err = true } else if (source?.kind === 'skill-invocation') { node.skill = typeof source.name === 'string' ? source.name : '?' } else if (source?.kind === 'plugin') { @@ -1177,14 +1164,15 @@ export function applyTimeline(state: TimelineState, event: TimelineEvent, bounds } case 'user/message': case 'developer/message': { - // The V3 dependency's deriveEventMessage has no developer case; - // supported V4 logs nest that message under data.message. + // The dependency's deriveEventMessage projects an empty developer + // message to null; the structural read keeps its surface position at + // zero tokens (the harness's own message-projection rule). const developer = event.type === 'developer/message' const msg = developer ? messageOf(data) : deriveEventMessage(event as never) as MessageLike | null if (developer && (msg === null || !Array.isArray(msg.content))) return state const s = ensure(['surface', 'sums', 'archived', 'events']) bumpDetailRev(s) - const node = applySurface(s, event, event.type, data, msg) + const node = applySurface(s, event, event.type, msg) const source = msg?.source if (developer || isInjection(source)) { const rec: ContextEventRecord = { @@ -1229,23 +1217,18 @@ export function applyTimeline(state: TimelineState, event: TimelineEvent, bounds } case 'tool/result': { // The model-visible message is data.message; `deriveEventMessage` - // returns that directly (the envelope also carries callId/error; pricing - // the envelope would miss all content). + // returns that directly (pricing the envelope would miss all content). const toolMsg = deriveEventMessage(event as never) as MessageLike | null // Read the pairing BEFORE applySurface consumes it (consume-once): // the armed call's name/arguments pair this result into file ops, and // the result's callId is the flush key for buffered Code-Mode ops. const msgSource = toolMsg?.source as { callId?: unknown } | undefined const srcId = msgSource?.callId - const firstBlock = toolMsg?.content?.[0] as { toolCallId?: unknown; isError?: unknown } | undefined - const blockId = firstBlock?.toolCallId - const pendingEntry = (typeof srcId === 'string' ? state.callNames[srcId] : undefined) - ?? (typeof blockId === 'string' ? state.callNames[blockId] : undefined) - const buffered = (typeof srcId === 'string' ? state.pendingCodeOps?.[srcId] : undefined) - ?? (typeof blockId === 'string' ? state.pendingCodeOps?.[blockId] : undefined) + const pendingEntry = typeof srcId === 'string' ? state.callNames[srcId] : undefined + const buffered = typeof srcId === 'string' ? state.pendingCodeOps?.[srcId] : undefined const s = ensure(['surface', 'sums', 'archived', 'events', 'fileOps', 'timing']) bumpDetailRev(s) - const node = applySurface(s, event, event.type, data, toolMsg) + const node = applySurface(s, event, event.type, toolMsg) // An answered question prompt is a human input too (whole-session // tally): the result only carries its tool name when it pairs with // the armed call, so an unpaired/foreign one counts nothing. @@ -1268,7 +1251,7 @@ export function applyTimeline(state: TimelineState, event: TimelineEvent, bounds tool: pendingEntry.name, argsRaw: pendingEntry.argsRaw, meta: data?.meta, - err: Boolean(data?.error) || toolMsg?.isError === true || firstBlock?.isError === true, + err: toolMsg?.isError === true, }) pushFileOps(s, ops) } @@ -1283,7 +1266,7 @@ export function applyTimeline(state: TimelineState, event: TimelineEvent, bounds }))) const kept: Record = {} for (const k in s.pendingCodeOps) { - if (k !== srcId && k !== blockId) kept[k] = s.pendingCodeOps[k] + if (k !== srcId) kept[k] = s.pendingCodeOps[k] } if (Object.keys(kept).length > 0) s.pendingCodeOps = kept else delete s.pendingCodeOps @@ -1453,7 +1436,7 @@ export function applyTimeline(state: TimelineState, event: TimelineEvent, bounds // null when the content array is empty (usage-only events project to no // message — same rule as dsh's surface fold). const asstMsg = deriveEventMessage(event as never) as MessageLike | null - applySurface(s, event, event.type, data, asstMsg) + applySurface(s, event, event.type, asstMsg) break } case 'session/end-seed': { diff --git a/src/host/index.ts b/src/host/index.ts index c6cc1a2..fbcf8f9 100644 --- a/src/host/index.ts +++ b/src/host/index.ts @@ -30,7 +30,6 @@ import { Config, resolveBounds } from './config' import { watchDetailChannel } from './detail' import { createFallbackActivityDefinition, createFallbackHeadersDefinition, createFallbackTimelineDefinition } from './fallback' import { createContextHeadersDefinition } from './headers' -import { installSettings } from './settings' import { watchStepIdentity } from './stepIdentity' import { createContextTimelineDefinition } from './timeline' import { detectHarnessVersion } from './version' @@ -95,7 +94,6 @@ export function apply(ctx: Context, config: Config): void { // demand — the dashboard (the rows' only reader) summons the pass through // the plugin's fetch route the first time it opens. watchActivityBackfill(ctx, coldReads) - installSettings(ctx) } // ---- public type surface (stable for downstream consumers) ------------------- diff --git a/src/host/logShapes.ts b/src/host/logShapes.ts index f5ceaed..6969ce8 100644 --- a/src/host/logShapes.ts +++ b/src/host/logShapes.ts @@ -1,19 +1,19 @@ /** * Shape-driven readers over the durable session-event vocabulary — the ONE * place the plugin's log spellings meet. The supported range (dsh - * 0.1.5-rc.1+, session formats V3/V4) speaks ONE dialect: + * 0.1.7-rc.2+, session format V4) speaks ONE dialect: * `system/message` surface nodes, `assistant/message.data.stream` / * `assistant/attempt.data.stream` (packed delta runs plus raw chunk * records), `SurfaceOp { startSeq, endSeq }`, `tool/ptc-dispatch`. * * The fold still reads SHAPES, never a detected harness version: the raw log - * is untrusted input at every layer, and the supported harness refuses — - * never rewrites — a legacy-format log, so anything that reaches these - * readers is current-dialect or hostile. Every reader is total over - * untrusted input — a malformed record yields "nothing here", never a throw - * (the projection registry drives the fold without an error boundary; one - * throw stalls the unit's push feed and the browser waits on "loading" - * forever). + * is untrusted input at every layer, and the supported harness migrates every + * older log to V4 on read (refusing — never serving raw — one it cannot + * convert), so anything that reaches these readers is current-dialect or + * hostile. Every reader is total over untrusted input — a malformed record + * yields "nothing here", never a throw (the projection registry drives the + * fold without an error boundary; one throw stalls the unit's push feed and + * the browser waits on "loading" forever). * * @module dsh-context/host/log-shapes */ diff --git a/src/host/pricing.ts b/src/host/pricing.ts index 51962cb..b79f9c7 100644 --- a/src/host/pricing.ts +++ b/src/host/pricing.ts @@ -60,9 +60,6 @@ function estimateBlocks(blocks: unknown): number { tokens += Math.ceil((block.name || '').length / CHARS_PER_TOKEN) + Math.ceil((block.arguments || '').length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD break - case 'tool-result': - tokens += estimateBlocks(block.content) + BLOCK_OVERHEAD - break case 'image': { const ref = block.attachment const priced = ref !== null && typeof ref === 'object' @@ -99,9 +96,11 @@ export function estimateToolSchema(tool: unknown): number { } /** - * Count image blocks in a message payload, recursing into nested content (tool-result blocks carry their inner blocks) — seeds each node's - * `imgs`, which the stats board's image cell sums over the LIVE surface (compacted/pruned messages stop counting). - */ + * Count image blocks in a message payload, recursing into nested content + * (a producer extension block may carry inner blocks) — seeds each node's + * `imgs`, which the stats board's image cell sums over the LIVE surface + * (compacted/pruned messages stop counting). + */ export function imageCountOf(blocks: unknown): number { let count = 0 if (!Array.isArray(blocks)) return 0 diff --git a/src/host/settings.ts b/src/host/settings.ts deleted file mode 100644 index 4534cc6..0000000 --- a/src/host/settings.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * dsh-context user settings — the per-user preference namespace served to - * browsers through the harness settings seam (`ctx.settings`). - * - * Distinct from the cordis `config:` block (config.ts), which is - * deployment-level: the settings document is per-user and GUI-editable (the - * Plugin configuration card the browser half registers, keyed by this - * namespace). The Host half only REGISTERS the namespace — every field is a - * client-side display preference, so nothing here is consumed on the Host. - * - * The register face is generation-specific: dsh V4+ removed - * `settings.register` (configuration forms derive from each loader entry's - * own Config schema there, a surface this plugin does not publish its - * preferences through yet), so the callback feature-detects the method and - * stays inert on a generation without it. - * - * Optional composition: a deployment without a settings provider never runs - * the inject callback and browsers simply see no card (schema defaults win). - */ - -import type { Context } from '@deepseek-ai/cordis' -import type { SettingsNamespace } from '@deepseek-ai/dsh-settings' -import z from '@deepseek-ai/schemastery' - -/** The namespace is the join key between the Host registration and the browser card. */ -export const SETTINGS_NAMESPACE = 'dsh-context' - -// The preference vocabulary is declared once in shared/types.ts; re-exported -// here so host-side consumers keep their canonical import path. -export type { DefaultDeltaBase, DefaultFileSort, DefaultGranularity, DefaultDurationCurve, DefaultPlacement, DefaultToolSort, DefaultTrendMode, InsightsEntry, PluginSettings } from '../shared/types' - -/** Section schema: also the wire envelope the browser scope validates against. */ -export const SettingsSchema = z.object({ - // Loose: a stale persisted value degrades to the default instead of breaking the section. - defaultPlacement: z.union(['all', 'tab', 'sidebar']).default('all').loose(), - defaultGranularity: z.union(['step', 'turn']).default('step'), - defaultTrendMode: z.union(['total', 'delta']).default('total').loose(), - defaultDeltaBase: z.union(['step', 'turn']).default('step').loose(), - defaultToolSort: z.union(['size', 'count', 'name']).default('count').loose(), - defaultFileSort: z.union(['count', 'latest', 'path']).default('count').loose(), - // Loose so a stale value also reads as the default (`show`): a config - // problem must never take the panel's entry away. - insightsEntry: z.union(['show', 'hide']).default('show').loose(), - defaultDurationCurve: z.union(['show', 'hide']).default('show').loose(), -}) - -/** Serve the namespace while a settings provider with the register face is composed; inert otherwise. */ -export function installSettings(ctx: Context): void { - ctx.inject(['settings'], (sctx) => { - const service = sctx.settings as typeof sctx.settings & { register?: (ns: SettingsNamespace, schema: unknown) => unknown } - if (typeof service.register !== 'function') return - // The settings packages register the raw namespace string (the - // `settingsNamespace()` brand helper is long gone); the branded cast - // only satisfies the dsh-settings type face. - service.register(SETTINGS_NAMESPACE as SettingsNamespace, SettingsSchema) - }) -} diff --git a/src/host/timeline.ts b/src/host/timeline.ts index 891371a..6e17bae 100644 --- a/src/host/timeline.ts +++ b/src/host/timeline.ts @@ -417,11 +417,15 @@ export function createContextTimelineDefinition(config: Config, slim: () => bool // `peak` and cannot be reinterpreted into the right periods, so rows // refold from the log, which rebuilds the split (issue #91). // - // Not bumped since: the supported-baseline move (0.1.2-rc.1 → 0.1.5-rc.1, - // the plugin floor) retired the pre-V3 log-shape branches + // Not bumped since: the supported-baseline moves (0.1.2-rc.1 → + // 0.1.5-rc.1 → 0.1.7-rc.2, the plugin floor) retired log-shape branches + // that only unsupported logs exercise — first the pre-V3 spellings // (`header.system` envelope, `assistant/chunk` floods, `start`/`end` - // replacement endpoints, `tool/code-dispatch`) that only pre-V3 logs - // exercise — no supported log folds differently. Rows folded from those + // replacement endpoints, `tool/code-dispatch`), then the V3 wrapper + // spellings (the `tool-result` content block, the event-level `data.error` + // mark) once the floor left every supported harness on the V4 generation, + // whose migration rewrites them before the fold ever runs — no supported + // log folds differently. Rows folded from those // logs cannot reach this schema anyway: the projection cache's identity // gate (cache-record format version vs the header's current-generation // version) discards them wholesale and the session refolds from the diff --git a/src/shared/fileOps.ts b/src/shared/fileOps.ts index eb84d8f..34e9eaa 100644 --- a/src/shared/fileOps.ts +++ b/src/shared/fileOps.ts @@ -7,7 +7,7 @@ * host serves no `fileOps`. * * Tool coverage matches the harness's built-ins on every supported baseline - * (0.1.5-rc.1): read / read_image / write / edit (tool-fs) and grep / glob + * (0.1.7-rc.2+): read / read_image / write / edit (tool-fs) and grep / glob * (tool-fs-search), plus the Anthropic-style `str_replace_editor` (view * reads, every other command writes). Line deltas are estimates read off the * call ARGUMENTS (an edit's old/new strings, a write's content), never off diff --git a/src/shared/types.ts b/src/shared/types.ts index b81fdef..f27df00 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -139,11 +139,12 @@ export interface ContextActivity { } /** - * The per-user display-preference vocabulary of the `dsh-context` settings - * namespace — the ONE declaration both halves share: the Host registers the - * namespace schema against it (host/settings.ts), the Client binds the scope - * and edits fields by name (client/settings.ts). Type-only, so both bundles - * erase it. + * The per-user display-preference vocabulary of the `dsh-context` entry — + * the ONE declaration both halves share: the Host's entry Config schema + * carries the fields as `.volatile()` preferences (host/config.ts — the + * Config-form generation serves them live on the Plugins page), the Client + * binds the form and edits fields by name (client/settings.ts). Type-only, + * so both bundles erase it. */ export type DefaultGranularity = 'step' | 'turn' diff --git a/src/shared/version.ts b/src/shared/version.ts index 8610fff..55d2644 100644 --- a/src/shared/version.ts +++ b/src/shared/version.ts @@ -11,7 +11,7 @@ */ /** The oldest supported dsh release (see the matrix note above). */ -export const BASELINE_DSH_VERSION = '0.1.5-rc.1' +export const BASELINE_DSH_VERSION = '0.1.7-rc.2' /** * Release-channel rank at an equal X.Y.Z: a final release outranks its diff --git a/tests/baselines.ts b/tests/baselines.ts index 8ed5814..fc19ecb 100644 --- a/tests/baselines.ts +++ b/tests/baselines.ts @@ -16,7 +16,7 @@ */ /** The supported dsh tags, in lockstep with the BASELINES entries below. */ -export type BaselineId = 'v0.1.5-rc.1' | 'v0.1.7-rc.2' | 'v0.2.0-rc.2' +export type BaselineId = 'v0.1.7-rc.2' | 'v0.2.0-rc.2' /** The harness web half's client faces, as far as the compat probes consume them. */ export interface ClientSeam { @@ -48,11 +48,10 @@ export interface ClientSeam { * The session-jump seam (the Context Dashboard's session cards and the * Agent network card's nodes — issue #90). Every supported line selects a * session through the view owner's `uiWorkspace.openSession` (the sidebar - * row click's own verb), but the 0.1.6 selection refactor re-spelled the - * parameter (`sessionId` → `target: SessionTarget`) and retired the - * sessions service's own `open(id)` spelling — present through V3, absent - * on V4+. Both faces are probed presence-as-declared so a future move - * names the seam instead of silently dead-ending the jump. + * row click's own verb), re-spelled `openSession(target: SessionTarget)` by + * the 0.1.6 selection refactor, which also retired the sessions service's + * own `open(id)` spelling — absent on every supported line, probed so a + * resurrection names the seam instead of silently dead-ending the jump. */ sessionNav: { /** The view-owner navigation verb's source, plus this line's signature spelling. */ @@ -79,11 +78,10 @@ export interface ClientSeam { mainNeedle: string } /** - * The right Sidebar's tab seam — every supported generation ships it - * (0.1.5-rc.1 introduced it). The plugin's registration stays an OPTIONAL - * deferred inject: a below-baseline host (the gate's fallback composition) - * may lack the service entirely, and the client must simply never register - * the tab there instead of pending. + * The right Sidebar's tab seam — every supported generation ships it. The + * plugin's registration stays an OPTIONAL deferred inject: a below-baseline + * host (the gate's fallback composition) may lack the service entirely, and + * the client must simply never register the tab there instead of pending. */ sidebar: { /** The tab-type registry service's providing source. */ @@ -140,10 +138,10 @@ export interface Baseline { /** * The durable-event families THIS line's log carries that the host fold * switches on — the probe asserts every one exists in the tag's - * `KNOWN_SESSION_EVENT_TYPES`. Both supported generations (V3, V4) carry - * the same families, so the lists match; the matrix also asserts the union - * covers the fold's whole vocabulary, so a fold case added without a - * baseline list fails loudly instead of going unprobed. + * `KNOWN_SESSION_EVENT_TYPES`. Every supported line is the V4 generation, + * so the lists match; the matrix also asserts the union covers the fold's + * whole vocabulary, so a fold case added without a baseline list fails + * loudly instead of going unprobed. */ foldEventTypes: readonly string[] client: ClientSeam @@ -159,49 +157,44 @@ export interface Baseline { prependProofFile: string } /** - * The tag's settings-namespace surface. Through V3 the settings service - * carries the `register(ns, schema)` face the plugin's host half calls - * (feature-detected), optionally behind a namespace pattern that must - * accept the plugin literal, the browser half binds the `settingsScope` - * service and registers its card on the `settings.plugin.item` slot. V4+ - * derives configuration forms from each loader entry's own Config schema — - * the register face is gone (the plugin stays inert there) and the browser - * half rides the `configForms` service and the Plugins page's keyed - * `plugins.bundle.config` seat instead. + * The tag's settings-namespace surface. Every supported line is the V4+ + * Config-form generation: configuration forms derive from each loader + * entry's own Config schema — the `settings.register` host face is gone + * (probed absent, so the plugin never calls it), and the browser half rides + * the `configForms` service and the Plugins page's keyed + * `plugins.bundle.config` seat. */ settings: { - /** The settings service source carrying (or missing) the register face. */ + /** The settings service source missing the register face. */ serviceFile: string - /** Whether this generation serves `settings.register`. */ + /** Whether this generation serves `settings.register` (none do — asserted absent). */ register: boolean - /** The NAMESPACE_PATTERN source, where the generation enforces one. */ - patternFile?: string /** The browser slot the preferences card registers on this generation. */ cardSlot: string /** The sources declaring (or missing) the card slot. */ cardSlotFiles: readonly string[] /** The browser settings-transport service this generation composes. */ - transport: 'settingsScope' | 'configForms' + transport: 'configForms' /** The transport's declaring source, plus whether it exists. */ transportFile: string transportPresent: boolean } /** * The balance capsule's host seam (src/host/balance.ts). The facts fold - * reads the DeepSeek API-key provider's settings row through whichever face - * the line serves: the `get(ns)` registered-section read through V3, the - * Config-form `describe()` projection on V4+ — where the provider row is - * the entry whose served value declares the top-level `apiKeyEnv` - * credential ref, preferred under the settings ids the generations have - * served it as. Each face is probed against the tag's real sources. + * reads the DeepSeek API-key provider's settings row off the Config-form + * `describe()` projection — the provider row is the entry whose served + * value declares the top-level `apiKeyEnv` credential ref, preferred under + * the settings ids the generations have served it as. The retired + * registered-section `get(ns)` read is probed absent, so a resurrection + * names the seam instead of silently shadowing the projection. */ balance: { /** The settings service source the face needles are asserted in. */ settingsFile: string - /** The `get(ns)` section read — served through V3, retired on V4+. */ + /** The retired `get(ns)` section read — absent on every supported line. */ settingsGetNeedle: string settingsGetPresent: boolean - /** The Config-form projection needle, when this line serves one the fold reads. */ + /** The Config-form projection needle the fold reads. */ settingsDescribeNeedle: string | undefined /** The provider sources carrying the plugin id and the section shape the fold matches. */ providerFiles: readonly { file: string; needles: readonly string[] }[] @@ -210,117 +203,20 @@ export interface Baseline { export const BASELINES: readonly Baseline[] = [ { - // The oldest supported line — Session format V3: the system prompt is a - // surface node (`system/message`), replacement endpoints spell - // `startSeq`/`endSeq`, and the nested PTC vocabulary is - // `tool/ptc-dispatch`. From 0.1.5-alpha.2 the conversation surface moved - // under the keyed `main` panel (`main.conversation`); at 0.1.5-rc.1 the - // guide entry regained its optional description line. - id: 'v0.1.5-rc.1', - tag: 'dsh-v0.1.5-rc.1', - cordis: '4.0.2', - session: '0.1.5-rc.1', - foldEventTypes: [ - 'request/header', 'request/context', 'step/start', 'step/end', - 'user/message', 'tool/call', 'tool/result', 'assistant/message', 'assistant/attempt', - 'tool/ptc-dispatch', - 'plan/mode', 'compaction/summary', 'compaction/prune', 'system/message', - ], - client: { - imageFaceMethod: 'imageUrl', - markdownChrome: 'labels', - platformModules: [ - 'react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', '@deepseek-ai/cordis', - '@deepseek-ai/dsh-client-store', - '@deepseek-ai/dsh-client-ui-slots', - '@deepseek-ai/dsh-client-ui-primitives', - '@deepseek-ai/dsh-client-ui-dockkit', - ], - detailChannel: { - hostRpcFile: 'packages/client/connection/src/rpc.ts', - hostRpcNeedle: 'HostConnectionRpc', - clientRpcFile: 'packages/client/connection/src/client/rpc.ts', - clientRpcNeedle: 'call(channel, endpoint, payload', - registryFile: 'packages/session/session-projection/src/index.ts', - registryNeedle: 'stateOf<', - }, - sidebar: { - serviceFile: 'packages/client/ui-sidebar-right/src/client/index.ts', - serviceNeedle: 'sidebarRightTabs', - slotFile: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts', - slotNeedle: 'sidebar.right.pane.tab', - titleSlotNeedle: 'sidebar.right.pane.tab.title', - nav: { - file: 'packages/client/ui-sidebar-right/src/client/service.ts', - needle: 'openResource(address', - }, - guideEntry: { - file: 'packages/client/ui-sidebar-right/src/client/tab-registry.ts', - fields: [ - 'readonly order: number', - 'readonly title: () => string', - 'readonly description?: () => string', - 'readonly icon?: ComponentType', - ], - }, - }, - sessionNav: { - workspaceFile: 'packages/client/ui-workspace/src/client/navigation.ts', - workspaceNeedle: 'openSession(sessionId: SessionId): void', - sessionsFile: 'packages/api/session-controller/src/client/contract/sessions.ts', - sessionsOpen: true, - }, - insightPage: { - panellistFile: 'packages/client/ui-sidebar/src/client/contract/slots.ts', - panellistNeedle: "'sidebar.panellist'", - mainFile: 'packages/client/ui-layout/src/client/index.ts', - mainNeedle: "'main': { kind: 'keyed'", - }, - }, - settings: { - serviceFile: 'packages/settings/settings/src/index.ts', - register: true, - patternFile: 'packages/settings/settings/src/index.ts', - cardSlot: 'settings.plugin.item', - cardSlotFiles: ['packages/client/ui-settings-plugins/src/**'], - transport: 'settingsScope', - transportFile: 'packages/client/ui-settings/src/client/settings-scope.ts', - transportPresent: true, - }, - balance: { - settingsFile: 'packages/settings/settings/src/index.ts', - settingsGetNeedle: 'get(ns:', - settingsGetPresent: true, - settingsDescribeNeedle: undefined, - providerFiles: [ - { - file: 'packages/llm/llm-deepseek/src/index.ts', - needles: ["export const name = 'llm-deepseek'", "apiKeyEnv: z.string().role('credential-ref')"], - }, - ], - }, - stepGuard: { - loopFile: 'packages/core/agent-loop/src/agent.ts', - loopNeedles: ["'agent/pre-step'", "append('user/message'"], - prependProofFile: 'packages/context/time-context/src/index.ts', - }, - }, - { - // Session format V4: the fold's switched families are unchanged (the - // system prompt still rides `system/message`), but the line retires the - // `settings.register` host face and the `settings.plugin.item` slot — the - // plugin's preferences card registers on the Plugins page's keyed - // `plugins.bundle.config` slot there, keeping the old registration for the - // V3 line (each slot exists on exactly one side, so the deferred injects - // pick their generation and never pend). V4 also rewrites the tool result: - // a first-class role-`tool` message with lifted `toolCallId`/`isError` - // (the fold reads both spellings). The baseline pins `0.1.7-rc.2`, the - // first release of the line with a complete npm dependency closure; rc.2 - // adds `startsSeries` to the first `request/header` and the - // `developer/message` tool-registry events. startsSeries is inert to the - // fold; registry messages contribute to injected context. + // The oldest supported line — Session format V4: the fold's switched + // families run the current dialect (`system/message` surface nodes, the + // embedded assistant streams, `startSeq`/`endSeq` replacements, + // `tool/ptc-dispatch`), the tool result is a first-class role-`tool` + // message with lifted `toolCallId`/`isError`, and plugin configuration + // derives from the entry's own Config schema (the Plugins page's keyed + // `plugins.bundle.config` seat over the `configForms` transport). The + // baseline pins `0.1.7-rc.2`, the first release of the line with a + // complete npm dependency closure; rc.2 adds `startsSeries` to the first + // `request/header` of a resumed series and the `developer/message` + // tool-registry events. startsSeries is inert to the fold; registry + // messages contribute to injected context. // 0.1.7 enforces plugin dsh-peer compatibility at startup and install - // (evaluatePluginCompatibility); this plugin's `>=0.1.5-rc.1` dsh peers + // (evaluatePluginCompatibility); this plugin's `>=0.1.7-rc.2` dsh peers // satisfy every supported line under the gate's includePrerelease check. id: 'v0.1.7-rc.2', tag: 'dsh-v0.1.7-rc.2', @@ -424,15 +320,13 @@ export const BASELINES: readonly Baseline[] = [ // registry's register/snapshot/checkpoint/restore contract (drive, // stateVersion, wire views), the platform module table, the right // Sidebar's tab + chip-title seats and guide-entry contract, the - // session-jump verb pair, the detail channel's Connection RPC faces, the + // session-jump verb, the detail channel's Connection RPC faces, the // Plugins page's keyed `plugins.bundle.config` card slot over the - // `configForms` transport, the balance settings faces, the step-boundary + // `configForms` transport, the balance settings face, the step-boundary // guard, and the durable event vocabulary (still the V4 generation — the - // session-format tree carries no v4-to-v5 migration). The pre-0.1.6 icon - // spellings stay absent exactly as on 0.1.7 (either-spelling resolution - // covers the range). Cordis stays 4.0.4, and the line continues to - // enforce the plugin dsh-peer gate this plugin's `>=0.1.5-rc.1` peers - // satisfy. + // session-format tree carries no v4-to-v5 migration). Cordis stays 4.0.4, + // and the line continues to enforce the plugin dsh-peer gate this + // plugin's `>=0.1.7-rc.2` peers satisfy. id: 'v0.2.0-rc.2', tag: 'dsh-v0.2.0-rc.2', cordis: '4.0.4', diff --git a/tests/client/compat/clientFaces.spec.ts b/tests/client/compat/clientFaces.spec.ts index 684d431..a80aeff 100644 --- a/tests/client/compat/clientFaces.spec.ts +++ b/tests/client/compat/clientFaces.spec.ts @@ -41,9 +41,9 @@ vi.mock('@deepseek-ai/dsh-client-ui-primitives', async () => { }, // The rich-text copy control's glyphs and clipboard writer; the // Plugin Info card's settings gear. - IconCopyOutline16: () => React.createElement('span', null), - IconCheckOutline16: () => React.createElement('span', null), - IconSettingsOutline14: () => React.createElement('span', null), + IconCopyOutlineRegular: () => React.createElement('span', null), + IconCheckOutlineRegular: () => React.createElement('span', null), + IconSettingsOutlineMedium: () => React.createElement('span', null), writeClipboard: async () => true, } }) diff --git a/tests/client/components/agentGraph.spec.ts b/tests/client/components/agentGraph.spec.ts index aa17981..f367798 100644 --- a/tests/client/components/agentGraph.spec.ts +++ b/tests/client/components/agentGraph.spec.ts @@ -56,7 +56,7 @@ class FakeSessions { }, } - open(id: string): void { + openSession(id: string): void { this.opened.push(id) } @@ -73,7 +73,9 @@ class FakeSessions { } function makeView(sessions: unknown, options: { locale?: 'en' | 'zh' } = {}) { - const ctx = new TestClientCtx({ locale: options.locale, services: { sessions } }) + // The session jump rides the view owner's verb (uiWorkspace.openSession). + const nav = { openSession: (id: string) => (sessions as { openSession?: (id: string) => void }).openSession?.(id) } + const ctx = new TestClientCtx({ locale: options.locale, services: { sessions, uiWorkspace: nav } }) return makeAgentGraph(asClientCtx(ctx), options.locale === 'zh' ? makeKit('zh') : kit) } @@ -307,7 +309,7 @@ describe('AgentGraph — the family tree', () => { test('a face without refreshSubagents still renders', async () => { const face = new FakeSessions(family()) - const bare: unknown = { list: face.list, open: (id: string) => face.open(id) } + const bare: unknown = { list: face.list } const View = makeView(bare) const m = await mount(h(View, { sessionId: 'root' })) assert.equal(queryAll(m.container, 'g.lc-agent-node').length, 3) diff --git a/tests/client/components/browser.spec.ts b/tests/client/components/browser.spec.ts index 48f7fc1..2854b61 100644 --- a/tests/client/components/browser.spec.ts +++ b/tests/client/components/browser.spec.ts @@ -599,9 +599,9 @@ describe('ContextBrowser header epochs', () => { await m.unmount() }) - test('a V3 system prompt rides the timeline systems list, fetched from its own event seq', async () => { - // The V3 generation: the header epoch carries NO system price (the prompt - // is a `system/message` surface node), so the section resolves the prompt + test('a system prompt rides the timeline systems list, fetched from its own event seq', async () => { + // The header epoch carries NO system price (the prompt is a + // `system/message` surface node), so the section resolves the prompt // from `data.systems` and reads its text off that node's seq. const headers: ContextHeaders = { headers: [{ seq: 15, time: 1500, tools: [{ name: 'x', tokens: 1 }] }] } const data = tl({ @@ -611,14 +611,14 @@ describe('ContextBrowser header epochs', () => { const asked: number[] = [] const fetchHeader = (seq: number): Promise => { asked.push(seq) - return Promise.resolve(seq === 7 ? { system: 'V3 PROMPT', tools: [] } : { tools: [] }) + return Promise.resolve(seq === 7 ? { system: 'THE PROMPT', tools: [] } : { tools: [] }) } const m = await mount(h(Browser, props({ data, headers, fetchHeader }))) assert.ok(text(catRow(m, 'system')).includes(kit.t('browser.items', { n: 1 }))) await click(catRow(m, 'system')) await flush() assert.deepEqual(asked, [7], 'the prompt is fetched from the system node, not the header epoch') - assert.ok(text(query(m.container, '.lc-br-body')).includes('V3 PROMPT')) + assert.ok(text(query(m.container, '.lc-br-body')).includes('THE PROMPT')) await m.unmount() }) diff --git a/tests/client/components/contextView.spec.ts b/tests/client/components/contextView.spec.ts index 77cf5c7..445a2bd 100644 --- a/tests/client/components/contextView.spec.ts +++ b/tests/client/components/contextView.spec.ts @@ -244,14 +244,14 @@ describe('ContextView — baseline gate', () => { const View = makeView(new TestClientCtx()) const m = await mount(h(View, { sessionId: 'sv-gated', - useProjection: projectionsFor(timeline({ unsupported: { current: '0.1.1-rc.2', minimum: '0.1.5-rc.1' } })), + useProjection: projectionsFor(timeline({ unsupported: { current: '0.1.1-rc.2', minimum: '0.1.7-rc.2' } })), })) // The modal pops over the tab, naming both versions. assert.ok(m.container.querySelector('.lc-modal-backdrop') !== null) const card = query(m.container, '.lc-gate-card') assert.ok(text(card).includes(DICT_EN['gate.title'])) assert.ok(text(card).includes('v0.1.1-rc.2')) - assert.ok(text(card).includes('v0.1.5-rc.1')) + assert.ok(text(card).includes('v0.1.7-rc.2')) // The cards keep rendering the fallback's zeroed data behind it. assert.ok(text(m.container).includes(DICT_EN['overview.title'])) assert.ok(text(m.container).includes(DICT_EN['trend.empty'])) diff --git a/tests/client/components/events.spec.ts b/tests/client/components/events.spec.ts index b839fad..1e38483 100644 --- a/tests/client/components/events.spec.ts +++ b/tests/client/components/events.spec.ts @@ -108,12 +108,12 @@ describe('EventList', () => { assert.equal(down.textContent, '−100') assert.equal(queryAll(rows[2], '.lc-event-at').length, 0) - // Model switch: real IconBranchOutline16 svg. + // Model switch: real IconBranchOutlineRegular svg. assert.ok(query(rows[3], '.lc-event-icon svg') instanceof SVGElement) assert.ok(text(rows[3]).includes('Model switched: a → b')) assert.equal(query(rows[3], '.lc-kind').textContent, 'Switch') - // Inject: real IconPlusOutline16 svg, positive token delta, position, time. + // Inject: real IconPlusOutlineRegular svg, positive token delta, position, time. assert.ok(query(rows[4], '.lc-event-icon svg') instanceof SVGElement) const up = query(rows[4], '.lc-event-tokens') assert.ok(up.className.includes('lc-up')) diff --git a/tests/client/components/images.spec.ts b/tests/client/components/images.spec.ts index 611669f..21c2acf 100644 --- a/tests/client/components/images.spec.ts +++ b/tests/client/components/images.spec.ts @@ -218,7 +218,7 @@ describe('AttachmentLightbox', () => { assert.equal(query(box, '.lc-att-lightbox-img').getAttribute('src'), 'blob:x') const close = query(box, '.lc-att-lightbox-close') assert.equal(close.getAttribute('aria-label'), 'Close') - assert.ok(query(box, 'svg') instanceof SVGElement) // real IconCloseOutline16 + assert.ok(query(box, 'svg') instanceof SVGElement) // real IconCloseOutlineRegular assert.equal(document.activeElement, close) // focus moved into the dialog await keydown('Enter') // non-Escape keys keep it open assert.equal(queryAll(document.body, '.lc-att-lightbox').length, 1) diff --git a/tests/client/components/overviewPanel.spec.ts b/tests/client/components/overviewPanel.spec.ts index e253c6d..6cb1c8e 100644 --- a/tests/client/components/overviewPanel.spec.ts +++ b/tests/client/components/overviewPanel.spec.ts @@ -335,7 +335,7 @@ describe('OverviewPanel', () => { test('a card click opens the session through the harness verb', async () => { const opened: string[] = [] - const ctx = makeCtx({ open: (id: string) => { opened.push(id) } }) + const ctx = new TestClientCtx({ services: { uiWorkspace: { openSession: (id: string) => { opened.push(id) } } } }) const { m } = await openPanel(ctx) await click(query(m.container, '.lc-ov-grid > .lc-ov-session')) // The verb's own navigation returns the center column to the conversation diff --git a/tests/client/components/settingsCard.spec.ts b/tests/client/components/settingsCard.spec.ts index c76d4f8..daf2382 100644 --- a/tests/client/components/settingsCard.spec.ts +++ b/tests/client/components/settingsCard.spec.ts @@ -1,20 +1,18 @@ -// SettingsCard (src/client/components/settingsCard.tsx) rendered with real -// React against the real DICT_EN strings; the select rows open the REAL -// Menu primitive (portaled into document.body) and pick through it. The -// "Open in Settings" jump path mounts the card pre-expanded (settingsJump.ts -// expand request), with the scroll best-effort against stubbed prototypes. +// The preference card (src/client/components/settingsCard.tsx): the +// Plugins-page seat rendered with real React against the real DICT_EN +// strings; the select rows open the REAL Menu primitive (portaled into +// document.body) and pick through it. import { createElement as h } from 'react' import assert from 'node:assert/strict' import { describe, test } from 'vitest' -import { makePluginConfigCard, makeSettingsCard } from '../../../src/client/components/settingsCard' +import { makePluginConfigCard } from '../../../src/client/components/settingsCard' import type { SettingsState } from '../../../src/client/settings' import { DICT_EN } from '../../../src/client/i18n' -import { requestCardExpand } from '../../../src/client/settingsJump' import { click, keydown, makeKit, mount, query, queryAll, text } from '../helpers/kit' const kit = makeKit() -const SettingsCard = makeSettingsCard(kit) +const PluginConfigCard = makePluginConfigCard(kit) function hookFor(state: SettingsState) { return (sel: (s: SettingsState) => T): T => sel(state) @@ -29,61 +27,34 @@ function menuItems(): HTMLElement[] { return queryAll(document.body, '[role="menu"] [role="menuitem"]') } -type ScrollIntoViewLike = (this: Element, arg?: unknown) => void - -/** Swap Element.prototype.scrollIntoView (absent in jsdom); returns the original. */ -function stubScrollIntoView(impl: ScrollIntoViewLike | undefined): ScrollIntoViewLike | undefined { - const proto = Element.prototype as unknown as { scrollIntoView?: ScrollIntoViewLike } - const original = proto.scrollIntoView - proto.scrollIntoView = impl - return original -} - -describe('SettingsCard', () => { +describe('PluginConfigCard (the Plugins-page seat)', () => { test('renders nothing without a settings hook or when the namespace is unavailable', async () => { - const m1 = await mount(h(SettingsCard, {})) + const m1 = await mount(h(PluginConfigCard, {})) assert.equal(m1.container.childElementCount, 0) await m1.unmount() - const m2 = await mount(h(SettingsCard, { useContextSettings: hookFor(stateOf({ status: 'unavailable' })) })) + const m2 = await mount(h(PluginConfigCard, { useContextSettings: hookFor(stateOf({ status: 'unavailable' })) })) assert.equal(m2.container.childElementCount, 0) await m2.unmount() }) - test('loading state renders the card with disabled selects; the head toggles the body', async () => { - const m = await mount(h(SettingsCard, { useContextSettings: hookFor(stateOf({ status: 'loading', writable: false })) })) - const card = query(m.container, '.lc-settings-card') - assert.ok(!card.className.includes('lc-settings-open')) - const head = query(m.container, '.lc-settings-head') - assert.equal(head.getAttribute('aria-expanded'), 'false') - assert.equal(head.getAttribute('aria-label'), `${DICT_EN['settings.expand']}: ${DICT_EN['settings.title']}`) - assert.ok(text(m.container).includes(DICT_EN['settings.desc'])) - assert.equal(queryAll(m.container, '.lc-settings-select').length, 0) - - await click(head) - assert.equal(head.getAttribute('aria-expanded'), 'true') - assert.equal(head.getAttribute('aria-label'), `${DICT_EN['settings.collapse']}: ${DICT_EN['settings.title']}`) - assert.ok(card.className.includes('lc-settings-open')) - const selects = queryAll(m.container, '.lc-settings-select') + test('renders the eight rows flat — no card chrome', async () => { + const m = await mount(h(PluginConfigCard, { useContextSettings: hookFor(stateOf({ status: 'loading', writable: false })) })) + assert.equal(m.container.querySelector('.lc-settings-card'), null, 'no settings-section chrome') + assert.ok(query(m.container, '.lc-settings-prefs')) + const selects = queryAll(m.container, '.lc-settings-select') assert.equal(selects.length, 8) - assert.ok(selects.every(s => (s as HTMLButtonElement).disabled)) - // Loading is not ready: no read-only note. + assert.ok(selects.every(s => s.disabled), 'loading is not ready: the rows are disabled') assert.equal(m.container.querySelector('.lc-settings-note'), null) - - await click(head) - assert.equal(head.getAttribute('aria-expanded'), 'false') - assert.ok(!card.className.includes('lc-settings-open')) - assert.equal(queryAll(m.container, '.lc-settings-select').length, 0) await m.unmount() }) test('ready+writable: enabled selects pick through the real portaled Menu', async () => { const calls: [string, string][] = [] - const m = await mount(h(SettingsCard, { + const m = await mount(h(PluginConfigCard, { useContextSettings: hookFor(stateOf()), set: (field, value) => { calls.push([field, value]) }, })) - await click(query(m.container, '.lc-settings-head')) const selects = queryAll(m.container, '.lc-settings-select') assert.ok(selects.every(s => !s.disabled)) assert.equal(m.container.querySelector('.lc-settings-note'), null) @@ -109,13 +80,10 @@ describe('SettingsCard', () => { assert.ok(text(m.container).includes(DICT_EN['settings.insightsEntry'])) assert.ok(text(selects[1]).includes(DICT_EN['insightsEntry.show'])) await click(selects[1]) - assert.equal(selects[1].getAttribute('aria-expanded'), 'true') const entryItems = menuItems() assert.deepEqual(entryItems.map(i => text(i)), [DICT_EN['insightsEntry.show'], DICT_EN['insightsEntry.hide']]) await click(entryItems[1]) // 'Hide' assert.deepEqual(calls, [['defaultPlacement', 'sidebar'], ['insightsEntry', 'hide']]) - assert.equal(selects[1].getAttribute('aria-expanded'), 'false') - assert.equal(document.body.querySelector('[role="menu"]'), null) // The granularity row follows. assert.ok(text(selects[2]).includes(DICT_EN['gran.step'])) @@ -132,19 +100,14 @@ describe('SettingsCard', () => { assert.ok(text(selects[7]).includes(DICT_EN['files.sort.count'])) await click(selects[2]) - assert.equal(selects[2].getAttribute('aria-expanded'), 'true') const items = menuItems() - assert.equal(items.length, 2) assert.deepEqual(items.map(i => text(i)), [DICT_EN['gran.step'], DICT_EN['gran.turn']]) - await click(items[1]) // 'Turn' assert.deepEqual(calls, [ ['defaultPlacement', 'sidebar'], ['insightsEntry', 'hide'], ['defaultGranularity', 'turn'], ]) - assert.equal(selects[2].getAttribute('aria-expanded'), 'false') - assert.equal(document.body.querySelector('[role="menu"]'), null) // The trend-mode row writes the other field. await click(selects[3]) @@ -157,7 +120,6 @@ describe('SettingsCard', () => { ['defaultGranularity', 'turn'], ['defaultTrendMode', 'delta'], ]) - assert.equal(document.body.querySelector('[role="menu"]'), null) // The duration-curve row writes its field through the same show/hide vocabulary. await click(selects[4]) @@ -171,7 +133,6 @@ describe('SettingsCard', () => { ['defaultTrendMode', 'delta'], ['defaultDurationCurve', 'hide'], ]) - assert.equal(document.body.querySelector('[role="menu"]'), null) // The delta-baseline row writes its field through the toolbar's vocabulary. await click(selects[5]) @@ -186,7 +147,6 @@ describe('SettingsCard', () => { ['defaultDurationCurve', 'hide'], ['defaultDeltaBase', 'turn'], ]) - assert.equal(document.body.querySelector('[role="menu"]'), null) // The tool-sort row leads the file-sort one. await click(selects[6]) @@ -206,7 +166,6 @@ describe('SettingsCard', () => { ['defaultDeltaBase', 'turn'], ['defaultToolSort', 'name'], ]) - assert.equal(document.body.querySelector('[role="menu"]'), null) // The file-sort row writes the last field. await click(selects[7]) @@ -240,11 +199,10 @@ describe('SettingsCard', () => { test('Menu onClose (Escape) closes an open select without picking', async () => { const calls: [string, string][] = [] - const m = await mount(h(SettingsCard, { + const m = await mount(h(PluginConfigCard, { useContextSettings: hookFor(stateOf()), set: (field, value) => { calls.push([field, value]) }, })) - await click(query(m.container, '.lc-settings-head')) const select = query(m.container, '.lc-settings-select') await click(select) assert.equal(select.getAttribute('aria-expanded'), 'true') @@ -256,10 +214,8 @@ describe('SettingsCard', () => { await m.unmount() }) - test('ready+readonly: disabled selects and the read-only note when open', async () => { - const m = await mount(h(SettingsCard, { useContextSettings: hookFor(stateOf({ writable: false })) })) - // Note only renders once the body is open. - await click(query(m.container, '.lc-settings-head')) + test('ready+readonly: disabled selects and the read-only note', async () => { + const m = await mount(h(PluginConfigCard, { useContextSettings: hookFor(stateOf({ writable: false })) })) const note = query(m.container, '.lc-settings-note') assert.equal(note.getAttribute('role'), 'status') assert.equal(text(note), DICT_EN['settings.readOnly']) @@ -268,8 +224,7 @@ describe('SettingsCard', () => { }) test('a value matching no option falls back to the raw id; a missing set never throws', async () => { - const m = await mount(h(SettingsCard, { useContextSettings: hookFor(stateOf({ placement: 'weird' as never })) })) - await click(query(m.container, '.lc-settings-head')) + const m = await mount(h(PluginConfigCard, { useContextSettings: hookFor(stateOf({ placement: 'weird' as never })) })) const selects = queryAll(m.container, '.lc-settings-select') assert.ok(text(selects[0]).includes('weird')) // props.set undefined: picking still closes the menu, no throw. @@ -278,112 +233,4 @@ describe('SettingsCard', () => { assert.equal(document.body.querySelector('[role="menu"]'), null) await m.unmount() }) - - test('a fresh expand request mounts the card open, scrolled into view — once', async () => { - const scrolled: Array<{ el: Element; arg: unknown }> = [] - const restore = stubScrollIntoView(function (this: Element, arg?: unknown) { - scrolled.push({ el: this, arg }) - }) - try { - requestCardExpand() - const m = await mount(h(SettingsCard, { useContextSettings: hookFor(stateOf()) })) - const card = query(m.container, '.lc-settings-card') - assert.ok(card.className.includes('lc-settings-open')) - assert.equal(query(m.container, '.lc-settings-head').getAttribute('aria-expanded'), 'true') - assert.equal(queryAll(m.container, '.lc-settings-select').length, 8) - assert.equal(scrolled.length, 1, 'the card scrolls itself into view') - assert.deepEqual(scrolled[0].arg, { block: 'nearest' }) - assert.equal(scrolled[0].el, card) - await m.unmount() - - // Consumed once: a later mount starts collapsed again. - const again = await mount(h(SettingsCard, { useContextSettings: hookFor(stateOf()) })) - assert.equal(query(again.container, '.lc-settings-head').getAttribute('aria-expanded'), 'false') - assert.equal(scrolled.length, 1) - await again.unmount() - } finally { - stubScrollIntoView(restore) - } - }) - - test('a host whose scrollIntoView throws still mounts expanded', async () => { const restore = stubScrollIntoView(() => { throw new Error('no scrolling here') }) - try { - requestCardExpand() - const m = await mount(h(SettingsCard, { useContextSettings: hookFor(stateOf()) })) - assert.equal(query(m.container, '.lc-settings-head').getAttribute('aria-expanded'), 'true') - await m.unmount() - } finally { - stubScrollIntoView(restore) - } - }) - - test('a pending request is consumed even when the card renders nothing', async () => { - requestCardExpand() - const m = await mount(h(SettingsCard, { useContextSettings: hookFor(stateOf({ status: 'unavailable' })) })) - assert.equal(m.container.childElementCount, 0) - await m.unmount() - - const again = await mount(h(SettingsCard, { useContextSettings: hookFor(stateOf()) })) - assert.equal(query(again.container, '.lc-settings-head').getAttribute('aria-expanded'), 'false') - await again.unmount() - }) -}) - -describe('PluginConfigCard (the Plugins-page seat)', () => { - const PluginConfigCard = makePluginConfigCard(kit) - - test('renders nothing without a settings hook or when the namespace is unavailable', async () => { - const m1 = await mount(h(PluginConfigCard, {})) - assert.equal(m1.container.childElementCount, 0) - await m1.unmount() - - const m2 = await mount(h(PluginConfigCard, { useContextSettings: hookFor(stateOf({ status: 'unavailable' })) })) - assert.equal(m2.container.childElementCount, 0) - await m2.unmount() - }) - - test('renders the eight rows flat — no card chrome, no expand request consumption', async () => { - requestCardExpand() - const m = await mount(h(PluginConfigCard, { useContextSettings: hookFor(stateOf({ status: 'loading', writable: false })) })) - assert.equal(m.container.querySelector('.lc-settings-card'), null, 'no settings-section chrome') - assert.ok(query(m.container, '.lc-settings-prefs')) - assert.equal(m.container.querySelector('.lc-settings-head'), null) - // The rows render immediately: no expand/collapse leg. - const selects = queryAll(m.container, '.lc-settings-select') - assert.equal(selects.length, 8) - assert.ok(selects.every(s => s.disabled), 'loading is not ready: the rows are disabled') - assert.equal(m.container.querySelector('.lc-settings-note'), null) - await m.unmount() - }) - - test('ready rows pick through the portaled Menu; the read-only note renders without a disclosure', async () => { - const calls: [string, string][] = [] - const m = await mount(h(PluginConfigCard, { - useContextSettings: hookFor(stateOf({ writable: false })), - set: (field, value) => { calls.push([field, value]) }, - })) - const note = query(m.container, '.lc-settings-note') - assert.equal(text(note), DICT_EN['settings.readOnly']) - const selects = queryAll(m.container, '.lc-settings-select') - assert.ok(selects.every(s => s.disabled), 'read-only: the rows are disabled') - - const writable = await mount(h(PluginConfigCard, { - useContextSettings: hookFor(stateOf()), - set: (field, value) => { calls.push([field, value]) }, - })) - assert.equal(writable.container.querySelector('.lc-settings-note'), null) - const enabled = queryAll(writable.container, '.lc-settings-select') - assert.ok(enabled.every(s => !s.disabled)) - await click(enabled[7]) // the file-sort row trails - const items = menuItems() - assert.deepEqual(items.map(i => text(i)), [ - DICT_EN['files.sort.count'], - DICT_EN['files.sort.latest'], - DICT_EN['files.sort.path'], - ]) - await click(items[2]) // 'By path' - assert.deepEqual(calls, [['defaultFileSort', 'path']]) - await writable.unmount() - await m.unmount() - }) }) diff --git a/tests/client/components/upgradeGate.spec.ts b/tests/client/components/upgradeGate.spec.ts index 0a5d2be..dc01915 100644 --- a/tests/client/components/upgradeGate.spec.ts +++ b/tests/client/components/upgradeGate.spec.ts @@ -14,7 +14,7 @@ const kitZh = makeKit('zh') const Gate = makeUpgradeGate(kit) function render(sessionId: string | undefined, extra: { current?: string; minimum?: string } = {}) { - return mount(h(Gate, { sessionId, current: extra.current ?? '0.1.1-rc.2', minimum: extra.minimum ?? '0.1.5-rc.1' })) + return mount(h(Gate, { sessionId, current: extra.current ?? '0.1.1-rc.2', minimum: extra.minimum ?? '0.1.7-rc.2' })) } describe('UpgradeGate', () => { @@ -22,7 +22,7 @@ describe('UpgradeGate', () => { const en = await render('s-en') try { assert.match(text(en.container), /Update DeepSeek Harness/) - assert.match(text(en.container), /dsh-context supports DeepSeek Harness v0\.1\.5-rc\.1 or newer\./) + assert.match(text(en.container), /dsh-context supports DeepSeek Harness v0\.1\.7-rc\.2 or newer\./) assert.match(text(en.container), /v0\.1\.1-rc\.2/) assert.match(text(en.container), /Current/) assert.match(text(en.container), /Minimum required/) @@ -30,11 +30,11 @@ describe('UpgradeGate', () => { await en.unmount() } const ZhGate = makeUpgradeGate(kitZh) - const zh = await mount(h(ZhGate, { sessionId: 's-zh', current: '0.1.0', minimum: '0.1.5-rc.1' })) + const zh = await mount(h(ZhGate, { sessionId: 's-zh', current: '0.1.0', minimum: '0.1.7-rc.2' })) try { assert.match(text(zh.container), /请更新 DeepSeek Harness 版本/) - assert.match(text(zh.container), /dsh-context 插件支持 DeepSeek Harness v0\.1\.5-rc\.1 或更新版本。/) - assert.match(text(zh.container), /v0\.1\.5-rc\.1及更新的版本/) + assert.match(text(zh.container), /dsh-context 插件支持 DeepSeek Harness v0\.1\.7-rc\.2 或更新版本。/) + assert.match(text(zh.container), /v0\.1\.7-rc\.2及更新的版本/) assert.match(text(zh.container), /v0\.1\.0/) } finally { await zh.unmount() @@ -88,10 +88,10 @@ describe('UpgradeGate', () => { await click(query(m.container, '.lc-gate-ok')) assert.equal(m.container.querySelector('.lc-modal-backdrop'), null) // Switching to a fresh session shows its gate again on the same mount. - await m.update(h(Gate, { sessionId: 's-switch-b', current: '0.1.0', minimum: '0.1.5-rc.1' })) + await m.update(h(Gate, { sessionId: 's-switch-b', current: '0.1.0', minimum: '0.1.7-rc.2' })) assert.ok(m.container.querySelector('.lc-modal-backdrop') !== null) // Switching back to the dismissed session stays closed. - await m.update(h(Gate, { sessionId: 's-switch-a', current: '0.1.0', minimum: '0.1.5-rc.1' })) + await m.update(h(Gate, { sessionId: 's-switch-a', current: '0.1.0', minimum: '0.1.7-rc.2' })) assert.equal(m.container.querySelector('.lc-modal-backdrop'), null) await m.unmount() }) diff --git a/tests/client/dockMeasure.spec.ts b/tests/client/dockMeasure.spec.ts index 2b08fb6..1a5c192 100644 --- a/tests/client/dockMeasure.spec.ts +++ b/tests/client/dockMeasure.spec.ts @@ -31,14 +31,14 @@ describe('measureDock', () => { assert.deepEqual(measureDock(start), { left: 0, right: 0, frame: null }) }) - test('parses the sidebar tracks off the frame ancestor (V3 spelling, fixed right track)', () => { + test('parses the sidebar tracks off the frame ancestor (fixed right track)', () => { vi.stubGlobal('innerWidth', 1024) const frame = frameEl('280px minmax(0, 1fr) 360px') const start = chainOf(frame, document.createElement('div'), document.createElement('div')) assert.deepEqual(measureDock(start), { left: 280, right: 360, frame }) }) - test('parses the V4 minmax spelling: the right panel open and closed', () => { + test('parses the minmax spelling: the right panel open and closed', () => { vi.stubGlobal('innerWidth', 1280) const openFrame = frameEl('280px minmax(400px, 1fr) minmax(0px, 576px)') assert.deepEqual(measureDock(chainOf(openFrame, document.createElement('div'))), { left: 280, right: 576, frame: openFrame }) diff --git a/tests/client/helpers/harness.ts b/tests/client/helpers/harness.ts index f2a4a1f..a357f10 100644 --- a/tests/client/helpers/harness.ts +++ b/tests/client/helpers/harness.ts @@ -87,7 +87,7 @@ export class TestClientCtx { setService(name: string, service: unknown): void { this.services.set(name, service) - // Cordis surfaces services as context properties (ctx.settingsScope …). + // Cordis surfaces services as context properties (ctx.configForms …). if (!(name in this)) { Object.defineProperty(this, name, { get: () => this.services.get(name), configurable: true }) } diff --git a/tests/client/historyPage.spec.ts b/tests/client/historyPage.spec.ts index 25be6ce..c9135b7 100644 --- a/tests/client/historyPage.spec.ts +++ b/tests/client/historyPage.spec.ts @@ -64,10 +64,10 @@ describe('pageNodesOf — the durable-page mapper', () => { assert.deepEqual(nodes.get(12), { kind: 'tool-result', seq: 12, call: null, content: [], isError: false }) }) - test('the call head falls back to the result block\u2019s toolCallId when the source omits it', () => { + test('the call head reads the message toolCallId when the source omits it', () => { const page = [ row('tool/call', 20, { callId: 'cb', name: 'read', arguments: '{"path":"a.ts"}' }), - row('tool/result', 21, { message: { content: [{ toolCallId: 'cb', content: [] }] } }), + row('tool/result', 21, { message: { role: 'tool', toolCallId: 'cb', content: [] } }), ] assert.deepEqual(pageNodesOf(page).get(21)?.call, { name: 'read', argsRaw: '{"path":"a.ts"}' }) }) @@ -118,25 +118,23 @@ describe('pageNodesOf — the durable-page mapper', () => { assert.deepEqual(pageNodesOf([row('assistant/message', 21, {})]).get(21), { kind: 'assistant', seq: 21, blocks: [] }) }) - test('tool results pair with their in-page call head; isError rides block or envelope', () => { + test('tool results pair with their in-page call head; isError rides the message mark', () => { const page = [ row('tool/call', 30, { callId: 'c9', name: 'bash', arguments: '{"command":"pwd"}' }), row('tool/result', 31, { - callId: 'c9', - error: true, - message: { source: { callId: 'c9' }, content: [{ toolCallId: 'c9', content: [{ type: 'text', text: 'out' }] }] }, + message: { role: 'tool', toolCallId: 'c9', isError: true, source: { kind: 'tool', callId: 'c9' }, content: [{ type: 'text', text: 'out' }] }, }), row('tool/result', 32, { - message: { source: { callId: 'missing' }, content: [{ isError: true, content: [] }] }, + message: { role: 'tool', toolCallId: 'missing', isError: true, source: { kind: 'tool', callId: 'missing' }, content: [] }, }), row('tool/result', 33, { message: {} }), ] const nodes = pageNodesOf(page) const paired = nodes.get(31) assert.deepEqual(paired?.call, { name: 'bash', argsRaw: '{"command":"pwd"}' }) - assert.deepEqual(paired?.content, [{ type: 'text', text: 'out' }]) - assert.equal(paired?.isError, true, 'envelope error flag') - assert.equal(nodes.get(32)?.isError, true, 'block error flag without a pairable call') + assert.deepEqual(paired?.content, [{ type: 'text', text: 'out' }], 'the direct content blocks') + assert.equal(paired?.isError, true, 'the message error mark') + assert.equal(nodes.get(32)?.isError, true, 'the mark without a pairable call') assert.equal(nodes.get(32)?.call, null) assert.deepEqual(nodes.get(33)?.content, []) assert.equal(nodes.get(33)?.isError, false) @@ -305,12 +303,12 @@ describe('makeContentFetcher — the per-session targeted read', () => { broken.dispose() }) - test('a V3 system/message event maps its prompt text (the epoch fetch reads both shapes)', async () => { + test('a system/message event maps its prompt text', async () => { const ctx = armHistoryFaces({ session: pageFace([ - ev('system/message', 7, { message: { content: [{ type: 'text', text: 'V3 prompt' }, { type: 'text', text: ' line two' }] } }), + ev('system/message', 7, { message: { content: [{ type: 'text', text: 'the prompt' }, { type: 'text', text: ' line two' }] } }), ]) }) const content = await makeHeaderFetcher('s')!(7) - assert.deepEqual(content, { system: 'V3 prompt line two', tools: [] }) + assert.deepEqual(content, { system: 'the prompt line two', tools: [] }) ctx.dispose() }) diff --git a/tests/client/index.spec.ts b/tests/client/index.spec.ts index a781d79..870e430 100644 --- a/tests/client/index.spec.ts +++ b/tests/client/index.spec.ts @@ -1,6 +1,6 @@ // Client entry (src/client/index.ts): the plugin's apply() wiring asserted // through the faithful harness-context seams — dictionaries, slots, the -// /context trigger source, and the deferred settingsScope inject — plus real +// /context trigger source, and the deferred configForms inject — plus real // renders of the registered components. import { createElement as h, type ReactElement } from 'react' @@ -10,7 +10,7 @@ import { DICT_EN, DICT_ZH } from '../../src/client/i18n' import { modalStoreOf, type ModalStore } from '../../src/client/modalStore' import type { SettingsField, SettingsScopeLike, SettingsState } from '../../src/client/settings' import { TestClientCtx, TestSessions, asClientCtx } from './helpers/harness' -import { click, mount, query, queryAll, text } from './helpers/kit' +import { mount, query, queryAll, text } from './helpers/kit' // The entry ships via `module.exports` (bundle handoff shape); its runtime // exports are the plugin triple, opaque to the static import type. @@ -43,6 +43,37 @@ function makeScope(snapshot: { status: string; value: unknown; writable: boolean return rec } +/** A configForms face serving the given form (the card seat's transport). */ +function formsServing(form: SettingsScopeLike): { + get(namespace: string): SettingsScopeLike + whileServed(namespaces: readonly string[], register: () => () => void): () => void +} { + return { + get: () => form, + whileServed: () => () => {}, + } +} + +/** A settings form whose snapshot the test drives (the emit path). */ +function scopeWith(snapshot: { status: string; value: unknown; writable: boolean }): SettingsScopeLike & { + emit(next: { status: string; value: unknown; writable: boolean }): void +} { + let current = snapshot + const listeners = new Set<() => void>() + return { + getSnapshot: () => current, + subscribe(listener) { + listeners.add(listener) + return () => { listeners.delete(listener) } + }, + set: () => Promise.resolve(), + emit(next) { + current = next + for (const listener of listeners) listener() + }, + } +} + describe('client entry: constants', () => { test('name and inject declare the plugin identity and hard dependencies', () => { assert.equal(name, 'dsh-context') @@ -229,22 +260,14 @@ describe('client entry: Context Insights page seats', () => { }) test('the insightsEntry preference unwinds and remounts the pair', async () => { - const ctx = new TestClientCtx() + const scope = scopeWith({ status: 'ready', value: {}, writable: true }) + const ctx = new TestClientCtx({ services: { configForms: formsServing(scope) } }) applyTo(ctx) - // The preference flips through the registered settings card's set verb - // (the configForms transport is absent here, so drive the store directly - // through the last registered settings card face). - const scope = makeScope({ status: 'ready', value: {}, writable: true }) - ctx.setService('settingsScope', { bind: () => scope }) - assert.equal(ctx.slots.of('settings.plugin.item').length, 1, 'the V3 card seat armed') - const face = ctx.slots.of('settings.plugin.item')[0].registration.inject?.() as { - hooks: { contextSettings: { getSnapshot(): SettingsState } } - set: (field: SettingsField, value: string) => void - } - face.set('insightsEntry', 'hide') + assert.equal(ctx.slots.of('main').length, 1, 'the default entry mounts the page') + scope.emit({ status: 'ready', value: { insightsEntry: 'hide' }, writable: true }) assert.equal(ctx.slots.of('main').length, 0, 'hiding the entry unwinds the page') assert.equal(ctx.slots.of('sidebar.panellist').length, 0, 'hiding unwinds the entry') - face.set('insightsEntry', 'show') + scope.emit({ status: 'ready', value: { insightsEntry: 'show' }, writable: true }) assert.equal(ctx.slots.of('main').length, 1) assert.equal(ctx.slots.of('sidebar.panellist').length, 1) ctx.dispose() @@ -253,41 +276,7 @@ describe('client entry: Context Insights page seats', () => { }) }) -describe('client entry: settingsScope inject', () => { - test('absent at apply time: the inject stays pending — no settings.plugin.item slot', () => { - const ctx = new TestClientCtx() - applyTo(ctx) - assert.equal(ctx.slots.of('settings.plugin.item').length, 0) - ctx.dispose() - }) - - test('armed later: the pending inject runs — scope attached and card slot registered', () => { - const ctx = new TestClientCtx() - applyTo(ctx) - const scope = makeScope({ status: 'ready', value: {}, writable: true }) - const specs: { namespace: string }[] = [] - ctx.setService('settingsScope', { - bind: (spec: { namespace: string }) => { - specs.push(spec) - return scope - }, - }) - assert.deepEqual(specs, [{ namespace: 'dsh-context' }]) - assert.equal(scope.subscribes, 1) - assert.equal(ctx.slots.of('settings.plugin.item').length, 1) - ctx.dispose() - }) - - test('defensive arm: service key present but undefined — early return, no slot, no throw', () => { - const ctx = new TestClientCtx() - ctx.setService('settingsScope', undefined) - applyTo(ctx) - assert.equal(ctx.slots.of('settings.plugin.item').length, 0) - ctx.dispose() - }) -}) - -describe('client entry: configForms inject (the Config-form generation)', () => { +describe('client entry: configForms inject', () => { /** A configForms stand-in capturing the whileServed registration. */ function fakeConfigForms(form: SettingsScopeLike): { gets: string[] @@ -338,7 +327,7 @@ describe('client entry: configForms inject (the Config-form generation)', () => const scope = makeScope({ status: 'ready', value: { defaultTrendMode: 'delta' }, writable: true }) const forms = fakeConfigForms(scope) ctx.setService('configForms', forms) - // The namespace follows the settingsScope generation's join key. + // The namespace is the entry id. assert.deepEqual(forms.gets, ['dsh-context']) assert.equal(scope.subscribes, 1, 'the settings store attached to the form') assert.deepEqual(forms.whileServedCalls, [['dsh-context']]) @@ -409,86 +398,7 @@ describe('client entry: right Sidebar Context tab', () => { }) }) -describe('client entry: settings card slot', () => { - function setup(): { - ctx: TestClientCtx - scope: ReturnType - registration: { name: string; key?: string; locale?: string; inject?: () => unknown } - component: (props: Record) => unknown - } { - const scope = makeScope({ status: 'loading', value: null, writable: false }) - const ctx = new TestClientCtx({ - services: { settingsScope: { bind: () => scope } }, - }) - applyTo(ctx) - const entry = ctx.slots.of('settings.plugin.item')[0] - return { - ctx, - scope, - registration: entry.registration as never, - component: entry.component as never, - } - } - - test('registers the keyed card; inject exposes the settings store and a set verb', () => { - const { ctx, scope, registration } = setup() - assert.equal(registration.name, 'settings.plugin.item') - assert.equal(registration.key, 'dsh-context') - assert.equal(registration.locale, 'dsh-context') - - const face = registration.inject?.() as { - hooks: { contextSettings: { getSnapshot(): SettingsState } } - set: (field: SettingsField, value: string) => void - } - assert.equal(face.hooks.contextSettings.getSnapshot().granularity, 'step') - face.set('defaultGranularity', 'turn') - assert.equal(face.hooks.contextSettings.getSnapshot().granularity, 'turn') - assert.deepEqual(scope.sets, [{ field: 'defaultGranularity', value: 'turn' }]) - ctx.dispose() - }) - - test('the component renders the card DOM (loading status → disabled selects)', async () => { - const { ctx, component } = setup() - const el = component({}) as ReactElement - assert.equal(typeof el.type, 'function') - assert.equal((el.type as { name: string }).name, 'SettingsCard') - const store = (ctx.slots.of('settings.plugin.item')[0].registration.inject?.() as { - hooks: { contextSettings: { getSnapshot(): SettingsState } } - }).hooks.contextSettings - const m = await mount(h(el.type as never, { - useContextSettings: (sel: (state: SettingsState) => T): T => sel(store.getSnapshot()), - })) - assert.ok(query(m.container, '.lc-settings-card')) - await click(query(m.container, '.lc-settings-head')) - const selects = queryAll(m.container, '.lc-settings-select') - assert.equal(selects.length, 8) - for (const s of selects) assert.ok((s as HTMLButtonElement).disabled) - await m.unmount() - ctx.dispose() - }) -}) - describe('client entry: placement gating', () => { - /** A settings scope whose snapshot the test drives (the emit path). */ - function scopeWith(snapshot: { status: string; value: unknown; writable: boolean }): SettingsScopeLike & { - emit(next: { status: string; value: unknown; writable: boolean }): void - } { - let current = snapshot - const listeners = new Set<() => void>() - return { - getSnapshot: () => current, - subscribe(listener) { - listeners.add(listener) - return () => { listeners.delete(listener) } - }, - set: () => Promise.resolve(), - emit(next) { - current = next - for (const listener of listeners) listener() - }, - } - } - /** The sidebar registry stand-in counting registrations and disposer calls. */ function registry(): { definitions: unknown[]; disposed: number; register(d: unknown): () => void } { const rec = { @@ -504,7 +414,7 @@ describe('client entry: placement gating', () => { test("the persisted 'sidebar' placement skips the conversation tab and keeps the sidebar", () => { const scope = scopeWith({ status: 'ready', value: { defaultPlacement: 'sidebar' }, writable: true }) - const ctx = new TestClientCtx({ services: { settingsScope: { bind: () => scope } } }) + const ctx = new TestClientCtx({ services: { configForms: formsServing(scope) } }) applyTo(ctx) assert.deepEqual(ctx.slots.of('conversation.view'), [], 'the dropped tab never registered') const tabs = registry() @@ -518,7 +428,7 @@ describe('client entry: placement gating', () => { test('a preference flip moves the registrations live; disposal unwinds everything', () => { const scope = scopeWith({ status: 'ready', value: {}, writable: true }) - const ctx = new TestClientCtx({ services: { settingsScope: { bind: () => scope } } }) + const ctx = new TestClientCtx({ services: { configForms: formsServing(scope) } }) applyTo(ctx) const tabs = registry() ctx.setService('sidebarRightTabs', tabs) diff --git a/tests/client/primitives.spec.ts b/tests/client/primitives.spec.ts index be27926..caac9e1 100644 --- a/tests/client/primitives.spec.ts +++ b/tests/client/primitives.spec.ts @@ -1,45 +1,39 @@ -// The generation-proof icon resolver (src/client/primitives.ts): each ui-primitives -// icon resolves across the range's two naming vocabularies — modern first, legacy -// fallback — and a name missing from BOTH (a future rename) degrades to a -// render-nothing component instead of the React #130 element-type crash. +// The defensive icon resolver (src/client/primitives.ts): each ui-primitives +// icon resolves by its export name; a name missing from the module (a future +// rename) or a namespace that throws on the read degrades to a render-nothing +// component instead of the React #130 element-type crash. import assert from 'node:assert/strict' import { describe, test } from 'vitest' import { resolveIcon, IconBranch, IconPlus, IconCheck, IconCopy, IconClose, IconSettings, IconChevronDown } from '../../src/client/primitives' describe('resolveIcon', () => { - const modern = function ModernIcon(): null { return null } - const legacy = function LegacyIcon(): null { return null } + const icon = function SomeIcon(): null { return null } - test('the modern spelling wins when both generations export the icon', () => { - assert.equal(resolveIcon('modern', 'legacy', { modern, legacy }), modern) - }) - - test('the legacy spelling backs the modern one up (through-0.1.5 generations)', () => { - assert.equal(resolveIcon('modern', 'legacy', { legacy }), legacy) + test('a present export resolves to the component', () => { + assert.equal(resolveIcon('icon', { icon }), icon) }) test('a name present but not a component renders nothing', () => { - const icon = resolveIcon('modern', 'legacy', { modern: 42, legacy: null }) - assert.equal(icon({}), null, 'the fallback component renders null') + const resolved = resolveIcon('icon', { icon: 42 }) + assert.equal(resolved({}), null, 'the fallback component renders null') }) - test('a name missing from both spellings renders nothing (future rename)', () => { - const icon = resolveIcon('IconGhostOutlineRegular', 'IconGhostOutline16', {}) - assert.equal(icon({}), null) + test('a missing name renders nothing (a future rename)', () => { + const resolved = resolveIcon('IconGhostOutlineRegular', {}) + assert.equal(resolved({}), null) }) test('a namespace that throws on property access degrades to the fallback', () => { // Partial module mocks and hostile namespaces throw on the READ itself. const hostile = new Proxy({}, { get() { throw new Error('hostile export read') } }) - const icon = resolveIcon('IconBranchOutlineRegular', 'IconBranchOutline16', hostile) - assert.equal(icon({}), null) + const resolved = resolveIcon('IconBranchOutlineRegular', hostile) + assert.equal(resolved({}), null) }) test('the module-level exports resolve against the pinned generation', () => { - // The devDep carries the legacy vocabulary, so these exercised the - // legacy branch at module load; on a modern harness the same seven - // exports resolve through the modern names instead. + // The devDep carries the supported vocabulary, so these resolved at + // module load. for (const icon of [IconBranch, IconPlus, IconCheck, IconCopy, IconClose, IconSettings, IconChevronDown]) { assert.equal(typeof icon, 'function', 'every seam resolves to a component') } diff --git a/tests/client/services.spec.ts b/tests/client/services.spec.ts index 223b8da..e2018d6 100644 --- a/tests/client/services.spec.ts +++ b/tests/client/services.spec.ts @@ -278,9 +278,9 @@ describe('timelineOf', () => { }) test('the unsupported gate record survives the sanitizing slow path only when well-formed', () => { - const kept = timelineOf({ current: 1, unsupported: { current: '0.1.1-rc.2', minimum: '0.1.5-rc.1' } }) + const kept = timelineOf({ current: 1, unsupported: { current: '0.1.1-rc.2', minimum: '0.1.7-rc.2' } }) assert.ok(kept !== null) - assert.deepEqual(kept.unsupported, { current: '0.1.1-rc.2', minimum: '0.1.5-rc.1' }) + assert.deepEqual(kept.unsupported, { current: '0.1.1-rc.2', minimum: '0.1.7-rc.2' }) for (const bad of ['x', null, {}, { current: 1, minimum: 'm' }, { current: 'c' }]) { const out = timelineOf({ current: 1, unsupported: bad }) assert.ok(out !== null) @@ -354,7 +354,7 @@ describe('timelineOf', () => { describe('unsupportedOf', () => { test('well-formed records pass through as plain data', () => { - assert.deepEqual(unsupportedOf({ current: '0.1.1-rc.2', minimum: '0.1.5-rc.1' }), { current: '0.1.1-rc.2', minimum: '0.1.5-rc.1' }) + assert.deepEqual(unsupportedOf({ current: '0.1.1-rc.2', minimum: '0.1.7-rc.2' }), { current: '0.1.1-rc.2', minimum: '0.1.7-rc.2' }) }) test('non-records and wrong-typed fields degrade to null', () => { @@ -854,8 +854,8 @@ describe('openSessionVia', () => { const ctxWith = (services: Record): ClientCtx => ({ get: (name: string) => services[name] }) as unknown as ClientCtx test('jumps through the view owner — every supported line\'s own verb (issue #90)', () => { - // Exactly the rc.2 composition: uiWorkspace serves the navigation verb, - // the sessions service carries no selection at all. + // Exactly the supported composition: uiWorkspace serves the navigation + // verb, the sessions service carries no selection at all. const opened: string[] = [] openSessionVia(ctxWith({ uiWorkspace: { openSession(id: string) { opened.push(id) } }, @@ -864,21 +864,6 @@ describe('openSessionVia', () => { assert.deepEqual(opened, ['s2']) }) - test('the retired sessions-service verb still serves as the degradation path', () => { - const opened: string[] = [] - openSessionVia(ctxWith({ sessions: { open: (id: string) => { opened.push(id) } } }), 's1') - assert.deepEqual(opened, ['s1']) - }) - - test('when both faces serve a verb, the view owner wins', () => { - const via: string[] = [] - openSessionVia(ctxWith({ - uiWorkspace: { openSession: (id: string) => { via.push(`workspace:${id}`) } }, - sessions: { open: (id: string) => { via.push(`sessions:${id}`) } }, - }), 's3') - assert.deepEqual(via, ['workspace:s3']) - }) - test('the verb is invoked bound to its service instance', () => { let seen: unknown const face = { @@ -889,17 +874,14 @@ describe('openSessionVia', () => { assert.equal(seen, 's4:s4') }) - test('a generation serving neither verb, or a verb-less face, swallows silently', () => { + test('a composition without the workspace module, or a verb-less face, swallows silently', () => { openSessionVia(ctxWith({}), 's5') - openSessionVia(ctxWith({ uiWorkspace: {}, sessions: null }), 's5') + openSessionVia(ctxWith({ uiWorkspace: {} }), 's5') openSessionVia(ctxWith({ uiWorkspace: { openSession: 7 } }), 's5') - openSessionVia(ctxWith({ sessions: {} }), 's5') - openSessionVia(ctxWith({ sessions: { open: 7 } }), 's5') }) test('a hostile face never throws into the click handler', () => { openSessionVia(ctxWith({ uiWorkspace: { openSession: () => { throw new Error('boom') } } }), 's6') - openSessionVia(ctxWith({ sessions: { open: () => { throw new Error('boom') } } }), 's6') // A traced proxy can throw on the property READ itself (issue #42). openSessionVia(ctxWith({ uiWorkspace: { get openSession() { throw new Error('boom') } } }), 's6') openSessionVia({ get: () => { throw new Error('boom') } } as unknown as ClientCtx, 's6') diff --git a/tests/client/settings.spec.ts b/tests/client/settings.spec.ts index afc81e5..8a36ffa 100644 --- a/tests/client/settings.spec.ts +++ b/tests/client/settings.spec.ts @@ -1,13 +1,13 @@ // Settings binding (src/client/settings.ts): defaults, the observable store, -// scope attach/sync, preference parsing, and the local-echo set path. +// form attach/sync, preference parsing, and the local-echo set path. import assert from 'node:assert/strict' import { describe, test } from 'vitest' import { createContextSettings, type SettingsScopeLike } from '../../src/client/settings' /** - * A faithful in-memory settings scope (the harness settingsScope.bind - * contract: getSnapshot/subscribe/set), not a mock of plugin code. + * A faithful in-memory settings form (the harness configForms.get contract: + * getSnapshot/subscribe/set), not a mock of plugin code. */ class TestSettingsScope implements SettingsScopeLike { private snapshot: { status: string; value: unknown; writable: boolean } diff --git a/tests/client/settingsJump.spec.ts b/tests/client/settingsJump.spec.ts index 738d199..22306f1 100644 --- a/tests/client/settingsJump.spec.ts +++ b/tests/client/settingsJump.spec.ts @@ -1,12 +1,12 @@ // openPluginSettings (src/client/settingsJump.ts): the guarded DOM jump to -// the plugin's settings page — settings trigger first, Plugins section nav -// row second — plus the hostile degradations (missing chrome, throwing -// elements, already-open panel) and the expand request the surviving jump -// leaves for the plugin's settings card. jsdom supplies the real document. +// the plugin's preferences on the Plugins main panel — panel entry first, +// the bundle card's open control after the page settles — plus the hostile +// degradations (missing chrome, throwing elements, a card that never lists). +// jsdom supplies the real document. import assert from 'node:assert/strict' import { afterEach, describe, test } from 'vitest' -import { consumeCardExpand, openPluginSettings, requestCardExpand } from '../../src/client/settingsJump' +import { openPluginSettings } from '../../src/client/settingsJump' /** Immediate scheduler: captures the deferred runs in issue order. */ function syncSchedule(): { runs: Array<() => void>; schedule: (run: () => void, ms: number) => void } { @@ -14,7 +14,7 @@ function syncSchedule(): { runs: Array<() => void>; schedule: (run: () => void, return { runs, schedule: (run) => { runs.push(run) } } } -/** A sidebar-style settings trigger (dialog semantics + expanded flag). */ +/** A sidebar-style settings trigger (dialog semantics + expanded flag) — a distractor the jump must ignore. */ function trigger(doc: Document, expanded: 'true' | 'false'): HTMLButtonElement { const b = doc.createElement('button') b.setAttribute('aria-haspopup', 'dialog') @@ -30,153 +30,9 @@ function clicks(el: HTMLElement): { count: () => number } { return { count: () => n } } -describe('openPluginSettings', () => { +describe('openPluginSettings (the Plugins main panel)', () => { afterEach(() => { document.body.textContent = '' - consumeCardExpand() - }) - - test('clicks the collapsed trigger, then the Plugins section row', () => { - const t = trigger(document, 'false') - const tClicks = clicks(t) - const section = document.createElement('button') - section.textContent = '插件' - document.body.appendChild(section) - const sClicks = clicks(section) - const { runs, schedule } = syncSchedule() - - openPluginSettings(document, schedule) - - assert.equal(tClicks.count(), 1, 'the collapsed trigger is clicked immediately') - assert.equal(sClicks.count(), 0, 'the section row waits for the panel to open') - assert.equal(runs.length, 1) - runs[0]() - assert.equal(sClicks.count(), 1, 'the Plugins nav row is clicked once the panel opens') - }) - - test('the en-labeled section row matches too', () => { - trigger(document, 'false') - const section = document.createElement('button') - section.textContent = 'Plugins' - document.body.appendChild(section) - const sClicks = clicks(section) - const { runs, schedule } = syncSchedule() - - openPluginSettings(document, schedule) - runs[0]() - - assert.equal(sClicks.count(), 1) - }) - - test('the default schedule defers through window.setTimeout', async () => { - trigger(document, 'false') - const section = document.createElement('button') - section.textContent = '插件' - document.body.appendChild(section) - const sClicks = clicks(section) - - openPluginSettings(document) - await new Promise(resolve => { setTimeout(resolve, 120) }) - - assert.equal(sClicks.count(), 1) - }) - - test('an already-open panel skips the trigger but still selects the section', () => { - const t = trigger(document, 'true') - const tClicks = clicks(t) - const section = document.createElement('button') - section.textContent = '插件' - document.body.appendChild(section) - const sClicks = clicks(section) - const { runs, schedule } = syncSchedule() - - openPluginSettings(document, schedule) - runs[0]() - - assert.equal(tClicks.count(), 0, 'no re-click on an open panel') - assert.equal(sClicks.count(), 1) - }) - - test('a chrome with no settings trigger degrades to a silent no-op', () => { - const { runs, schedule } = syncSchedule() - assert.doesNotThrow(() => openPluginSettings(document, schedule)) - assert.equal(runs.length, 0, 'no deferred section selection without a trigger') - }) - - test('elements that throw on click never surface the error', () => { - const t = trigger(document, 'false') - t.click = () => { throw new Error('detached') } - const { runs, schedule } = syncSchedule() - assert.doesNotThrow(() => openPluginSettings(document, schedule)) - // The trigger's failure aborts this jump entirely — the guarded path. - assert.equal(runs.length, 0) - }) - - test('a throwing section row is contained by its own guard', () => { - trigger(document, 'false') - const section = document.createElement('button') - section.textContent = '插件' - section.click = () => { throw new Error('detached') } - document.body.appendChild(section) - const { runs, schedule } = syncSchedule() - - assert.doesNotThrow(() => openPluginSettings(document, schedule)) - assert.doesNotThrow(() => runs[0]()) - }) - - test('lookalike chrome is ignored: a menu trigger is not the settings dialog', () => { - const menu = document.createElement('button') - menu.setAttribute('aria-haspopup', 'menu') - menu.setAttribute('aria-expanded', 'false') - document.body.appendChild(menu) - const menuClicks = clicks(menu) - const { runs, schedule } = syncSchedule() - - openPluginSettings(document, schedule) - - assert.equal(menuClicks.count(), 0, 'only dialog triggers are candidates') - assert.equal(runs.length, 0) - }) - - test('a surviving jump leaves a fresh expand request for the settings card', () => { - trigger(document, 'false') - const section = document.createElement('button') - section.textContent = '插件' - document.body.appendChild(section) - const { schedule } = syncSchedule() - - openPluginSettings(document, schedule) - - assert.equal(consumeCardExpand(), true, 'the card mounts expanded on this jump') - assert.equal(consumeCardExpand(), false, 'the request is consumed once') - }) - - test('degraded jumps leave no expand request behind', () => { - const { runs, schedule } = syncSchedule() - openPluginSettings(document, schedule) - assert.equal(consumeCardExpand(), false, 'no settings dialog in this chrome: no request') - - const t = trigger(document, 'false') - t.click = () => { throw new Error('detached') } - assert.doesNotThrow(() => openPluginSettings(document, schedule)) - assert.equal(consumeCardExpand(), false, 'a failed trigger click leaves no stale request') - assert.equal(runs.length, 0) - }) - - test('the expand request is freshness-windowed (5s) and single-shot', () => { - requestCardExpand(1000) - assert.equal(consumeCardExpand(1000 + 4999), true, 'fresh within the window') - assert.equal(consumeCardExpand(1000 + 4999), false, 'already consumed') - - requestCardExpand(1000) - assert.equal(consumeCardExpand(1000 + 5000), false, 'stale past the window') - }) -}) - -describe('openPluginSettings: the Config-form generation (Plugins main panel)', () => { - afterEach(() => { - document.body.textContent = '' - consumeCardExpand() }) /** A sidebar-style Plugins panel entry (shipped aria-label). */ @@ -213,7 +69,7 @@ describe('openPluginSettings: the Config-form generation (Plugins main panel)', openPluginSettings(document, schedule) assert.equal(pClicks.count(), 1, 'the panel entry is clicked immediately') - assert.equal(tClicks.count(), 0, 'no settings-dialog legs') + assert.equal(tClicks.count(), 0, 'the settings-dialog chrome is never touched') assert.equal(runs.length, 1, 'the card poll is scheduled') // The list has not rendered the card yet: the poll keeps ticking. runs[0]() @@ -226,7 +82,27 @@ describe('openPluginSettings: the Config-form generation (Plugins main panel)', assert.equal(titleClicks.count(), 1, 'the title control opens the bundle page') assert.equal(switchClicks.count(), 0, 'the enable switch is never touched') assert.equal(runs.length, 2, 'the poll stops on success') - assert.equal(consumeCardExpand(), false, 'no expand request: this seat has no disclosure') + }) + + test('the en-labeled panel entry matches too', () => { + const panel = panelEntry(document, 'Plugins') + const pClicks = clicks(panel) + const { runs, schedule } = syncSchedule() + + openPluginSettings(document, schedule) + + assert.equal(pClicks.count(), 1) + assert.equal(runs.length, 1) + }) + + test('the default schedule defers through window.setTimeout', async () => { + const panel = panelEntry(document, '插件') + const pClicks = clicks(panel) + + openPluginSettings(document) + await new Promise(resolve => { setTimeout(resolve, 20) }) + + assert.equal(pClicks.count(), 1) }) test('the poll keeps ticking to its deadline and stops silently without a card', () => { @@ -242,6 +118,22 @@ describe('openPluginSettings: the Config-form generation (Plugins main panel)', assert.equal(runs.length, 0, 'the poll exhausted its ticks and stopped') }) + test('a chrome without the Plugins panel entry degrades to a silent no-op', () => { + trigger(document, 'false') + const { runs, schedule } = syncSchedule() + assert.doesNotThrow(() => openPluginSettings(document, schedule)) + assert.equal(runs.length, 0, 'no card poll without the panel entry') + }) + + test('a panel entry that throws on click never surfaces the error', () => { + const panel = panelEntry(document, '插件') + panel.click = () => { throw new Error('detached') } + const { runs, schedule } = syncSchedule() + assert.doesNotThrow(() => openPluginSettings(document, schedule)) + // The entry's failure aborts this jump entirely — the guarded path. + assert.equal(runs.length, 0) + }) + test('a hostile card (no title control, or one that throws) never surfaces an error', () => { panelEntry(document, '插件') const { runs, schedule } = syncSchedule() @@ -268,6 +160,6 @@ describe('openPluginSettings: the Config-form generation (Plugins main panel)', openPluginSettings(document, schedule) assert.equal(lClicks.count(), 0) - assert.equal(runs.length, 0, 'no dialog trigger either: silent no-op') + assert.equal(runs.length, 0, 'no panel entry: silent no-op') }) }) diff --git a/tests/compat/bundle.spec.ts b/tests/compat/bundle.spec.ts index 742fca0..cec89b7 100644 --- a/tests/compat/bundle.spec.ts +++ b/tests/compat/bundle.spec.ts @@ -83,7 +83,7 @@ describe.skipIf(staging.artifactsMissing())('bundle smoke — the built lib/clie inject(_deps: string[], cb: (c: unknown) => void) { // Cordis fires the callback once every requested service exists; this // fake answers every name it knows up front, so it fires immediately. - // The settingsScope wiring guards on the absent binder itself. + // The configForms wiring guards on the absent service itself. cb(ctx) }, effect(fn: () => (() => void) | void) { diff --git a/tests/compat/matrix.spec.ts b/tests/compat/matrix.spec.ts index 8f989b0..4f86ca5 100644 --- a/tests/compat/matrix.spec.ts +++ b/tests/compat/matrix.spec.ts @@ -105,31 +105,23 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base assert.equal(report.poisonedSnapshotThrows, true) }) - test('settings: the tag\'s namespace surface matches the plugin\'s feature-detected faces', () => { - const settings = baseline.settings - // The register face the host half calls (and feature-detects): present - // through V3, gone on the V4+ Config-form generations. + test('settings: the retired register face stays gone (the plugin never calls it)', () => { + // The Config-form generation derives forms from each loader entry's own + // Config schema; `settings.register` retired with the older lines. The + // probe pins the absence, so a resurrection names the seam. assert.equal( - staging.dshHasString(baseline.tag, 'register(', settings.serviceFile), - settings.register, + staging.dshHasString(baseline.tag, 'register(', baseline.settings.serviceFile), + baseline.settings.register, 'the settings service register face', ) - // Where the generation enforces a namespace pattern it must accept the - // plugin literal; where it does not (the pattern source is gone), the - // register probe above already pins the plugin's inert path. - if (settings.patternFile === undefined) return - const pattern = staging.namespacePatternOf(baseline, settings.patternFile) - assert.ok(pattern !== null, 'the tag source carries NAMESPACE_PATTERN') - assert.equal(pattern.test('dsh-context'), true) }) test('balance: the settings face and the DeepSeek provider row the facts fold reads', () => { - // The balance route resolves the provider's settings row through the - // face this generation serves — the `get(ns)` section read through V3, - // the Config-form `describe()` projection on V4+ (the row matched by - // its own apiKeyEnv-bearing section shape). Presence asserted both - // ways, so a moved seam names itself instead of silently hiding the - // capsule. + // The balance route resolves the provider's settings row off the + // Config-form `describe()` projection (the row matched by its own + // apiKeyEnv-bearing section shape); the retired `get(ns)` section read + // is asserted absent, so a moved seam names itself instead of silently + // hiding the capsule. const balance = baseline.balance assert.equal( staging.dshHasString(baseline.tag, balance.settingsGetNeedle, balance.settingsFile), @@ -158,11 +150,11 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base test('client: the preferences card seat and transport match the generation', () => { // The keyed slot the browser half's card registers on and the settings - // transport it rides: `settings.plugin.item` + `settingsScope` through - // V3, the Plugins page's `plugins.bundle.config` + `configForms` from - // the Config-form generation on. Asserted BOTH ways (like the sidebar - // seam) so a moved seam cannot read as "unsupported here" — a slot the - // generation does not declare simply never receives the registration. + // transport it rides: the Plugins page's `plugins.bundle.config` + + // `configForms` on every supported line. Asserted BOTH ways (like the + // sidebar seam) so a moved seam cannot read as "unsupported here" — a + // slot the generation does not declare simply never receives the + // registration. const settings = baseline.settings assert.equal( settings.cardSlotFiles.some(pattern => @@ -223,7 +215,7 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base test('client: the session-jump seam of this generation (issue #90)', () => { // The view owner's navigation verb (the sidebar row click's own), with // the signature this line spells; and the retired sessions-service - // `open(id)` selection verb, declared exactly where the line still has it. + // `open(id)` selection verb, probed staying gone. const nav = baseline.client.sessionNav assert.equal( staging.dshHasString(baseline.tag, nav.workspaceNeedle, nav.workspaceFile), @@ -248,10 +240,9 @@ describe.skipIf(reasons.length > 0)('compat matrix — real dsh sources per base }) test('client: the icon seams resolve on this generation (primitives.ts)', () => { - for (const [modern, legacy] of staging.ICON_SEAMS) { - const present = staging.dshHasString(baseline.tag, modern, 'packages/client/ui-primitives/src/**') - || staging.dshHasString(baseline.tag, legacy, 'packages/client/ui-primitives/src/**') - assert.equal(present, true, `icon seam: ${modern} | ${legacy}`) + for (const name of staging.ICON_SEAMS) { + const present = staging.dshHasString(baseline.tag, name, 'packages/client/ui-primitives/src/**') + assert.equal(present, true, `icon seam: ${name}`) } }) diff --git a/tests/compat/staging.ts b/tests/compat/staging.ts index 9cf3d63..d2857f9 100644 --- a/tests/compat/staging.ts +++ b/tests/compat/staging.ts @@ -85,13 +85,6 @@ export function ensureHostDeps(baseline: Baseline): void { if (run.status !== 0) throw new Error(`npm install ${JSON.stringify(deps)} failed: ${(run.stderr ?? '').trim().slice(0, 300)}`) } -/** The tag's settings namespace pattern, read off the file the baseline names (null when absent). */ -export function namespacePatternOf(baseline: Baseline, file: string): RegExp | null { - const source = dshShow(baseline.tag, file) - const match = /^const NAMESPACE_PATTERN = \/(.+)\/([gimsuy]*)$/m.exec(source) - return match === null ? null : new RegExp(match[1], match[2]) -} - export interface DriverReport { registered: boolean keys: string[] @@ -305,9 +298,8 @@ export function bundleRequires(): string[] { * The slot registrations the client half mounts on EVERY baseline (identical * spellings in each generation's sources). The preferences card seat and the * settings transport are asserted separately, per generation — the matrix - * spec pins them to the baseline's `settings` seam (`settings.plugin.item` + - * `settingsScope` through V3, the Plugins page's `plugins.bundle.config` + - * `configForms` from the Config-form generation on). + * spec pins them to the baseline's `settings` seam (`plugins.bundle.config` + + * `configForms` on every supported line). */ export const SLOT_SEAMS = [ 'conversation.view', @@ -316,29 +308,30 @@ export const SLOT_SEAMS = [ ] as const /** - * The ui-primitives icon seams the client bundle renders, as [modern, legacy] - * name pairs: the vocabulary renamed across the range (`…Outline16`/`…14` - * through 0.1.5, `…OutlineRegular`/`…OutlineMedium` from 0.1.6), and a - * missing name is `undefined` at the browser's runtime — React error #130. - * `src/client/primitives.ts` resolves whichever spelling the running - * generation serves; each pair must exist in at least one spelling per tag. + * The ui-primitives icon seams the client bundle renders: every supported + * line ships the `…OutlineRegular`/`…OutlineMedium` vocabulary, and a missing + * name is `undefined` at the browser's runtime — React error #130. + * `src/client/primitives.ts` resolves each by name; every one must exist per + * tag. */ export const ICON_SEAMS = [ - ['IconBranchOutlineRegular', 'IconBranchOutline16'], - ['IconPlusOutlineRegular', 'IconPlusOutline16'], - ['IconCheckOutlineRegular', 'IconCheckOutline16'], - ['IconCopyOutlineRegular', 'IconCopyOutline16'], - ['IconCloseOutlineRegular', 'IconCloseOutline16'], - ['IconSettingsOutlineMedium', 'IconSettingsOutline14'], - ['IconChevronDownOutlineMedium', 'IconChevronDownOutline14'], + 'IconBranchOutlineRegular', + 'IconPlusOutlineRegular', + 'IconCheckOutlineRegular', + 'IconCopyOutlineRegular', + 'IconCloseOutlineRegular', + 'IconSettingsOutlineMedium', + 'IconChevronDownOutlineMedium', + 'IconChevronUpOutlineMedium', + 'IconSearchOutlineRegular', ] as const /** * The event families the host fold switches on (src/host/fold.ts) — the UNION - * over every supported generation (developer/message is V4-only). The - * per-baseline probe asserts the baseline's own `foldEventTypes` subset, and - * a matrix test asserts this union equals the baselines' union — a fold case - * added without a baseline list fails loudly instead of going unprobed. + * over every supported generation. The per-baseline probe asserts the + * baseline's own `foldEventTypes` subset, and a matrix test asserts this + * union equals the baselines' union — a fold case added without a baseline + * list fails loudly instead of going unprobed. */ export const FOLD_EVENT_TYPES = [ 'request/header', 'request/context', 'step/start', 'step/end', diff --git a/tests/host/backfill.spec.ts b/tests/host/backfill.spec.ts index 6beb34e..d14aac4 100644 --- a/tests/host/backfill.spec.ts +++ b/tests/host/backfill.spec.ts @@ -284,26 +284,21 @@ describe('watchActivityBackfill', () => { assert.deepEqual(state.coldSnapshots, []) }) - test('the 0.1.7 face answers when the 3-arg spelling throws (the rc.2 cachedSnapshot signature)', async () => { - // dsh 0.1.7-rc.2 dropped the probe's offset parameter: a 3-arg call binds - // the branded offset onto `keys`, where `new Set(0)` throws as soon as a - // served record is reached. The probe must fall through to the newer - // 2-arg face instead of reading every served session as "not served". + test('the cached-rows probe rides the 2-arg face (header + keys)', async () => { + // The projection cache's `cachedSnapshot(meta, keys?)` — the lifecycle + // identity rides the header alone on every supported line. const ctx = new Context() const state = fakeState([{ header: { id: 'a', cwd: '/repo/a' } }]) - const arities: number[] = [] + const calls: number[] = [] const route: RouteBox = {} ctx.provide('connection', connectionOf(route)) ctx.provide('sessionQuery', { - listSessions: async () => { - arities.push(0) - return state.listed - }, + listSessions: async () => state.listed, }) ctx.provide('sessionProjectionCache', { - cachedSnapshot: (header: { id: string }, offsetOrKeys: unknown, keys?: unknown) => { - arities.push(keys === undefined ? 2 : 3) - if (keys !== undefined) throw new TypeError('offset is not iterable') + cachedSnapshot: (header: { id: string }, keys?: unknown) => { + calls.push(1) + assert.ok(Array.isArray(keys), 'the probe passes the keys as the second argument') return header.id === 'a' ? { asOfSeq: 0, values: { contextActivity: { days: {} }, contextTimeline: { ok: true } } } : undefined @@ -317,12 +312,11 @@ describe('watchActivityBackfill', () => { ctx.provide('sessions', { get: () => undefined }) const dispose = watchActivityBackfill(ctx) await trigger(route) - await until(() => (arities.length > 0 ? true : undefined), 'the corpus was queried') + await until(() => (calls.length > 0 ? true : undefined), 'the corpus was queried') await new Promise(resolve => setTimeout(resolve, 40)) dispose() - assert.deepEqual(state.coldReads, [], 'the newer face served the rows — no cold read') + assert.deepEqual(state.coldReads, [], 'the served rows skip the cold read') assert.deepEqual(state.coldSnapshots, []) - assert.deepEqual([...new Set(arities)].sort(), [0, 2, 3], 'the 3-arg spelling was tried first, the 2-arg face answered') }) test('a rejection landing AFTER abort skips the warn (the unload owns the silence)', async () => { diff --git a/tests/host/balance.spec.ts b/tests/host/balance.spec.ts index ea5eeef..7b1a898 100644 --- a/tests/host/balance.spec.ts +++ b/tests/host/balance.spec.ts @@ -64,12 +64,7 @@ function ctxOf(spec: CtxSpec): { ctx: Context; captured: { path?: string; fetch? return { ctx: ctx as unknown as Context, captured, disposers } } -/** The harness settings face serving one namespace's resolved section. */ -function settingsOf(section: unknown): { get(ns: string): unknown } { - return { get: (ns: string) => (ns === 'llm-deepseek' ? section : undefined) } -} - -/** The V4+ settings face projecting one row per configurable entry (`throws` fails the read). */ +/** The harness settings face projecting one row per configurable entry (`throws` fails the read). */ function settingsDescribeOf(rows: unknown, throws = false): { describe(): unknown } { return { describe() { @@ -79,6 +74,11 @@ function settingsDescribeOf(rows: unknown, throws = false): { describe(): unknow } } +/** The settings face serving one provider section, as the Config-form projection's `llm-deepseek` row. */ +function settingsOf(section: unknown): { describe(): unknown } { + return settingsDescribeOf([{ ns: 'llm-deepseek', value: section }]) +} + /** The harness credentials face serving one resolved value. */ function credentialsOf(value: unknown, calls?: string[]): { resolve(ref: string): Promise<{ value: unknown } | undefined> } { return { @@ -95,11 +95,11 @@ const CNY_BALANCE = { balance_infos: [{ currency: 'CNY', total_balance: '110.00', granted_balance: '10.00', topped_up_balance: '100.00' }], } -/** The all-configured ctx spec: a default section, a resolvable key. */ +/** The all-configured ctx spec: the provider row (its served value always declares the credential ref), a resolvable key. */ function configuredCtx(spec: CtxSpec = {}): CtxSpec { return { connection: { fetch: { register: () => () => {} } }, - settings: settingsOf({}), + settings: settingsOf({ apiKeyEnv: 'DEEPSEEK_API_KEY' }), credentials: credentialsOf('sk-test'), ...spec, } @@ -180,7 +180,7 @@ describe('balance route outcomes', () => { const refs: string[] = [] const { ctx, captured } = ctxOf({ connection: { fetch: { register: () => () => {} } }, - settings: settingsOf({}), + settings: settingsOf({ apiKeyEnv: 'DEEPSEEK_API_KEY' }), credentials: credentialsOf('sk-test', refs), }) watchBalanceChannel(ctx) @@ -230,7 +230,7 @@ describe('balance route outcomes', () => { const absent: CtxSpec[] = [ configuredCtx({ settings: undefined }), configuredCtx({ settings: {} }), - configuredCtx({ settings: { get: 'nope' } }), + configuredCtx({ settings: { describe: 'nope' } }), configuredCtx({ settings: settingsOf(undefined) }), configuredCtx({ settings: settingsOf('not a section') }), configuredCtx({ credentials: undefined }), @@ -292,8 +292,8 @@ describe('balance route outcomes', () => { }) }) -describe('settings faces across generations', () => { - test('the V4+ describe face serves the facts by the provider row\'s own shape', async () => { +describe('settings face (the Config-form projection)', () => { + test('the describe face serves the facts by the provider row\'s own shape', async () => { const refs: string[] = [] const { ctx, captured } = ctxOf({ connection: { fetch: { register: () => () => {} } }, @@ -345,38 +345,6 @@ describe('settings faces across generations', () => { assert.deepEqual(refs, ['PRODUCT_KEY']) }) - test('a V3 section that reads absent falls through to the describe face', async () => { - const refs: string[] = [] - const { ctx, captured } = ctxOf({ - connection: { fetch: { register: () => () => {} } }, - settings: { - get: () => undefined, - describe: () => [{ ns: 'llm-deepseek-api-key', value: { apiKeyEnv: 'FALLBACK_KEY' } }], - }, - credentials: credentialsOf('sk-fallback', refs), - }) - watchBalanceChannel(ctx) - stubPlatform(CNY_BALANCE) - await serve(captured) - assert.deepEqual(refs, ['FALLBACK_KEY']) - }) - - test('the V3 section wins when both faces are served', async () => { - const refs: string[] = [] - const { ctx, captured } = ctxOf({ - connection: { fetch: { register: () => () => {} } }, - settings: { - get: (ns: string) => (ns === 'llm-deepseek' ? { apiKeyEnv: 'V3_KEY' } : undefined), - describe: () => [{ ns: 'llm-deepseek-api-key', value: { apiKeyEnv: 'V4_KEY' } }], - }, - credentials: credentialsOf('sk-both', refs), - }) - watchBalanceChannel(ctx) - stubPlatform(CNY_BALANCE) - await serve(captured) - assert.deepEqual(refs, ['V3_KEY']) - }) - test('every broken describe shape serves a typed null without touching the platform', async () => { const faces: unknown[] = [ settingsDescribeOf('not an array'), diff --git a/tests/host/compat/registry.spec.ts b/tests/host/compat/registry.spec.ts index 9e55404..eb6b153 100644 --- a/tests/host/compat/registry.spec.ts +++ b/tests/host/compat/registry.spec.ts @@ -296,16 +296,3 @@ for (const [index, baseline] of BASELINES.entries()) { }) }) } - -describe('registry contract — settings namespace seam', () => { - // The settings module enforces `/^[a-z][a-z0-9-]*$/` on registered - // namespaces (inside `settings.register` on the 0.1.5 line). The plugin - // registers the raw literal (branded cast; on the Config-form generations - // the namespace is the entry id), so the literal must pass the pattern. - const NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*$/ - const SETTINGS_NAMESPACE = 'dsh-context' - - test('the plugin namespace passes the enforcement pattern of every baseline', () => { - assert.equal(NAMESPACE_PATTERN.test(SETTINGS_NAMESPACE), true) - }) -}) diff --git a/tests/host/compat/registryDriver.ts b/tests/host/compat/registryDriver.ts index 9c5c05e..612a23e 100644 --- a/tests/host/compat/registryDriver.ts +++ b/tests/host/compat/registryDriver.ts @@ -3,7 +3,6 @@ // source at each baseline tag so the always-on suite pins the plugin's units // against every supported registry generation: // -// dsh-v0.1.5-rc.1 packages/session/session-projection/src/index.ts // dsh-v0.1.7-rc.2 packages/session/session-projection/src/index.ts // dsh-v0.2.0-rc.2 packages/session/session-projection/src/index.ts // diff --git a/tests/host/fixtures/version/homes/baseline/profiles/node_modules/@deepseek-ai/dsh/package.json b/tests/host/fixtures/version/homes/baseline/profiles/node_modules/@deepseek-ai/dsh/package.json index fd851bd..cb23cb4 100644 --- a/tests/host/fixtures/version/homes/baseline/profiles/node_modules/@deepseek-ai/dsh/package.json +++ b/tests/host/fixtures/version/homes/baseline/profiles/node_modules/@deepseek-ai/dsh/package.json @@ -1 +1 @@ -{"name":"@deepseek-ai/dsh","version":"0.1.5-rc.1"} +{"name":"@deepseek-ai/dsh","version":"0.1.7-rc.2"} \ No newline at end of file diff --git a/tests/host/fixtures/version/homes/module-skips-cli/profiles/node_modules/@deepseek-ai/dsh-session-projection/package.json b/tests/host/fixtures/version/homes/module-skips-cli/profiles/node_modules/@deepseek-ai/dsh-session-projection/package.json index 4d18320..ab88fee 100644 --- a/tests/host/fixtures/version/homes/module-skips-cli/profiles/node_modules/@deepseek-ai/dsh-session-projection/package.json +++ b/tests/host/fixtures/version/homes/module-skips-cli/profiles/node_modules/@deepseek-ai/dsh-session-projection/package.json @@ -1 +1 @@ -{"name":"@deepseek-ai/dsh-session-projection","version":"0.1.5-rc.1"} +{"name":"@deepseek-ai/dsh-session-projection","version":"0.1.7-rc.2"} \ No newline at end of file diff --git a/tests/host/fixtures/version/homes/module-skips-cli/profiles/node_modules/@deepseek-ai/dsh-session/package.json b/tests/host/fixtures/version/homes/module-skips-cli/profiles/node_modules/@deepseek-ai/dsh-session/package.json index fda15c0..5b952b9 100644 --- a/tests/host/fixtures/version/homes/module-skips-cli/profiles/node_modules/@deepseek-ai/dsh-session/package.json +++ b/tests/host/fixtures/version/homes/module-skips-cli/profiles/node_modules/@deepseek-ai/dsh-session/package.json @@ -1 +1 @@ -{"name":"@deepseek-ai/dsh-session","version":"0.1.5-rc.1"} +{"name":"@deepseek-ai/dsh-session","version":"0.1.7-rc.2"} \ No newline at end of file diff --git a/tests/host/fixtures/version/homes/old/profiles/node_modules/@deepseek-ai/dsh/package.json b/tests/host/fixtures/version/homes/old/profiles/node_modules/@deepseek-ai/dsh/package.json index ccf2039..8a28e3c 100644 --- a/tests/host/fixtures/version/homes/old/profiles/node_modules/@deepseek-ai/dsh/package.json +++ b/tests/host/fixtures/version/homes/old/profiles/node_modules/@deepseek-ai/dsh/package.json @@ -1 +1 @@ -{"name":"@deepseek-ai/dsh","version":"0.1.1-rc.2"} +{"name":"@deepseek-ai/dsh","version":"0.1.5-rc.1"} \ No newline at end of file diff --git a/tests/host/fold-v3.spec.ts b/tests/host/fold-dialect.spec.ts similarity index 99% rename from tests/host/fold-v3.spec.ts rename to tests/host/fold-dialect.spec.ts index b8c67d6..6dd6492 100644 --- a/tests/host/fold-v3.spec.ts +++ b/tests/host/fold-dialect.spec.ts @@ -1,4 +1,4 @@ -// The supported log dialect (dsh 0.1.5-rc.1+, session formats V3/V4): +// The supported log dialect (dsh 0.1.7-rc.2+, session format V4): // `system/message` surface nodes, the embedded assistant stream, the // `startSeq`/`endSeq` replacement endpoints, `tool/ptc-dispatch`, and // `assistant/attempt` — each case the seam the fold must read, plus the @@ -43,7 +43,7 @@ describe('system/message — the system prompt as a surface node', () => { assert.equal(view.current.total, price + state.sums.user + state.sums.assistant, 'the prompt joins the composition total') }) - test('a system-less request header never clears log-sourced nodes (V3 headers carry no prompt)', () => { + test('a system-less request header never clears log-sourced nodes (headers carry no prompt)', () => { const prompt = 'prompt '.repeat(10) const { state } = driveTimeline([ { type: 'system/message', seq: 1, time: 1000, data: { message: { content: text(prompt) } }, surfaceOp: 'append' }, diff --git a/tests/host/fold-events.spec.ts b/tests/host/fold-events.spec.ts index d4764b7..37bdf4a 100644 --- a/tests/host/fold-events.spec.ts +++ b/tests/host/fold-events.spec.ts @@ -307,25 +307,27 @@ describe('tool/result skill tagging', () => { assert.equal(state.events.length, 0) }) - test('skill content nested two tool-result levels deep still tags', () => { + test('skill content nested inside a foreign wrapper block never tags', () => { + // The skill wrapper rides the result's own text blocks (first-level); a + // nested foreign block is a hostile shape — unscanned, never a throw. const ev: TimelineEvent = { type: 'tool/result', seq: 1, time: at(), data: { message: { - content: [{ type: 'tool-result', content: [{ type: 'tool-result', content: skillBody('deep') }] }], + content: [{ type: 'foreign-wrapper', content: [{ type: 'text', text: 'x' }] }], }, }, surfaceOp: 'append', } const { state } = driveTimeline([ev]) - assert.equal(state.surface.at(-1)?.skill, 'deep') - assert.equal(state.events[0].name, 'deep') + assert.equal(state.surface.at(-1)?.skill, undefined) + assert.equal(state.events.length, 0) }) - test('nested empty text yields no tag', () => { + test('an empty text block yields no tag', () => { const ev: TimelineEvent = { type: 'tool/result', seq: 1, time: at(), - data: { message: { content: [{ type: 'tool-result', toolCallId: 'e', content: [{ type: 'text', text: '' }] }] } }, + data: { message: { role: 'tool', toolCallId: 'e', content: [{ type: 'text', text: '' }] } }, surfaceOp: 'append', } const { state } = driveTimeline([ev]) diff --git a/tests/host/fold-ops.spec.ts b/tests/host/fold-ops.spec.ts index 6b022ba..b1597d6 100644 --- a/tests/host/fold-ops.spec.ts +++ b/tests/host/fold-ops.spec.ts @@ -84,24 +84,23 @@ describe('the file-op log — call/result pairing', () => { assert.equal(state.callNames.c1, undefined, 'the pending entry consumed at the result') }) - test('the error flag reads the envelope error object, the block isError, or the V4 message isError', () => { + test('the error flag reads the message isError mark', () => { const { state } = driveTimeline([ toolCall(1, { callId: 'c1', name: 'read', arguments: JSON.stringify({ file_path: 'a.ts' }) }), toolResult(2, { callId: 'c1', content: [{ type: 'text', text: 'no' }], error: true }), toolCall(3, { callId: 'c2', name: 'read', arguments: JSON.stringify({ file_path: 'b.ts' }) }), - // A block-level isError (no envelope error object). + // A hostile envelope-only mark (the retired pre-V4 spelling): the + // admission validation forbids `data.error` without the message mark, + // so a row carrying it alone reads as non-error. { type: 'tool/result', seq: 4, time: 4, data: { - callId: 'c2', - message: { content: [{ type: 'tool-result', toolCallId: 'c2', isError: true, content: [] }], source: { kind: 'tool', callId: 'c2' } }, + error: true, + message: { role: 'tool', toolCallId: 'c2', source: { kind: 'tool', callId: 'c2' }, content: [] }, }, } as unknown as TimelineEvent, - toolCall(5, { callId: 'c3', name: 'read', arguments: JSON.stringify({ file_path: 'c.ts' }) }), - // The V4 spelling: the mark lifted onto the message, no envelope error. - toolResult(6, { callId: 'c3', content: [{ type: 'text', text: 'v4' }], error: true, v4: true }), ]) - assert.deepEqual(state.fileOps.map(o => o.err), [true, true, true]) + assert.deepEqual(state.fileOps.map(o => o.err), [true, false]) }) test('a call whose raw arguments are not a string still pairs by name (ops degrade to nothing)', () => { diff --git a/tests/host/fold-surface.spec.ts b/tests/host/fold-surface.spec.ts index 714337e..35feb6a 100644 --- a/tests/host/fold-surface.spec.ts +++ b/tests/host/fold-surface.spec.ts @@ -49,36 +49,18 @@ describe('applySurface tool/result branch', () => { ]) const node = state.surface.at(-1) assert.equal(node?.tool, 'bash') - assert.equal(node?.err, undefined, 'no error flag without data.error') + assert.equal(node?.err, undefined, 'no error flag without the message mark') assert.deepEqual(state.callNames, {}, 'the consumed entry leaves the persisted map') }) - test('an unknown source callId falls through to the content block id', () => { - // Neither id is armed: the node ends up tool-less (a replay window where - // the tool/call event is gone). Note the fold assigns the lookup miss - // onto the node, so this state is not assertPlainJson-able. + test('an unknown source callId pairs nothing (a replay window where the tool/call event is gone)', () => { + // Note the fold assigns the lookup miss onto the node, so this state is + // not assertPlainJson-able. const { state } = driveTimeline([toolResult(1, { callId: 'ghost', content: text('ok') })]) assert.equal(state.surface.at(-1)?.tool, undefined) assert.deepEqual(state.callNames, {}) }) - test('a block toolCallId hit names the node when the source id is unknown', () => { - const ev: TimelineEvent = { - type: 'tool/result', seq: 2, time: at(), - data: { - callId: 'x', - message: { - source: { kind: 'tool', callId: 'x' }, - content: [{ type: 'tool-result', toolCallId: 'c1', content: text('ok') }], - }, - }, - surfaceOp: 'append', - } - const { state } = driveTimeline([toolCall(1, { callId: 'c1', name: 'bash' }), ev]) - assert.equal(state.surface.at(-1)?.tool, 'bash') - assert.deepEqual(state.callNames, {}, 'both the source id and the block id are consumed') - }) - test('consume-once rebuild keeps the other pending calls', () => { const { state } = driveTimeline([ toolCall(1, { callId: 'c1', name: 'bash' }), @@ -88,10 +70,13 @@ describe('applySurface tool/result branch', () => { assert.equal(state.callNames.c2?.name, 'read', 'the unanswered call stays armed') }) - test('a result with neither id leaves callNames untouched', () => { + test('a result without the source callId pairs nothing and leaves callNames untouched', () => { + // A hostile/foreign envelope: the admission validation requires the + // source callId, so a source-less row can only arrive hostile — it folds + // to an unnamed node instead of throwing. const ev: TimelineEvent = { type: 'tool/result', seq: 2, time: at(), - data: { message: { content: [{ type: 'tool-result', content: text('ok') }] } }, + data: { message: { role: 'tool', toolCallId: 'keep', content: text('ok') } }, surfaceOp: 'append', } const { state } = driveTimeline([toolCall(1, { callId: 'keep', name: 'read' }), ev]) @@ -99,22 +84,33 @@ describe('applySurface tool/result branch', () => { assert.equal(state.callNames.keep?.name, 'read') }) - test('data.error flags the node', () => { + test('the message isError flags the node', () => { const { state } = driveTimeline([ toolCall(1, { callId: 'c1', name: 'bash' }), toolResult(2, { callId: 'c1', content: text('boom'), error: true }), ]) - assert.equal(state.surface.at(-1)?.err, true) - }) - - test('the V4 message isError flags the node (no envelope error object)', () => { - const { state } = driveTimeline([ - toolCall(1, { callId: 'c1', name: 'bash' }), - toolResult(2, { callId: 'c1', content: text('boom'), error: true, v4: true }), - ]) const node = state.surface.at(-1) assert.equal(node?.err, true) - assert.equal(node?.tool, 'bash', 'the V4 source callId still pairs the node') + assert.equal(node?.tool, 'bash', 'the source callId still pairs the node') + }) + + test('the retired envelope-only error mark no longer flags the node', () => { + // The pre-V4 spelling (an event-level `data.error` with no message mark) + // is unreachable through the supported harnesses' admission validation; + // a hostile replay carrying it degrades to an unflagged node, never a + // throw. + const ev: TimelineEvent = { + type: 'tool/result', seq: 2, time: at(), + data: { + error: true, + message: { role: 'tool', toolCallId: 'c1', source: { kind: 'tool', callId: 'c1' }, content: text('boom') }, + }, + surfaceOp: 'append', + } + const { state } = driveTimeline([toolCall(1, { callId: 'c1', name: 'bash' }), ev]) + const node = state.surface.at(-1) + assert.equal(node?.err, undefined) + assert.equal(node?.tool, 'bash') }) }) diff --git a/tests/host/fold-timing.spec.ts b/tests/host/fold-timing.spec.ts index 39eeaa0..68acfae 100644 --- a/tests/host/fold-timing.spec.ts +++ b/tests/host/fold-timing.spec.ts @@ -145,26 +145,15 @@ describe('timing — tool call durations', () => { }) }) - test('repeated names accumulate; the block-id fallback prices too', () => { + test('repeated names accumulate into the per-name tally', () => { const call = (seq: number, callId: string, time: number): TimelineEvent => ({ type: 'tool/call', seq, time, data: { callId, name: 'bash', arguments: '{}' }, }) - const blockResult: TimelineEvent = { - type: 'tool/result', seq: 5, time: 9_000, - data: { - callId: 'x', - message: { - source: { kind: 'tool', callId: 'x' }, - content: [{ type: 'tool-result', toolCallId: 'c2', content: text('ok') }], - }, - }, - surfaceOp: 'append', - } const { state } = driveTimeline([ call(1, 'c1', 1_000), toolResult(2, { callId: 'c1', content: text('ok'), time: 3_000 }), call(4, 'c2', 5_000), - blockResult, + toolResult(5, { callId: 'c2', content: text('ok'), time: 9_000 }), ]) assert.equal(state.timing?.toolsMs, 2_000 + 4_000) assert.deepEqual(state.timing?.tools, { bash: { calls: 2, ms: 6_000 } }) diff --git a/tests/host/helpers/events.ts b/tests/host/helpers/events.ts index 83fd7f4..c741712 100644 --- a/tests/host/helpers/events.ts +++ b/tests/host/helpers/events.ts @@ -60,33 +60,26 @@ export function stepEnd(seq: number, opts: { time?: number } = {}): TimelineEven return { type: 'step/end', seq, time: at(opts.time) } } -/** tool/result: the model-visible message rides data.message with the tool source. */ +/** tool/result: the model-visible message rides data.message — the first-class + * tool-role message (the lifted `toolCallId`/`isError`, direct content) whose + * tool source carries the callId the fold pairs on. */ export function toolResult(seq: number, opts: { callId: string content: ContentBlock[] + /** The durable error mark: `message.isError`. */ error?: boolean - /** Drop the durable source (a legacy/foreign envelope). */ - noSource?: boolean - /** Drop the envelope callId (source carries it). */ - noEnvelopeId?: boolean /** The bounded presentation meta (search matches / read window) on the durable event. */ meta?: unknown time?: number - /** - * The V4 spelling: `role: 'tool'`, the lifted message `toolCallId`/`isError`, - * direct content (no wrapper block), and no envelope `error` object — the - * mark rides the message alone. - */ - v4?: boolean }): TimelineEvent { - const message: Record = opts.v4 === true - ? { role: 'tool', toolCallId: opts.callId, content: opts.content } - : { content: [{ type: 'tool-result', toolCallId: opts.callId, content: opts.content }] } - if (opts.noSource !== true) message.source = { kind: 'tool', callId: opts.callId } - if (opts.v4 === true && opts.error === true) message.isError = true + const message: Record = { + role: 'tool', + toolCallId: opts.callId, + source: { kind: 'tool', callId: opts.callId }, + content: opts.content, + } + if (opts.error === true) message.isError = true const data: Record = { message } - if (opts.noEnvelopeId !== true) data.callId = opts.callId - if (opts.v4 !== true && opts.error === true) data.error = true if (opts.meta !== undefined) data.meta = opts.meta return { type: 'tool/result', seq, time: at(opts.time), data, surfaceOp: 'append' } } diff --git a/tests/host/index.spec.ts b/tests/host/index.spec.ts index 9fa12c7..60b53f8 100644 --- a/tests/host/index.spec.ts +++ b/tests/host/index.spec.ts @@ -210,8 +210,7 @@ describe('dsh-context host plugin', () => { // A file-tool pair: the fold-derived op log serves through the detail endpoint. const call = session.append('tool/call', { turn: 1, step: 1, callId: 'c1', name: 'read', arguments: JSON.stringify({ file_path: 'src/a.ts' }) } as never) session.append('tool/result', { - callId: 'c1', - message: { content: [{ type: 'tool-result', toolCallId: 'c1', content: [{ type: 'text', text: 'ok' }] }], source: { kind: 'tool', callId: 'c1' } }, + message: { role: 'tool', toolCallId: 'c1', content: [{ type: 'text', text: 'ok' }], source: { kind: 'tool', callId: 'c1' } }, } as never, { surfaceOp: 'append', sourceEventSeqs: [call.seq] }) const timeline = ctx.sessionProjections.snapshot(session).values.contextTimeline @@ -279,7 +278,7 @@ describe('the baseline gate', () => { const timeline = ctx.sessionProjections.snapshot(session).values.contextTimeline assert.ok(timeline !== undefined, 'the fallback unit still delivers (no eternal loading)') - assert.deepEqual(timeline.unsupported, { current: '0.1.1-rc.2', minimum: BASELINE_DSH_VERSION }) + assert.deepEqual(timeline.unsupported, { current: '0.1.5-rc.1', minimum: BASELINE_DSH_VERSION }) assert.equal(timeline.current.total, 0, 'the log is NOT folded below the baseline') assert.equal(timeline.nodes.length, 0, 'real appends leave no surface nodes') assert.equal(timeline.model, undefined, 'no model metadata either') diff --git a/tests/host/pricing.spec.ts b/tests/host/pricing.spec.ts index fbc207a..4a36d1d 100644 --- a/tests/host/pricing.spec.ts +++ b/tests/host/pricing.spec.ts @@ -47,11 +47,14 @@ describe('estimateMessage', () => { assert.equal(estimateMessage({ content: [{ type: 'tool-call' }] }), 8) }) - test('tool-result blocks price their nested content recursively', () => { + test('a retired wrapper block prices as generic JSON (the meter parity branch)', () => { + // The supported log generation carries no wrapper blocks; a hostile one + // prices through the same structural-JSON arm the meter applies to every + // unknown block. + const block = { type: 'tool-result', content: [{ type: 'text', text: 'ok' }] } assert.equal( - estimateMessage({ content: [{ type: 'tool-result', content: [{ type: 'text', text: 'ok' }] }] }), - // inner text: ceil(2/4)+4 = 5; wrapper: +4; role: +4 - 13, + estimateMessage({ content: [block] }), + 4 + Math.ceil(JSON.stringify(block).length / 4) + 4, ) }) diff --git a/tests/host/settings.spec.ts b/tests/host/settings.spec.ts deleted file mode 100644 index 85ca6a6..0000000 --- a/tests/host/settings.spec.ts +++ /dev/null @@ -1,161 +0,0 @@ -// Integration tests for the plugin settings namespace (src/host/settings.ts) -// against the REAL cordis context and the REAL dsh-settings provider base — -// the dsh-canonical harness pattern: an in-memory SettingsProvider subclass, -// mounted as a plugin, with installSettings layering the namespace on top. - -import assert from 'node:assert/strict' -import { describe, test } from 'vitest' -import { Context } from '@deepseek-ai/cordis' -import { SettingsProvider } from '@deepseek-ai/dsh-settings' -import type { SettingsNamespace } from '@deepseek-ai/dsh-settings' -import { installSettings, SETTINGS_NAMESPACE } from '../../src/host/settings' -import type { PluginSettings } from '../../src/host/settings' - -/** A provider implementing only the two storage primitives; the Service Definition owns the rest. */ -class MemorySettings extends SettingsProvider { - doc: Record - - constructor(ctx: ConstructorParameters[0], options?: { doc?: Record }) { - super(ctx) - this.doc = structuredClone(options?.doc ?? {}) - } - - get writable(): boolean { - return true - } - - protected load(): Promise> { - return Promise.resolve(structuredClone(this.doc)) - } - - protected persist(ns: SettingsNamespace, section: Record): Promise { - this.doc[String(ns)] = structuredClone(section) - return Promise.resolve() - } -} - -// The raw string is what the dsh settings `register` accepts at runtime; the -// branded cast satisfies the SettingsNamespace type face (see -// src/host/settings.ts). -const ns = SETTINGS_NAMESPACE as SettingsNamespace - -/** Poll until the inject callback inside installSettings has registered the namespace. */ -async function untilRegistered(ctx: Context): Promise { - for (let i = 0; i < 200; i++) { - if (ctx.settings.get(ns) !== undefined) return - await new Promise(resolve => setTimeout(resolve, 1)) - } - assert.fail('the dsh-context settings namespace was never registered') -} - -async function boot(doc?: Record) { - const ctx = new Context() - await ctx.plugin(MemorySettings, doc === undefined ? undefined : { doc }) - installSettings(ctx) - await untilRegistered(ctx) - return { ctx, provider: ctx.get('settings') as MemorySettings } -} - -describe('installSettings', () => { - test('the namespace is the plugin short name', () => { - assert.equal(SETTINGS_NAMESPACE, 'dsh-context') - }) - - test('registers the dsh-context namespace with schema defaults', async () => { - const { ctx } = await boot() - const descriptors = ctx.settings.describe() - assert.ok(descriptors.some(d => String(d.ns) === 'dsh-context'), 'the namespace is registered') - assert.deepEqual(ctx.settings.get(ns), { - defaultPlacement: 'all', - defaultGranularity: 'step', - defaultTrendMode: 'total', - defaultDeltaBase: 'step', - defaultToolSort: 'count', - defaultFileSort: 'count', - insightsEntry: 'show', - defaultDurationCurve: 'show', - }, 'schema defaults resolve') - }) - - test('updates flow through the real scope; invalid values reject', async () => { - const { ctx, provider } = await boot() - await ctx.settings.update(ns, { defaultGranularity: 'turn' }) - assert.deepEqual(ctx.settings.get(ns), { - defaultPlacement: 'all', - defaultGranularity: 'turn', - defaultTrendMode: 'total', - defaultDeltaBase: 'step', - defaultToolSort: 'count', - defaultFileSort: 'count', - insightsEntry: 'show', - defaultDurationCurve: 'show', - }, 'the update resolves over the schema defaults') - assert.deepEqual(provider.doc['dsh-context'], { defaultGranularity: 'turn' }, 'the provider persisted the section') - - await ctx.settings.update(ns, { defaultPlacement: 'sidebar', defaultTrendMode: 'delta', defaultDeltaBase: 'turn', defaultFileSort: 'path', insightsEntry: 'hide', defaultDurationCurve: 'hide' }) - assert.deepEqual(ctx.settings.get(ns), { - defaultPlacement: 'sidebar', - defaultGranularity: 'turn', - defaultTrendMode: 'delta', - defaultDeltaBase: 'turn', - defaultToolSort: 'count', - defaultFileSort: 'path', - insightsEntry: 'hide', - defaultDurationCurve: 'hide', - }, 'every preference field resolves independently') - - await assert.rejects( - ctx.settings.update(ns, { defaultGranularity: 'week' }), - 'an unknown granularity fails validation before anything persists', - ) - // The loose fields degrade instead of rejecting: a stale file sort, - // placement, tool sort, delta baseline, insights entry, or curve choice resolves to the default. - await ctx.settings.update(ns, { defaultFileSort: 'net', defaultPlacement: 'window', defaultToolSort: 'net', defaultDeltaBase: 'net', insightsEntry: 'gone', defaultDurationCurve: 'always' }) - assert.deepEqual(ctx.settings.get(ns), { - defaultPlacement: 'all', - defaultGranularity: 'turn', - defaultTrendMode: 'delta', - defaultDeltaBase: 'step', - defaultToolSort: 'count', - defaultFileSort: 'count', - insightsEntry: 'show', - defaultDurationCurve: 'show', - }, 'a stale value degrades to the schema default') - assert.deepEqual( - provider.doc['dsh-context'], - { defaultPlacement: 'window', defaultGranularity: 'turn', defaultTrendMode: 'delta', defaultToolSort: 'net', defaultDeltaBase: 'net', defaultFileSort: 'net', insightsEntry: 'gone', defaultDurationCurve: 'always' }, - 'the stale value stays raw in storage and degrades at read', - ) - }) - - test('a stale persisted preference degrades to the default (loose)', async () => { - const { ctx } = await boot({ 'dsh-context': { defaultPlacement: 'window', defaultTrendMode: 'net', defaultToolSort: 'alpha', defaultDeltaBase: 'net', defaultFileSort: 'alpha', insightsEntry: 'gone', defaultDurationCurve: 'always' } }) - const value = ctx.settings.get(ns) as PluginSettings - assert.equal(value.defaultPlacement, 'all', 'the stale placement falls back instead of breaking the section') - assert.equal(value.defaultTrendMode, 'total', 'the stale value falls back instead of breaking the section') - assert.equal(value.defaultDeltaBase, 'step', 'the stale delta baseline falls back instead of breaking the section') - assert.equal(value.defaultToolSort, 'count', 'the stale tool sort falls back instead of breaking the section') - assert.equal(value.defaultFileSort, 'count', 'the stale file sort falls back instead of breaking the section') - assert.equal(value.insightsEntry, 'show', 'the stale insights entry falls back to visible instead of breaking the section') - assert.equal(value.defaultDurationCurve, 'show', 'the stale curve choice falls back to visible instead of breaking the section') - assert.equal(value.defaultGranularity, 'step') - }) - - test('without a settings provider the install is inert', () => { - const ctx = new Context() - assert.doesNotThrow(() => installSettings(ctx)) - assert.equal(ctx.get('settings'), undefined, 'no provider composed, nothing registered') - }) - - test('a settings service without the register face (dsh V4+) stays inert', async () => { - // The V4+ settings service derives forms from the entry's own Config - // schema and no longer carries `settings.register`; the install must - // feature-detect the face instead of throwing inside the inject fiber. - const ctx = new Context() - ctx.provide('settings', { describe: () => [] }) - assert.doesNotThrow(() => installSettings(ctx)) - // Let the inject callback run; a mis-spelled call would fail the fiber. - await new Promise(resolve => setTimeout(resolve, 20)) - assert.ok(ctx.get('settings') !== undefined, 'the foreign service stays composed') - }) -}) diff --git a/tests/host/version.spec.ts b/tests/host/version.spec.ts index a5b0aaa..0d72098 100644 --- a/tests/host/version.spec.ts +++ b/tests/host/version.spec.ts @@ -178,10 +178,12 @@ beforeAll(() => { // baseline release (the gate must still trip from the running anchor), and // a resolving-but-unreadable library (the trusted witness with no answer). writeScratchHome('running-supported', '@deepseek-ai/dsh-session', - JSON.stringify({ name: '@deepseek-ai/dsh-session', version: '0.1.5-rc.1', exports: { '.': './lib/index.js' } }), + JSON.stringify({ name: '@deepseek-ai/dsh-session', version: '0.1.7-rc.2', exports: { '.': './lib/index.js' } }), { path: 'lib/index.js' }) + // The just-dropped line: below the baseline since the floor moved to + // 0.1.7-rc.2, so the gate must trip on it. writeScratchHome('running-old', '@deepseek-ai/dsh-session', - JSON.stringify({ name: '@deepseek-ai/dsh-session', version: '0.1.1-rc.2', exports: { '.': './lib/index.js' } }), + JSON.stringify({ name: '@deepseek-ai/dsh-session', version: '0.1.5-rc.1', exports: { '.': './lib/index.js' } }), { path: 'lib/index.js' }) writeScratchHome('running-decoy', '@deepseek-ai/dsh-session', JSON.stringify({ name: '@deepseek-ai/dsh-decoy', version: '9.9.9', exports: { '.': './lib/index.js' } }), @@ -199,13 +201,13 @@ describe('detectHarnessVersion — running anchor', () => { test('outranks a stale home mirror (issue #59: a healthy Desktop harness)', () => { // The mirror names an old global CLI while the running tree is a supported // release: the running anchor wins, so the gate never trips. - assert.equal(detectHarnessVersion(ctxWithHome(homeResolver('old')), runningResolver('running-supported', scratch), ELSEWHERE), '0.1.5-rc.1') + assert.equal(detectHarnessVersion(ctxWithHome(homeResolver('old')), runningResolver('running-supported', scratch), ELSEWHERE), '0.1.7-rc.2') }) test('a below-baseline running tree still trips the gate over a newer mirror', () => { // The reverse direction must hold too: a genuinely old harness is reported // even when the mirror names something newer. - assert.equal(detectHarnessVersion(ctxWithHome(homeResolver('future')), runningResolver('running-old', scratch), ELSEWHERE), '0.1.1-rc.2') + assert.equal(detectHarnessVersion(ctxWithHome(homeResolver('future')), runningResolver('running-old', scratch), ELSEWHERE), '0.1.5-rc.1') }) test('discards a witness inside this package own closure for the home anchor', () => { @@ -214,14 +216,14 @@ describe('detectHarnessVersion — running anchor', () => { // default package root (the real one, which contains the repo's // node_modules) performs the discard. const ctx = ctxWithHome(homeResolver('old')) - assert.equal(detectHarnessVersion(ctx, runningResolver('module-skips-cli')), '0.1.1-rc.2') + assert.equal(detectHarnessVersion(ctx, runningResolver('module-skips-cli')), '0.1.5-rc.1') }) test('never probes the CLI package from the running anchor', () => { // The fixture pins the CLI at 0.0.1 but the libraries at the baseline: // only a library answer may come back. const ctx = ctxWithHome(scratchResolver('empty')) - assert.equal(detectHarnessVersion(ctx, runningResolver('module-skips-cli'), ELSEWHERE), '0.1.5-rc.1') + assert.equal(detectHarnessVersion(ctx, runningResolver('module-skips-cli'), ELSEWHERE), '0.1.7-rc.2') }) test('a resolving witness with no readable version falls through to home', () => { @@ -248,8 +250,8 @@ describe('detectHarnessVersion — running anchor', () => { describe('detectHarnessVersion — home anchor', () => { test('reads the CLI package manifest of the running installation', () => { const ctx = (home: string) => ctxWithHome(homeResolver(home)) - assert.equal(detectHarnessVersion(ctx('old'), NO_RUNNING), '0.1.1-rc.2') - assert.equal(detectHarnessVersion(ctx('baseline'), NO_RUNNING), '0.1.5-rc.1') + assert.equal(detectHarnessVersion(ctx('old'), NO_RUNNING), '0.1.5-rc.1') + assert.equal(detectHarnessVersion(ctx('baseline'), NO_RUNNING), '0.1.7-rc.2') assert.equal(detectHarnessVersion(ctx('future'), NO_RUNNING), '0.2.0') assert.equal(detectHarnessVersion(ctx('dev'), NO_RUNNING), '0.0.0-dev') }) diff --git a/tests/shared/estimate.spec.ts b/tests/shared/estimate.spec.ts index 6938762..0bf9a65 100644 --- a/tests/shared/estimate.spec.ts +++ b/tests/shared/estimate.spec.ts @@ -1,8 +1,8 @@ // The shared token heuristics (src/shared/estimate.ts): the fixed-density // figures the host fold and the client boundary must agree on. The system -// prompt is priced from EITHER generation's payload — the V0/V2 envelope's -// rendered string or a V3 `system/message`'s content blocks — with the -// harness token-meter's own no-per-block-overhead rule. +// prompt is priced from a `system/message`'s content blocks (or a rendered +// string, at the legacy-wire boundary) — with the harness token-meter's own +// no-per-block-overhead rule. import assert from 'node:assert/strict' import { describe, test } from 'vitest' diff --git a/tests/shared/version.spec.ts b/tests/shared/version.spec.ts index b4580db..5e9faa5 100644 --- a/tests/shared/version.spec.ts +++ b/tests/shared/version.spec.ts @@ -44,18 +44,19 @@ describe('compareVersions', () => { describe('meetsBaseline', () => { test(`the supported baseline is ${BASELINE_DSH_VERSION}`, () => { - assert.equal(BASELINE_DSH_VERSION, '0.1.5-rc.1') + assert.equal(BASELINE_DSH_VERSION, '0.1.7-rc.2') }) test('baseline and above pass; anything below fails', () => { - assert.equal(meetsBaseline('0.1.5-rc.1'), true, 'the baseline itself') - assert.equal(meetsBaseline('0.1.5-rc.2'), true) - assert.equal(meetsBaseline('0.1.5'), true, 'the final release of the baseline line') - assert.equal(meetsBaseline('0.1.7-rc.2'), true) - assert.equal(meetsBaseline('0.1.7-alpha.1'), true, 'a newer X.Y.Z outranks any channel (the 0.1.7 previews pass)') - assert.equal(meetsBaseline('0.1.3-alpha.2'), false) - assert.equal(meetsBaseline('0.1.5-beta.3'), false, 'beta is below rc at equal X.Y.Z') - assert.equal(meetsBaseline('0.1.5-alpha.1'), false) + assert.equal(meetsBaseline('0.1.7-rc.2'), true, 'the baseline itself') + assert.equal(meetsBaseline('0.1.7-rc.3'), true) + assert.equal(meetsBaseline('0.1.7'), true, 'the final release of the baseline line') + assert.equal(meetsBaseline('0.2.0-rc.2'), true, 'the newest supported line') + assert.equal(meetsBaseline('0.2.0-alpha.1'), true, 'a newer X.Y.Z outranks any channel (the 0.2.0 previews pass)') + assert.equal(meetsBaseline('0.1.7-rc.1'), false, 'an older rc of the baseline line') + assert.equal(meetsBaseline('0.1.7-alpha.1'), false, 'alpha is below rc at equal X.Y.Z') + assert.equal(meetsBaseline('0.1.6'), false, 'a hypothetical final of the dropped cycle') + assert.equal(meetsBaseline('0.1.5-rc.1'), false, 'the previous floor, now gated') assert.equal(meetsBaseline('0.1.2-rc.1'), false) assert.equal(meetsBaseline('0.0.9'), false) })