# dsh-effort-dial English | [中文](README.md) A reasoning-effort dial for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) composer. It takes over the composer's model seat and puts an animated effort control in it, writing the chosen level back through the session's model selection — the same contract the host's own seat uses. ![The field renderer at six moments: a mid level, the top level at rest, and the landing burst easing back down](docs/preview.png) *The rows above are the renderer's own output, drawn against a mock canvas by `scripts/render-burst.mjs` — not a browser screenshot. They show a mid level, the top level at rest, then +200 ms, +700 ms, +1400 ms and +2200 ms after landing on the top level.* ## What you get - **An effort dial** — the levels the current model advertises become stops on a track. The thumb is a plain white circle; everything behind it is drawn on a canvas. - **Level is the field's cue** — a low level is a slow, sparse drift; the top level is dense, fast, and adds sustained shockwave rings, arc bolts and a swollen core. - **A landing burst** — arriving at the top level spikes the field (about 8x its resting brightness at the peak) and then eases back down over roughly two seconds. A wash of light sweeps outward from the thumb across the whole rail as it fires. - **Magnetic anchors** — passing a level holds the thumb back slightly and releases it in between, so a stop is felt. The lag is bounded: the thumb travels at 0.5x–1.5x of the finger and can neither stall nor bolt. - **Eased travel** — taps, presses and keyboard steps glide to the target along a cubic Bézier. Dragging tracks the finger directly, with any leftover glide offset dissolving underneath it. - **The model list keeps the host's look** — the picker inside the popup is rendered plainly with host design tokens. This plugin owns the dial, not the host's list. ## Install Straight from this repository: ```sh dsh plugin --profile web add github:WONGIII/dsh-effort-dial ``` The built `lib/` is committed, and the package declares **no `prepare` script**, so nothing has to be compiled at install time and pnpm has no build to allow. If you would rather not install from git, `dsh plugin --profile web add ./dsh-effort-dial` works the same way from a checkout. Then restart DSH (or reload the page). ### One ordering caveat `conversation.input.model` is a **`single` slot**: exactly one entry renders there, and the plugin that registers *last* wins it. The host ships its own `ModelSelect` into that seat. Installed as a bundle, this plugin composes before the profile's own `cordis.patch.yml`, so the shipped seat may register after it. The dial handles that itself: it checks the slot's `entries()` listing and re-registers until the seat is its own, bounded to the first few seconds of start-up. **Mount it exactly once — as the bundle.** `dsh plugin add` writes both the dependency and the `dsh.profile.bundles` entry, and that is all it needs. Do **not** also add an `insert` row for it to the profile's `cordis.patch.yml`: ```yaml # ~/.dsh/profiles/web/cordis.patch.yml — do NOT add this - insert: - id: effort-dial name: dsh-effort-dial ``` Two rows with the same `id` compose as two **candidates** for a single entry, and the Plugins page then refuses to address the row — [`dsh-plugin-manager`](https://github.com/deepseek-ai/deepseek-harness) decides addressability with `candidates.length > 1` and reports: ``` readOnlyReason: "unaddressable" ``` which locks that row's on/off switch and stops the toggle state from ever reaching disk. The visible symptom is that the plugin cannot be turned off from the UI at all — the switch flips back and nothing is written. ## How the field works The effect is a **train of emitted fronts**, not a scrolling texture. The thumb continuously emits fronts. A front is a moving pulse with its own position, width and strength: born at the thumb, travelling outward, dying when it runs off the end of the rail. A column lights up as fronts arrive at it and fades as they pass, so no band is a standing wave that happens to scroll — every bright stripe is a front emitted earlier that has moved further out. That is what makes the field read as being pushed out of the source. A front's profile is asymmetric: it rises fast on arrival and trails off slowly behind. The hard leading edge gives the sense of travel, and the long tail fills the space between fronts so the rail stays covered instead of looking striped. ## Notes for other plugin authors Three things here cost real time to find. They are written down because none of them produce an error that names the actual problem. **Declare every service your callbacks touch.** Service access is gated by a plugin's `inject` list, and the gate applies to *transitive* reads. A seat whose `inject()` callback calls the model directory needs `remote.session` declared, because resolving a session address reads it. Without the declaration the callback throws at render time only — the plugin looks mounted, and the seat dies inside the slot's error boundary: ``` Error: cannot get property "remote.session" without inject slot entry crashed in 'conversation.input.model' ``` **A `single` slot ignores the priority you pass.** For any slot that is not a `chain`, the runtime overwrites `options.priority` with its own decreasing allocation, so `priority: -100` is inert and **registration order alone decides** the winner. `slots.entries(key)[0]` is the entry that renders. **A verification script must not rewrite the artifact it checks.** Any write to the served bundle fires the client-HMR watcher, which makes the running page reload and re-apply the plugin. A check that rebuilt a byte-identical file still disturbed the thing it was checking — which looks exactly like the plugin randomly disappearing. `build.mjs --check` compares against an in-memory build instead. ## Development ```sh node scripts/build.mjs # src/ -> lib/client.js node scripts/build.mjs --check # prove the bundle is current, without writing it npm run verify # all checks ``` `lib/client.js` is generated. Edit `src/` and rebuild; never edit the bundle. | Path | Role | |---|---| | `src/client.js` | Cordis plugin: the seat, the popup, the plain model list | | `src/fx.js` | `FieldRenderer` — the whole animation, one instance per dial | | `src/styles.css` | Dial, popup and list styles | | `cordis.patch.yml` | The bundle layer: one `insert` row | | `lib/index.js` | Host half: a mountable entry point with no host behaviour | | `lib/client.js` | Built browser half | | `scripts/mock-canvas.mjs` | Premultiplied 2-D canvas mock the checks run against | | `scripts/verify-*.mjs` | Load, geometry, motion, light, and rendering checks | | `scripts/check-original.mjs` | Clean-room check (see below) | | `scripts/render-*.mjs` | Render the field to PNG outside a browser | ## Verification The effect is verified by measurement rather than by eye, on a mock canvas that models premultiplied alpha the way the real one composites: - **Load** — the built bundle registers a module row and mounts against stubs. - **Geometry** — the thumb is a circle, proud of the rail, its inset is derived rather than written out, and its glow's visible edge falls outside the disc. - **Sweep** — the landing wash covers nothing at the moment of landing, only the near half half-way through, the whole rail at the end, and never retreats. - **Field** — the bottom edge is painted at five rail heights including non-multiples of the cell size, and every band of the rail keeps moving. - **Light** — the level ladder is monotonic, and a frame stays inside its time budget. `scripts/check-original.mjs` is a clean-room check. The field model here was written after reading a reference implementation, and an early draft of it was far too close to that reference; the script looks for that reference's distinctive expressions in `src/fx.js` and fails if any survive. It is an optional check — it reports that it was skipped when the reference is not installed. ## Requirements - DeepSeek Harness with the web profile (the plugin targets `conversation.input.model`). - Node 18 or newer to build from source. Installing from git needs no build. ## License MIT — see [LICENSE](LICENSE).