fix!: port the settings page and shell seam to dsh 0.1.7-rc.2
Release / release (push) Successful in 6s
Release / release (push) Successful in 6s
0.1.7-rc.2 moved four things this bundle was built on, and every one of them
failed quietly rather than loudly, so the plugin looked installed and did
nothing:
* `ctx.settings.installSection(...)` is gone. The call threw into a
`try/catch` that only logged a warning, so the settings namespace was never
registered and `configure` had nowhere to persist. An entry's exported
Config IS the form now; the namespace is the Loader entry id the bundle's
patch declares, and `ctx.settings.configure({ auto: false })` is what claims
that entry's own page.
* `settingsScope` and the `settings.plugin.item` slot are gone. The card
registered into both, so it never rendered — the tab draws the intersection
of served namespaces and registered cards, and that intersection was empty.
It now registers into `plugins.bundle.config` keyed by the bundle's package
name, over the `configForms` service, and uses the framework's own form
chrome (`SettingsFormModel` / `SettingsForm` / `SettingsValueField` /
`SettingsSecretField`) instead of the hand-rolled equivalent. A save is one
revision-fenced write.
* `ctx.shell.run(spec)` is gone. Every action that runs a command threw
`shell.run is not a function`; the seam is now
`await ctx.shell.execute(ctx.shell.resolve(request)).result()`.
* A job's `owner` is a session id, not the agent object. The wrong owner
registered a job no session could collect or cancel, and the producer's
`done` resolved `output` where the registry reads `result`, so a finished
job handed back nothing.
Two conformance gaps went with them: a Config field that is not marked
`volatile()` is invisible to every settings UI — the form projection keeps only
volatile fields, and an entry with no form is dropped from `settings.describe`
entirely — and `embeddingApiKey` was a plain field, so the key rode every
settings read back to the browser. It is `role('secret')` now: the browser is
told whether a key is stored and never receives the key.
Also declares the `@deepseek-ai/dsh-*` peerDependencies the compatibility gate
reads, so an incompatible runtime refuses this plugin at startup instead of
half-loading it.
Verified against a real 0.1.7-rc.2 boot: `settings.describe` serves the entry
with `autoGenerate: false`, the key redacted and reported only as `set: true`,
and no startup diagnostics. The host mock in the tests now has no `run` and no
`installSection`, so those seams are contract-tested rather than assumed, and
the browser tests read the installed harness's own slot catalog and package
exports — a rename fails a test instead of quietly emptying a page.
This commit is contained in:
1 parent
ffefc42e01
commit
1b2ab3613f
8 files changed
+1549
-689
No files matched your search
@@ -17,8 +17,9 @@ are in without remembering flags.
|
||||
| `/slopo-analyze-ignore` | Triages the whole codebase's clusters, writing the obvious false positives into the ignore file. |
|
||||
| `/slopo-analyze-one` | Reports one cluster at a time — optionally by hash — and judges whether it is worth acting on. |
|
||||
| the `slopo` tool | Resolves the CLI, materialises the config, runs the work, and turns slopo's terse failures into something actionable. |
|
||||
| the `dsh-slopo` settings namespace | The embedding backend and per-workspace state, registered on the Host. |
|
||||
| the settings card | A form for all of it, in Settings → Plugins → Plugin configuration. |
|
||||
| the `dsh-slopo` configuration | The embedding backend and per-workspace state. It is the Host plugin's own `Config`, so the deployment's schema and the form can never disagree. |
|
||||
| the settings card | A form for all of it, on the **Plugins** page under the `dsh-slopo` bundle. |
|
||||
|
||||
All three skills are **user-invocable and deliberately not model-invocable**,
|
||||
matching slopo's own agent configs. Typing `/slopo-review` anywhere in a message
|
||||
injects that skill; the model will not reach for one on its own. Every one of
|
||||
@@ -41,23 +42,31 @@ allowlist a build before the first install can succeed.
|
||||
|
||||
## Settings
|
||||
|
||||
The plugin contributes a card to **Settings → Plugins → Plugin configuration**,
|
||||
keyed on the `dsh-slopo` settings namespace the Host registers. Every setting in
|
||||
the table below is editable there, overridden fields are marked, and Reset
|
||||
returns a field to the deployment default rather than to empty.
|
||||
The plugin ships a configuration page on the **Plugins** page — the sidebar
|
||||
entry that lists bundles, rows and official plugins — reached by opening the
|
||||
`dsh-slopo` bundle and pressing **Configure**. Its key is the bundle's package
|
||||
name, which is also the namespace: the Loader entry id the bundle's patch
|
||||
declares.
|
||||
|
||||
The card behaves the way the other plugin cards do: it starts collapsed, its
|
||||
header is a button that discloses the controls in place, the header marks it when
|
||||
it holds unsaved edits — visible even while collapsed, since staged edits outlive
|
||||
being folded away — and a save that lands collapses it again.
|
||||
That the namespace is the entry id is the point. Since dsh 0.1.7-rc.2 the
|
||||
framework projects this plugin's exported `Config` schema onto the entry that
|
||||
composed it, so there is one source of truth for what is configurable. Every
|
||||
field is marked `volatile()`, which is what puts it on the page, and
|
||||
`embeddingApiKey` additionally carries `role('secret')`, so the browser is told
|
||||
whether a key is stored and never receives the key itself.
|
||||
|
||||
Every setting in the table below is editable there; overridden fields are
|
||||
marked, a draft that is not a valid value blocks the save, and Reset returns a
|
||||
field to the deployment default rather than to empty. A save is one
|
||||
revision-fenced write, so a second editor cannot be silently overwritten.
|
||||
|
||||
The card is the bundle's second half — the browser half. It needs no build step
|
||||
and ships as plain ESM in the module-loader wrapper, but the platform snapshots
|
||||
client bundles at activation, so **a client half only appears after the profile
|
||||
restarts.** Without it the namespace still works — it is editable in
|
||||
`$DSH_HOME/settings.yaml` and through the tool's `configure` action — there is
|
||||
simply no form drawn for it, because the tab renders the intersection of served
|
||||
namespaces and registered cards.
|
||||
restarts.** Without it the namespace still works — it is editable in the
|
||||
profile's patch layer and through the tool's `configure` action — there is
|
||||
simply no form drawn for it, because the page only renders a bundle that
|
||||
registers a configuration entry for itself.
|
||||
|
||||
## Configure it
|
||||
|
||||
@@ -84,7 +93,9 @@ Then set it up:
|
||||
}
|
||||
```
|
||||
|
||||
Values land in the `dsh-slopo` section of `$DSH_HOME/settings.yaml`.
|
||||
Values land in the `config` of the `dsh-slopo` row in the profile's patch layer
|
||||
(and, from the browser, in the same row through the settings domain — the card
|
||||
and the tool write the same place).
|
||||
|
||||
| Setting | Meaning |
|
||||
| --- | --- |
|
||||
@@ -228,6 +239,29 @@ Every action indexes first so the gate runs before anything expensive, and
|
||||
|
||||
## Things worth knowing
|
||||
|
||||
**The harness seam moved under this plugin once, and the failure was silent.**
|
||||
dsh 0.1.7-rc.2 replaced three things this bundle used:
|
||||
|
||||
| was | is |
|
||||
| --- | --- |
|
||||
| `ctx.settings.installSection(ctx, ns, Config, config, …)` | the exported `Config` itself, projected onto the Loader entry; `ctx.settings.configure({ auto: false })` claims the entry's own page |
|
||||
| a plugin-chosen settings namespace + a `settings.plugin.item` card | the entry id, plus a `plugins.bundle.config` card keyed by the bundle's package name |
|
||||
| `ctx.shell.run(spec)` | `await ctx.shell.execute(ctx.shell.resolve(request)).result()` |
|
||||
|
||||
All three broke quietly rather than loudly. `installSection` was missing, so the
|
||||
call threw into a `try/catch` that logged a warning nobody reads; the card's slot
|
||||
and the `settingsScope` service it asked for no longer existed, so the card
|
||||
simply never rendered; and `shell.run` threw only on the paths that actually run
|
||||
a command. On top of that, a `Config` field that is not marked `volatile()` is
|
||||
invisible to every settings UI, and the `jobs` registry wants `exec.agent.id`
|
||||
(a session id) as its owner — handing it the agent object registered a job no
|
||||
session could collect.
|
||||
|
||||
Two habits follow. First, this bundle's tests bind to the *installed* harness's
|
||||
own slot catalog and package exports, so a rename fails a test instead of
|
||||
quietly emptying a page. Second, the browser half keeps its slot and service
|
||||
names in exactly one place each, so the next rename is a one-line edit.
|
||||
|
||||
**Five settings are frozen at first index.** `source_dir`, `embedding_model`,
|
||||
`embedding_dimensions` and both node-count thresholds are written into
|
||||
`slopo.db` on the first `index`, and every later run refuses to mix generations:
|
||||
@@ -296,18 +330,38 @@ that silently does nothing. Two real bugs were caught this way: a config
|
||||
renderer that would write the API key to disk if handed one, and `--config-version`
|
||||
placed before the subcommand, which slopo rejects with a usage error.
|
||||
|
||||
The mock's seams are shaped like the harness's, deliberately, because that is
|
||||
what makes this file a contract test. `ctx.shell` exposes `resolve`/`execute`
|
||||
with a handle whose `result()` settles the run — there is no `run(spec)`, which
|
||||
is exactly the method dsh 0.1.7-rc.2 removed. `ctx.settings` has no
|
||||
`installSection` either; it has `configure` and `update`, and the tests assert
|
||||
that the plugin claims its own page and writes to the entry id the bundle patch
|
||||
declares. The `jobs` fake records the spec, so the test can check that a
|
||||
background job is owned by a **session id** and that its `done` resolves a
|
||||
`JobOutcome` carrying `result`.
|
||||
|
||||
The browser half has no browser to run in, so its tests evaluate the bundle the
|
||||
way the real module loader does — inside `window.__ModuleLoader__.load` — with a
|
||||
fake `require` that answers `react` and throws on anything else. That last part is
|
||||
deliberate: it checks the card stays inside the platform's seed table, which is
|
||||
the constraint a hand-written client half is most likely to break. The card is
|
||||
then rendered and its handlers driven, which is not a substitute for looking at
|
||||
it but does cover the wiring it cannot work without.
|
||||
fake `require` that answers `react` and `@deepseek-ai/dsh-client-ui-primitives`
|
||||
and throws on anything else. That last part is deliberate: it checks the card
|
||||
stays inside the platform's seed table, which is the constraint a hand-written
|
||||
client half is most likely to break. The card is then rendered and its handlers
|
||||
driven — including the field specs, run through a copy of the shipped
|
||||
`SettingsFormModel` staging semantics — which is not a substitute for looking at
|
||||
it but does cover the wiring it cannot work without. Three further tests read the
|
||||
*installed* harness instead: that `plugins.bundle.config` is a declared slot and
|
||||
`settings.plugin.item` is gone, that the slot is dispatched by bundle package
|
||||
name, and that every primitive the card requires is still exported. They skip
|
||||
when no global dsh installation is present.
|
||||
|
||||
`node_modules/@deepseek-ai` is a local symlink to the installed harness so the
|
||||
`@deepseek-ai/*` imports resolve during tests. It is gitignored and is not a
|
||||
declared dependency — an out-of-tree bundle that depended on an in-box package
|
||||
would install a second copy that drifts from the host's.
|
||||
declared dependency — `peerDependencies` states the compatibility contract
|
||||
instead, and an out-of-tree bundle that depended on an in-box package would
|
||||
install a second copy that drifts from the host's. The declared peers are what
|
||||
the harness's compatibility gate reads: a plugin whose `@deepseek-ai/dsh-*`
|
||||
ranges do not admit the running version is refused at startup rather than
|
||||
half-loaded.
|
||||
|
||||
## Releases and updating
|
||||
|
||||
@@ -321,14 +375,20 @@ would install a second copy that drifts from the host's.
|
||||
`vX.Y.Z`;
|
||||
4. creates the Gitea release with those notes as the body.
|
||||
|
||||
The bump is a patch unless a commit in the release says otherwise:
|
||||
The bump is read from the commit subjects in the release, so the intent travels
|
||||
with the change instead of living in a file someone has to remember to update:
|
||||
|
||||
| in a commit subject | effect |
|
||||
|---|---|
|
||||
| `[minor]` | release as a minor version |
|
||||
| `[major]` | release as a major version |
|
||||
| `feat`, as its own word (`feat:`, `feat(x):`) | minor — `0.1.0` → `0.2.0` |
|
||||
| `feat!:` / `fix(x)!:` / `BREAKING` | major — `0.1.0` → `1.0.0` |
|
||||
| `fix:`, `docs:`, prose, anything else | patch — `0.1.0` → `0.1.1` |
|
||||
| `[minor]` / `[major]` | force the bump when the subject cannot carry it |
|
||||
| `[skip release]` | this push publishes nothing |
|
||||
|
||||
Only the **subject** line is read, and `feat` has to be a word — `Fix the feature
|
||||
flag leak` is a patch, not a minor.
|
||||
|
||||
With no tags yet, the version already in `package.json` is what gets tagged — the
|
||||
first release should match what the code says, not invent a number.
|
||||
|
||||
@@ -338,8 +398,16 @@ commits carry one; otherwise the notes are a flat list. Prose commit subjects ge
|
||||
no invented headings.
|
||||
|
||||
The logic lives in `.gitea/scripts/release.sh` so it runs without a runner, and
|
||||
`npm run test:release` exercises it — 31 checks, including an empty range, a
|
||||
repository with no tags yet, and a patch number already at 9.
|
||||
`npm run test:release` exercises it — an empty range, a repository with no tags
|
||||
yet, a patch number already at 9, a `feat` that is only a word. `npm run
|
||||
release:plan` says whether the current HEAD would release, and `npm run
|
||||
release:notes` prints the notes it would publish, both without releasing.
|
||||
|
||||
The runner needs nothing but Gitea's own job token (`secrets.GITEA_TOKEN`), which
|
||||
`permissions: contents: write` in the workflow is what unlocks. Two caveats worth
|
||||
knowing before the first push: Actions has to be enabled on the repository, and a
|
||||
runner with the `ubuntu-latest` label has to be registered against the instance —
|
||||
otherwise the run sits queued forever.
|
||||
|
||||
### Making `dsh plugin update` work
|
||||
|
||||
|
||||
Reference in new issue
Block a user