# dsh-prompt-optimizer [δΈ­ζ–‡](README.md) | **English** | 🐣 [Beginner guide (δΈ­ζ–‡)](README.simple.md) [![CI](https://github.com/Y1X1n/dsh-prompt-optimizer/actions/workflows/ci.yml/badge.svg)](https://github.com/Y1X1n/dsh-prompt-optimizer/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/@y1x1n/dsh-prompt-optimizer?label=npm&color=cb3837)](https://www.npmjs.com/package/@y1x1n/dsh-prompt-optimizer) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Release](https://img.shields.io/github/v/release/Y1X1n/dsh-prompt-optimizer?label=release)](https://github.com/Y1X1n/dsh-prompt-optimizer/releases/latest) [![Stars](https://img.shields.io/github/stars/Y1X1n/dsh-prompt-optimizer?logo=github)](https://github.com/Y1X1n/dsh-prompt-optimizer/stargazers) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) A DeepSeek Harness plugin that adds an **Optimize** button (✨) next to the composer, one click analyzes and rewrites the prompt draft in your input box, with results **streamed section by section over SSE**. The optimization call reuses the current session's model route by default (read live on every click β€” switching models in the session takes effect immediately). - **Host side**: registers two routes, `POST /dsh-prompt-optimizer/optimize` (SSE streaming) and `POST /dsh-prompt-optimizer/test-model` (connectivity probe), and drives `ctx.llm` for the "analyze + rewrite" call. - **Client side**: injects the button into the `conversation.input.right` slot, the result panel into `conversation.input.dock` (a full-width row above the input card, same family as TodoDock β€” renders on the new-session screen too and never covers the input box), and a collapsible settings card into `settings.plugin.item`. UI copy follows the DSH interface language (δΈ­ζ–‡ / English). ## Contents - [Features](#features) - [Installation](#installation) - [From npm (recommended, simplest)](#from-npm-recommended-simplest) - [From a Release (tarball, no build)](#from-a-release-tarball-no-build) - [From source (tarball)](#from-source-tarball) - [From GitHub](#from-github) - [Uninstall](#uninstall) - [Compatibility](#compatibility) - [FAQ](#faq) - [Verification status](#verification-status) - [How it works](#how-it-works) - [Development](#development) - [Security notes](#security-notes) - [Contributors](#contributors) - [License](#license) > 🐣 **First time using dsh plugins?** See the [beginner guide](README.simple.md) (in Chinese): three steps to install, one click to use. ![The result panel: five-dimension diagnosis and the rewrite streamed live, with replace/undo/copy; the badge shows the actual route and duration](docs/screenshots/optimize-panel.png) | | | |---|---| | ![Composer idle state: the ✨ Optimize button next to the model picker, disabled while empty](docs/screenshots/composer-idle.png) | ![Panel error state: upstream model errors surface in full, with one-click retry](docs/screenshots/panel-error.png) | | ![Settings card collapsed: the header shows a "model Β· mode" summary](docs/screenshots/settings-collapsed.png) | ![Settings card expanded: model / generation parameters / context groups](docs/screenshots/settings-expanded.png) | ## Features - An **Optimize** button on the right of the composer tool row (disabled while empty, breathing animation while working). During the wait the panel shows the live stage (waiting for model / analyzing / writing the rewrite) with an elapsed-seconds readout; on completion the model badge shows the total duration. - Reads the current session's provider/model on every click; **analysis and rewrite stream onto the panel** above the input card β€” no staring at a spinner for the whole generation. - **Dual optimization strategies**: with no conversation context (fresh session, or context disabled) it rewrites with a **structured template** (role / task / constraints / output format); with context it switches to **distill-intent + polish** β€” reads the recent conversation, keeps the draft's original framing instead of forcing a template, never re-asks what the context already answers, and never resurfaces **directions you already rejected** (the do-not-enter set). - **Fidelity discipline** (inspired by Fishsb/dsh-prompt-enhancer): semantic-equivalence floor, traceability (inferences are marked "unless otherwise specified / by default"), a pre-output element-by-element **fidelity self-check** against drift, with the self-check explicitly outranking the length discipline (target under 800 characters for simple tasks β€” a missing element is worse than verbosity), and few-shot examples to stabilize output style. - **Lightweight memory chain**: edit our optimized text and click Optimize again, and the previous round's result rides along as a continuity reference (accepted decisions carry over, only your changes are worked on). Same-text retries, cross-session calls, and degraded (non-wellFormed) results never ride (degradation is only judged in full mode β€” fast mode always counts as compliant); the chain resets the moment you send or close the panel, and it follows the "include context" switch. - **Slash-command safe**: input like `/goal help me …` optimizes only the body; the command prefix is re-attached on replace β€” the command word is never rewritten. A bare command with no body (just `/goal`) is rejected up front with a notice instead of being "optimized" into garbage. - **Context aware**: by default carries the session's recent conversation as reference (**user messages are guaranteed a floor** β€” agentic sessions produce far more assistant step fragments than user input, and naive recency sampling would crowd out what you actually asked), capped at 8 turns / 1600 chars; the meta-prompt explicitly forbids answering or continuing the context. Empty sessions fall back to the template strategy. Can be disabled in the settings card. - Result panel: **five-dimension diagnosis** (goal clarity / context / constraints / structure / output spec) + the **full optimized prompt**; actions: **replace the input box** (with **undo**, which auto-invalidates once you edit the replaced text) / copy / re-optimize / close. **The panel closes itself after you send the message or clear the input.** - **Cancel keeps the content**: press Esc while optimizing and the panel freezes in a cancelled state showing everything generated so far β€” review it, hit "re-optimize" to continue, or close. In any other state Esc just closes the panel. A client-side **timeout watchdog** (settings timeout + 5s margin) turns a hung Host or black-holed network into an explicit timeout error instead of an endless spinner. - Panels are session-scoped: switching sessions never leaks an old result into another session's view or input box. - **Auto-follow scrolling while streaming**: the live view sticks to the bottom as content grows, pauses when you scroll up, and resumes when you return near the bottom. - All panel styling uses official Harness design tokens (`--dsw-alias-*`); follows light/dark theme automatically. - Long-draft friendly: the output cap rises with estimated input length by default (full mode Γ—2 / fast mode Γ—1.5, capped at 32768); on real truncation the partial result is still shown with a clear notice instead of disappearing. - Format tolerance: marker variants (e.g. `<<< ANALYSIS >>>`) parse correctly; in full mode, outright non-compliant output degrades gracefully with a warning; in fast mode the result is a single section, so it always counts as compliant whether or not the model emits markers (no warning, memory chain unaffected) β€” friendly to third-party/free models that skip the marker format β€” their raw stream is previewed live, character by character, until a marker appears. - Settings β†’ Plugins β†’ "Prompt Optimizer" card (collapsed by default, click the header to expand; the collapsed header shows a "model Β· mode" summary; every change can be reverted in one step via "Undo last change" at the bottom β€” an in-memory stack of the last 20): - **Model**: optimization model (**follow the current session** (default), or pin one from the model catalog, provider/model dropdown with manual refresh) / fallback model (automatic failover when the primary route fails before producing anything β€” the panel badge shows "Β· fallback", hover to see why) / connectivity ("Test connection", 32-token / 20s capped probe showing the actual route and latency) - **Generation parameters**: output language (δΈ­ζ–‡ / English); mode: full (analysis + rewrite) / **fast** (rewrite only, roughly half the output tokens); reasoning effort: clamp to lowest tier (default β€” dramatically shortens the pre-first-token stall on reasoning models) / follow session; max output tokens (default 8192), timeout (default 120s), temperature (default 0.2, hover for the rationale); auto output-cap toggle (default on) - **Context**: include-context toggle (default on): when off, only the draft itself is read β€” no session history ## Installation Prerequisite: the `dsh` CLI (an environment where `npx @deepseek-ai/dsh web` works). ### From npm (recommended, simplest) ```sh dsh plugin --profile web add @y1x1n/dsh-prompt-optimizer ``` ### From a Release (tarball, no build) Download `y1x1n-dsh-prompt-optimizer-.tgz` from the [Releases page](https://github.com/Y1X1n/dsh-prompt-optimizer/releases/latest), then install the local file: ```sh dsh plugin --profile web add ./y1x1n-dsh-prompt-optimizer-.tgz ``` ### From source (tarball) ```sh cd dsh-prompt-optimizer npm install --legacy-peer-deps # the prepare hook builds lib/ automatically npm pack # produces y1x1n-dsh-prompt-optimizer-.tgz dsh plugin --profile web add ./y1x1n-dsh-prompt-optimizer-.tgz ``` Then (re)start `dsh web` and open the Web UI β€” the button appears next to the composer. > **Windows note**: `dsh plugin add ./directory` goes through pnpm `link:`, which currently misparses the drive-letter colon as a protocol separator and produces a broken symlink (`node_modules/` points nowhere), so the plugin never loads. Use the tarball form for local installs; the directory-link form works on macOS/Linux. ### From GitHub ```sh dsh plugin --profile web add github:Y1X1n/dsh-prompt-optimizer ``` Git installs pull the source; this package self-builds at install time via its `prepare` script (only Node needed, no monorepo). pnpm β‰₯10 refuses to run build scripts on first install β€” add the package name to `allowBuilds` in that profile's `pnpm-workspace.yaml` as the terminal prompt suggests, then retry. Pinning a commit is recommended: `github:Y1X1n/dsh-prompt-optimizer#`. ### Uninstall ```sh dsh plugin --profile web remove @y1x1n/dsh-prompt-optimizer ``` ## Compatibility > **This version (v0.3.17) targets DeepSeek Harness**: > > | dsh version | Optimize route / model calls | Settings page (incl. GitHub link) | Composer button / result panel | > |---|---|---|---| > | **0.1.5-rc.2** | βœ… verified | βœ… verified | βœ… verified | > | **0.1.5-rc.1** | βœ… verified | βœ… verified | βœ… verified | > | **0.1.2-rc.1 / 0.1.2-alpha.5** | βœ… verified | βœ… verified | βœ… verified | > | **0.1.1-rc.2 and below** (0.1.0-rc.7+) | βœ… verified | βœ… verified | βœ… verified | > > One build covers 0.1.0-rc.7 through 0.1.5; this annotation is maintained in every Release note from v0.3.15 onward, and earlier Release notes have been retroactively annotated. - Development baseline: `@deepseek-ai/*` **0.1.5-rc.1** (pinned by `sync:types` from v0.3.17); verified on **0.1.0-rc.8** (2026-08-20, Windows, real-profile install + Web routes / client bundle / session-history RPC / end-to-end LLM call), re-verified on **0.1.1-rc.2** (2026-08-27), and adapted + verified on **0.1.2-rc.1 / 0.1.2-alpha.5** (2026-09-05/06), on **0.1.5-rc.1** (2026-09-10, real-profile end-to-end smoke tests: composition layer, route SSE, client bundle, composer button and result panel, settings card), and on **0.1.5-rc.2** (2026-09-11, full-chain re-verification: composition-layer injection, Host load, route contract 405/400/200, end-to-end SSE with 155 delta frames, all four strategy paths, client bundle slots and settings-card API β€” all passing, 69 tests green). - Verified on **macOS** via automated tests (2026-09-02, macOS 26.5 (Darwin 25.5.0), Node.js v26.0.0; after `npm install --legacy-peer-deps`, `sync:types` / `typecheck` / `build` / `npm test` all pass β€” 62 tests green); CI now runs typecheck and the full suite on both ubuntu-latest and macos-latest (@ruijiaang-lab, #3). - **dsh-settings API compatibility layer**: the 0.1.2 line rewrote the settings API (standalone `installSettingsSection` removed, replaced by the `installSection` method on the `ctx.settings` service instance). 0.3.16+ feature-detects at runtime: uses the new interface when present, otherwise an inline equivalent (register + watch + unload fallback); when the settings service is absent it falls back to the composition-layer config. - **Client type surface migrated across 0.1.2/0.1.5** (adapted in v0.3.17): the `dsh-client-runtime` package is gone upstream, so `ClientContext` is simply cordis' `Context`; `SettingsScope` moved to `@deepseek-ai/dsh-client-ui-settings/client` (its `SettingsScopeBinder` now owns the `ctx.settingsScope` Context merge β€” re-declaring it would conflict); the `ctx.slots` merge is provided by `@deepseek-ai/dsh-client-ui-renderer/client`. `HistoryEntry` / `ModelProviderGroup` moved into the unpublished `dsh-api-session-controller`, so the plugin now ships its own minimal structural faces in `src/client/host-faces.ts` instead of chasing upstream types. - **Session model & catalog (real since v0.3.17)**: the model catalog goes through the connection-level generic RPC `session/modelCatalog` (the `/api` channel; the payload envelope requires exactly one plain-object `args` field β€” `{ args: {} }` for a zero-parameter method); "follow the current session" reads the official per-session model directory service `ctx.modelDirectories` (provided by `dsh-client-ui-model-selection`, the same directory the /model popup uses, whose `current` is the model the session will actually use). That service is captured through a **separate optional inject** β€” folding it into the main inject would leave clients waiting forever on hosts without it; when absent the plugin falls back to Host route resolution. - **Session history (context) remains a gap**: the legacy `connection.api.sessions.history` face was never declared in any published release (0.1.0-rc.7 β†’ 0.1.5) and is absent from the browser runtime; the plugin calls it through an optional structural face and optimizes without context when absent. The real path needs the `session/follow` / `session/page` streams (including `throughSeq` from the follow opening frame) β€” not wired yet. - **Client slot props dual form**: since 0.1.2 the composer slots are session-scoped β€” components read session/draft state via standard hooks (`useInput`/`useSession`), and registration uses the official two-layer shape (register inside an inject callback on the derived context, with an `inject(sessionId)` hook). Components dispatch on the props shape (hooks on 0.1.2, direct snapshots on older hosts), so both hosts share one build. - **Client bundle registration id**: the loader id in `lib/client.js` must equal the plugin npm package name (the host's client-modules validates registration against it); the client inject list no longer names `dsh-client-runtime`, which 0.1.2 merged away. From 0.1.5 the bundle is served through a combined `/plugins/??/client.js,…&rev=` URL β€” bare per-plugin paths are no longer exposed (the boot manifest's combined URL is authoritative). - Protocol contract: `/dsh-prompt-optimizer/optimize` returns 400/405/409/413 as plain JSON on pre-check failure and switches to an SSE stream on success (model errors arrive as `error` events); `/dsh-prompt-optimizer/test-model` **always returns HTTP 200** with an `ok` field in the body (a probe is application-level semantics, deliberately not mapped to transport status codes) β€” integrate against `ok`, not the status code. - The HTTP carrier service name has drifted between releases (`httpServer` in the npm 0.0.1-rc.x type packages, `webServer` in the 0.1.0-rc.x runtime): the plugin waits on both names via `ctx.inject` with no static hard dependency β€” if the name changes again, only this plugin's routes fail to register (with a log warning after 10s); the Harness startup is never dragged down. - Client and Host must be the same version (the SSE protocol is a private contract): after upgrading, restart `dsh web` and refresh the browser. ## FAQ - **Clicked "Optimize" and nothing happened?** Open the browser console and look for logs starting with `[dsh-prompt-optimizer]`; common causes are no model configured (set up a provider under Settings β†’ Models first), or the upstream slot the panel needs is not ready yet (refresh the page). - **"No usable model found"?** The session has no routable model selected and no model is pinned in the settings card; fix either one. You can also expand the settings card and hit "Test connection" to confirm the route works. - **The settings card says the model catalog failed to load?** Since v0.3.17 the catalog goes through the host's `session/modelCatalog` RPC and should list every provider and model; if it still fails, the notice includes the concrete reason (hit "Refresh" to retry). Note that "follow the session" depends on the host's per-session model directory service (`ctx.modelDirectories`); on host forms without it, optimization falls back to the Host's first available route (a matching Console warning appears). - **Result truncated?** The panel shows a truncation notice; the auto output cap (on by default) already rises with draft length β€” if it is still not enough, raise "Max output tokens" in the settings card. - **Session model change not taking effect?** Every click re-queries the session's current model; if it still looks wrong, check the console for a `session model query failed` warning (the first available route is used as a fallback then). Note: a model pinned in the settings card overrides the session selection. - **Occasional timeout / RATE_LIMIT failures on optimization?** Transient-error retries are handled by the host at the **provider layer** (dsh 0.1.1+ provider config ships a retry policy covering `RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT / EMPTY_RESPONSE` by default). Tune retry count and backoff under Settings β†’ Models β†’ the provider in question β€” not the plugin's "timeout" setting, which only bounds a single call's total duration. - **Can't find the card on the settings page?** 0.3.0's strict enum schema could clash with older settings documents and stop the card from rendering; fixed in 0.3.1 (unknown enum values fall back to defaults). Make sure you are on β‰₯0.3.1 and refresh. - **Button still there after uninstall?** Plugin-set changes only take effect after restarting `dsh web`; a page refresh is not enough. ## Verification status Verified in a real environment (dsh 0.1.0-rc.8 tested + 0.1.1-rc.2 re-verified, Windows β€” see Compatibility); macOS automated verification passed (2026-09-02, macOS 26.5, all 62 tests green, and CI now permanently runs the full suite on macos-latest); **v0.3.16 completed end-to-end verification on the 0.1.2 line** (dsh 0.1.2-rc.1 / 0.1.2-alpha.5: composer button, result panel, settings card, optimize route and SSE); **v0.3.17 completed end-to-end verification on 0.1.5-rc.1** (2026-09-10, Windows, real profile + browser-driven: the composerγ€ŒOptimize」button enables with input, the panel opens on click and passes the upstream error through SSE, and the settings card shows both the collapsed summary and the full controls), **and a full-chain re-verification on 0.1.5-rc.2** (2026-09-11: composition-layer injection, Host load, route contract, end-to-end SSE, all four strategy paths, client bundle slots and settings-card API β€” every item passing); v0.3.9–0.3.11 were additionally verified end-to-end on a **third-party free model** (openrouter `minimax/minimax-m3:free`, which emits no marker format): both strategies, element-by-element fidelity, `[TODO]` markers, memory chain, and cancel-keeps-content all behave as designed β€” and this live testing is what surfaced the fast-mode false-positive fixed in v0.3.10/11. - Composition-layer load: `--dump-config` shows the `# == @y1x1n/dsh-prompt-optimizer` layer; - Host: startup log `[dsh-prompt-optimizer] loaded`; both routes behave correctly across their 400/405/409/413 paths; SSE streaming verified live; - Client: the bundle is picked up by client-modules and listed in the page boot manifest (from 0.1.5 served through the combined `/plugins/??&rev=` URL), with the plugin row present in the module list; - End to end: a real `ctx.llm` call (DeepSeek route) completes "analysis + rewrite" with correct marker parsing (`wellFormed: true`). - Automated tests (`npm test`, 62 cases): - `test/smoke.mjs`: 28 Host smoke cases (real cordis Context + mocked services: route-resolution priority, empty/malformed config, 400/405/409/413, SSE event flow, max-tokens truncation, timeout, fast mode, reasoning clamp, legacy-settings normalization, connectivity test, fallback chain with fallback-reason passthrough, tool-calls guard, auto output cap, context injection and hard switch, strategy selection, memory-chain injection and truncation, origin fence); - `test/prompt.test.mjs`: 13 meta-prompt parsing cases (marker whitespace variants, degradation paths, partial-stream parsing, stream-buffer compaction, token estimation, context payload and budgeting, strategy fork, fidelity rules and examples, memory-chain payload); - `test/controller.test.mjs`: 21 client pure-logic cases (SSE frame parsing, frame coalescing, interrupted connections, session-query skipping, undo flow, retry, close-abort, cancel keeping partial output plus its state gate, client timeout watchdog, history extraction filtering and degradation, slash-prefix splitting and bare-command rejection, memory-chain passing and gates, send-to-close decisions, duration recording, user-first context sampling). ## How it works ``` Click "Optimize" β†’ Client reads the input draft + the session's model selection (session.models RPC, queried live on every click; skipped when a model is pinned in settings β€” the Host's pinned value wins) β†’ POST /dsh-prompt-optimizer/optimize { text, provider, model, reasoningEffort, context?, previous? } β†’ Host calls ctx.llm.stream() with the system meta-prompt (route resolution: pinned setting β†’ session selection β†’ first available route; an optional fallback route fails over on zero-output failure; the output cap rises with estimated input tokens by default; optional reasoning clamp / low temperature) β†’ text-delta chunks are pushed over SSE and the panel shows "analysis / rewrite" sections live (incrementally parsed on <<>> / <<>> markers, whitespace variants tolerated) β†’ the done event carries the final parsed result; a max-tokens finish carries a truncated flag; a fallback adds its reason β†’ one click replaces the input box (undoable, slash prefix re-attached) / copy / re-optimize (edits continue from the previous result; same text regenerates fresh) β†’ Esc while optimizing = cancel (a cancelled state keeps what was generated); a client-side watchdog guards against an unresponsive Host ``` ## Development ```sh npm install --legacy-peer-deps # install deps and trigger the build npm run sync:types # sync the client type packages (see below) npm run typecheck # tsc --noEmit npm run build # produces lib/{index,client,prompt,controller}.js npm test # smoke + prompt + controller suites ``` ### About `sync:types` The upstream monorepo publishes only part of the `@deepseek-ai/*` packages (the rest are `publishConfig: restricted`), so the client type packages' transitive dependencies cannot be installed from npm. `scripts/sync-types.mjs` handles it: 1. Published packages are `npm pack`ed and extracted straight into `node_modules` (bypassing npm's dependency-tree resolution); 2. For unpublished packages (e.g. `dsh-type-meta`), it scans every `.d.ts` reference and generates minimal stub packages (only module resolvability matters under `skipLibCheck`). These packages only participate in type checking; at runtime everything is provided by the Harness page/process (`react` and `@deepseek-ai/*` are external). ### Directory layout ``` dsh-prompt-optimizer/ β”œβ”€β”€ package.json # dsh.bundle + dsh.client dual manifest β”œβ”€β”€ cordis.patch.yml # composition layer: inserts the Host plugin line β”œβ”€β”€ src/ β”‚ β”œβ”€β”€ index.ts # Host plugin: settings namespace + two HTTP routes + llm call (with fallback chain) β”‚ β”œβ”€β”€ prompt.ts # meta-prompts, marker parsing, token estimation (pure functions) β”‚ └── client/ β”‚ β”œβ”€β”€ index.tsx # Client entry: slot registrations β”‚ β”œβ”€β”€ controller.ts # shared state machine for button/panel + SSE consumption (standalone artifact, unit-testable) β”‚ β”œβ”€β”€ i18n.ts # zh/en UI copy, follows the DSH interface language β”‚ β”œβ”€β”€ OptimizeButton.tsx # composer button β”‚ β”œβ”€β”€ ResultDock.tsx # result panel above the input card (live streaming + undo) β”‚ β”œβ”€β”€ SettingsCard.tsx # collapsible settings card β”‚ β”œβ”€β”€ SparkleIcon.tsx # hand-drawn ✨ icon β”‚ └── GitHubIcon.tsx # GitHub icon (repo link in the settings card header) β”œβ”€β”€ scripts/build.mjs # esbuild: Host ESM + Client lazy-CJS factory + two test artifacts β”œβ”€β”€ scripts/sync-types.mjs β”œβ”€β”€ scripts/prompt-probe.mjs # prompt evidence probe: fires real optimize requests at a running instance (PROBE_ROUTE=provider/model to pin a route) └── test/ # smoke.mjs (Host) / prompt.test.mjs / controller.test.mjs ``` ## Security notes - The HTTP routes are registered on dsh's own web server, which listens on `127.0.0.1` by default. If you expose dsh to your LAN (`0.0.0.0`), this plugin's optimize endpoint becomes callable from the LAN too β€” it consumes your configured model quota; be aware. - Both routes enforce an **origin fence**: requests carrying an `Origin` header (browser POSTs always do) must match `Host`, and `Host` must be a loopback address or the machine's actual local address β€” cross-site forged requests (CSRF) and DNS rebinding get a 403; command-line calls without `Origin` are unaffected. - The plugin holds no API keys: every model call goes through the Harness-configured `ctx.llm` routes. ## Contributors Thanks to the following contributors for improvements to this project (see the [contributing guide](CONTRIBUTING.md)): - [@ruijiaang-lab](https://github.com/ruijiaang-lab) β€” contributing guide ([#2](https://github.com/Y1X1n/dsh-prompt-optimizer/pull/2)) ## License MIT