Two things a user setting this up hit immediately.
The card drew `embeddingApiKey` twice. The render loop walked every entry in
FIELDS, and the secret — which is drawn by hand, as a write-only password
control — was one of them, so a second control appeared beside it: an ordinary
text input, echoing the key in clear as you typed. One key, two boxes, one of
them visible. The loop skips secrets now, and the tests count controls rather
than checking that each expected label is present, because a duplicate beside
the real control passed the old assertions.
`status` could not answer the one question a new user cannot look up: how wide
is this model? The probe that reports it was gated on every required setting
being present, so in exactly the state that needs the answer — dimensions
unset — it reported nothing and said "Missing: embedding_dimensions". It now
runs whenever the endpoint can be named (a model and a base URL), and the
status message names the width it observed:
Missing: embedding_dimensions. The endpoint is reachable and returned 768
dimensions, so set embeddingDimensions to 768.
The probe also tells "not set yet" apart from "set wrong", because the two have
different remedies.
Also documents both ways to read the width by hand, since a wrong value is
frozen into the index at first use and costs a full re-embed to correct.
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.
It rendered as a flat, always-open block with no header. The framework's card
chrome is a disclosure: the tab renders every card into a <ul>, each card's root
is an <li>, its header is its own <button> that discloses the controls in place,
it starts collapsed, the header marks it when it holds unsaved edits, and a save
that lands folds it back up.
Three things were wrong and only one of them was cosmetic:
* the root was a <div> inside the tab's <ul>, so the list and card styling
never reached it
* there was no header button, so no disclosure and no aria-expanded
* nothing marked unsaved edits, and nothing collapsed after a save
`PluginCard` is not importable — it lives in that package's client bundle, and
the client purity gate forbids cross-plugin value imports — so the behaviour is
reproduced rather than borrowed, with inline styles for the same reason.
The collapse-on-save originally lived in an effect watching a `saving` flag, which
is how the shipped card does it because its form state comes from a controller.
This card awaits its own writes, so it already knows the save landed; collapsing
directly in the success path is simpler and is what the tests can actually reach.
A failed save still keeps the card open with its drafts.
Tests cover the disclosure itself: the root element, collapsed-by-default, the
header's aria-expanded and label, that staged edits survive collapsing, and that
a landed save folds the card up.
The settings namespace was registered but nothing drew a form for it, because
the Plugin configuration tab renders the INTERSECTION of the namespaces the Host
serves and the cards a browser half claims. A served namespace with no card
renders nothing at all. This is the missing card.
Plain ESM in the module-loader wrapper, no build step, requiring only `react` —
which is in the platform seed table. Anything outside that table would have to be
bundled into this file or declared in `dsh.client.external`, and the tests fail
loudly on any other specifier.
It stages edits and writes them on Save; a cleared field is an `unset` rather
than a write of "", so the value re-inherits the composition layer instead of
becoming empty, and the user layer's presence is what marks a field overridden.
Reset clears every override. A namespace this client cannot reach renders
nothing, so a deployment without the Host half shows no trace.
package.json gains an `exports` map (the loader reads `exports["./client"]`; a
bare `main` would not be enough) and the `dsh.client` declaration.