Skip to content
dsh.fish
Bundle

dsh-input-optimizer

DeepSeek Harness plugin: a four-pointed-star button left of the composer model selector that rewrites your draft before you send it — fixing grammar, ambiguity and wrong execution order — plus a dedicated settings page.

Source
Xxsttb
License
MIT
Updated
Updated 8 days ago

Readme

# dsh-input-optimizer

A **DeepSeek Harness** plugin that does four things:

1. **Rewrites your draft.** A four-pointed-star button sits left of the
   composer's model selector. Press it, and the draft you have typed — but not
   yet sent — is rewritten before it goes out: grammar and wording fixed, and
   ordering problems that would quietly weaken the result restructured.
2. **Edits a message you already sent.** Every user message carries a pencil.
   It swaps the composer for an editing area without touching your draft, and
   sending cuts a new branch from just before that message.
3. **Asks a question again with no memory.** Every model reply carries a circular
   restart arrow: it re-answers that one message in a brand-new conversation that
   carries none of the earlier turns.
4. **Draws the conversation's branching history** as a memory tree in the right
   sidebar, in two modes. The same `‹ N/M ›` arrows switch between every answer a
   question has received, however each one was produced.

```text
[ star ] [ model selector ] [ send ]
   ^
   the button lives here
```

The rewrite is not a summarizer and not an answer generator. It never replies to
your request and never adds facts. It hands back the *same* request, written
better.

Everything is configurable on the plugin's own settings page: the rewrite
instruction, whether to follow the conversation's model, the provider and model
to pin instead, how hard the model thinks, and which shortcut sends an edit.

> **Note on authorship.** The code, the comments, the icon, the locale files and
> this document were all generated by an AI assistant, at a user's request. See
> [AI-generated](#ai-generated).

---

## What it does

Press the star beside the model selector. The Host half sends your draft to the
model the current conversation has selected — at **low** reasoning effort by
default — and puts the rewritten text back into the composer, so you can read it
before you send.

The rewrite targets five goals, in priority order:

| # | Goal | What it means in practice |
| - | ---- | ------------------------- |
| 1 | **Preserve intent** | Adds nothing you did not write. No answers, no opinions, no examples, no preamble. |
| 2 | **Fix language** | Grammar, spelling, punctuation, wrong word choice, ambiguous pronouns, broken agreement, machine-translated phrasing. Your language and register are kept. |
| 3 | **Fix execution order** | Steps written out of order, prerequisites after the action needing them, a goal buried under background, constraints split from what they constrain, two unrelated asks tangled in one sentence. |
| 4 | **Surface a clear implication** | If you list steps that obviously must run in sequence, it may say so. It never invents a requirement. |
| 5 | **Tighten** | Removes filler, duplication and empty hedging. |

Hard rules the instruction enforces: code, commands, file paths, identifiers,
numbers, units, URLs, error messages and quoted text are preserved
**byte-for-byte**; existing Markdown structure and list markers survive; the
original language (including a deliberate mix) is kept; text that is already
clear comes back unchanged; and the reply contains *only* the rewritten text —
no `Rewritten:` heading, no surrounding quotes, no notes.

### Behaviour you should know about

- **Your edits win.** If you keep typing while a rewrite is in flight, the
  result is *not* applied and the button reports that the draft changed. The
  model's output never silently clobbers newer typing.
- **The whole draft is replaced.** The result is written back as one edit, so a
  single undo restores what you had.
- **Empty drafts are refused** before a provider is contacted.
- **The rewrite is a side call.** It carries no session id and no purpose, so it
  never enters a conversation log and never trips the listeners that watch
  `llm/stream` for agent-loop traffic.

---

## Editing a message you already sent

Hovering a user message reveals a pencil next to the copy button. Clicking it
does **not** put the text into the composer — your draft is left exactly where
it was. Instead the composer is swapped for an editing area, and the original
message stays in the conversation, unchanged, as the start of the branch you
came from.

- The editing area starts at the message's own text and **grows with it** up to
  a little over half the window height; it never scrolls inside a fixed box.
- Send with **Ctrl+Enter** (or **Cmd+Enter**) by default, or with the **Send**
  button. Plain `Enter` inserts a newline, so a multi-line edit is one
  keystroke away from being sent by accident.
- **The shortcut is configurable** on the settings page: `Ctrl+Enter`,
  `Ctrl+Shift+Enter`, `Alt+Enter`, or none at all. It is a browser preference,
  saved locally, not a Harness shortcut registration — the Harness already
  binds `Ctrl+Enter` to its own complementary action, and a plugin cannot claim
  a binding that is taken.
- **Esc** cancels. So does the Cancel button.
- If the Harness refuses the text, the editing area stays open with the message
  you wrote, so nothing is lost.

### What "editing" means here

The Harness's session log is strictly append-only: there is no truncate, rewind
or rollback API, and this plugin does not pretend otherwise. Sending an edit
**forks** the conversation at the event just before the message and sends the
edited text into the new branch. The original message and everything the model
answered stay behind in the conversation you came from; the branch you were
editing joins the same lineage and appears in the session list with an
incremented title.

That is why the pencil is on user messages only. Editing a model reply would
mean asking a different question, not fixing the one you asked.

### Stepping between versions

Once a message has been edited — or a reply regenerated — the action row grows
`‹ 1/3 ›` arrows with an `N/M` counter. The arrows switch between the
conversations that message exists in: the original first, then each branch in the
order they were cut. A tooltip on the group says which one you are looking at. The
record is local to the browser.

The arrows under a **reply** lead to every answer the question received, whether
each came from editing the question or from regenerating the answer, because both
paths write to the same record.

### When the pencil is disabled

A message is editable only when re-asking it cannot mislead the model. Two things
are checked, and either one being true disables the pencil and shows the reason
under the bubble.

**The tools the reply used.** Editing cuts a branch before your message, so the
model in that branch meets a workspace that the previous reply may already have
rewritten — and it has no idea, because the turns that did the rewriting are
gone. So a turn is editable only when every tool it used only *reads*:

| Allowed | Blocked |
| --- | --- |
| `read`, `read_image`, `glob`, `grep` | `write`, `edit`, `str_replace_editor` |
| `web_search`, `web_fetch` | `pwsh`, `bash`, and any other shell |
| `job_list`, `job_output`, `get_goal` | `skill`, `todo_write`, `present` |
| `acp_status`, `acp_cache`, `search_context` | `decompress` (it writes a file when given `toFile`) |
| `load_workspace_dependencies` | any tool this build does not know |

Membership is decided by whether a tool can leave anything behind, not by whether
it sounds passive — which is why `decompress` is on the blocked side even though
it only reads context, and why a shell is blocked even when the command it ran
happened to be `ls`.

A plain chat reply used no tools at all, so it stays editable. The list is a
whitelist: anything unrecognised blocks the pencil, because a tool that cannot be
classified cannot be proven harmless. A reply that is still being written is
blocked too, since editing it would race the answer already in flight.

The very first message of a conversation has nothing before it to branch from, so
its pencil is drawn disabled rather than hidden — a missing button reads as a bug,
a disabled one explains itself.

**The files the message referred to.** A message that mentioned a file with
`@path` is checked against that file. If it has changed, is gone, or cannot be
read, the pencil is disabled with the reason shown.

The check goes through the Harness' own `workspaceFiles` service
(`remote.workspaceFiles.stat`), so the fingerprint is the Host's, resolved
inside the session's working directory — this plugin reads no files itself.

The limit is worth stating plainly: the Harness keeps no file history, so "has
this file changed since the message was sent" is not answerable. The guard
records a fingerprint the first time the plugin ever sees the message and blocks
once the file no longer matches that fingerprint, is missing, or is unreadable.
If a file changed while the message was never on screen, the guard cannot know.

---

## Asking again without memory

Under every model reply, next to the copy button, sits a circular restart arrow.
It answers **that same question again in a conversation that remembers nothing**.

Pressing it does not fork. A fork carries the whole prefix with it, which is
precisely the memory this is meant to be rid of. Instead a **brand-new Session**
is created in the same working directory, your one message is sent there, and the
Harness switches to it. The model sees that message and nothing before it — not
your earlier turns, not the reply you were reading, not any file the reply had
just rewritten. The original answer stays exactly where it was.

- It works on **any** reply, not just the last one. Regenerating an earlier one
  leaves the later turns behind in their conversation, untouched.
- The original answer is never rewritten, for the same reason an edited message
  is not: a Session log only grows.
- While the new conversation is starting, the icon spins and the button is
  disabled, so a second press cannot start a second Session.

Because a regenerated answer lands in a conversation that shares neither lineage
nor message seq with its siblings, it is filed under the same version record the
pencil writes — anchored on the conversation the question was *first* asked in.
That is what makes the arrows below it lead to every answer the question has
received, however each one was produced.

---

## The memory tree

The right sidebar gains a **Memory tree** tab. It draws the conversation's
lineage, not its transcript: which conversation came from which, and what was
asked at each step.

- **Stream** — the chain of conversations leading to the one you are reading,
  growing as it runs. This is the default.
- **Full** — every conversation in the workspace, including branches you are not
  on.

Each conversation is a node; hovering one shows the prompt that started it and
how many turns it has, and a node's expanded rail lists its own turns. A dashed
dot marks a running conversation, a filled one a finished conversation, and the
current one is highlighted. Where a conversation has siblings, the same `‹ N/M ›`
arrows appear, and clicking a node opens it.

The data comes from the Harness's own session catalog and `turnOutline`
projection — the tab reads what the session list already knows, it does not
build a second log.

---

## Requirements

- DeepSeek Harness **0.2.0-rc.2** or newer (`dsh.engines.dsh` in `package.json`).
- A profile whose composition includes the Harness web UI and the settings
  service — i.e. any normal desktop or web profile. The settings page needs
  `@deepseek-ai/dsh-settings` plus `@deepseek-ai/dsh-config-editor`, which the
  stock `desktop` and `web` profiles already carry.
- At least one usable LLM provider. The plugin calls `ctx.llm.stream()`, so
  whatever the Harness itself can talk to will work.

## Install

From a checkout of this repository:

```sh
dsh plugin --profile <profile> add /absolute/path/to/dsh-input-optimizer
```

A packed tarball works the same way and is the shape the profile stores:

```sh
pnpm pack
dsh plugin --profile <profile> add ./dsh-input-optimizer-1.0.0.tgz
```

Then reload the app (or just wait — Harness reconciles a changed `dsh.profile.bundles`
list on the fly) and confirm:

```sh
curl http://127.0.0.1:19387/plugins/input-optimizer/models
```

`{"ok":true,"providers":[…]}` means the Host half is live. The star button
appears on the next page load.

### Uninstall

```sh
dsh plugin --profile <profile> remove dsh-input-optimizer
```

Settings saved on the plugin's page live in the profile's `cordis.patch.yml`
under an `input-optimizer` entry; removing the plugin leaves that entry behind,
and you can delete it by hand if you want the file clean.

> **Do not install with `link:` and then replace it with a tarball.** A pnpm
> `link:` (junction) install points the profile at your working copy; replacing
> it deletes the junction and pnpm follows the reparse point, wiping the source
> directory. Development checkouts are better installed as tarballs, or tested
> on a throwaway profile.

> **Do not install by hand.** Adding the package to the profile's
> `package.json` and `node_modules` yourself leaves the running process holding
> a stale module-resolution table, and the plugin will load as a dead row with
> no visible error. Let `dsh plugin add` do it. If you have already hand-edited,
> restart the app once.

---

## Settings

Open **Settings → Input optimizer**. The page reads and writes this plugin's
config through the Harness settings service, so edits are saved to the profile's
`cordis.patch.yml` and survive restarts.

| Field | Default | Purpose |
| ----- | ------- | ------- |
| **Rewrite instruction** | *the shipped prompt* | The system prompt sent with every rewrite. Clear the field and save to restore the default. |
| **Follow the conversation's model** | **on** | On: rewrite with whichever model the current conversation has selected. Off: always use the pinned model below. |
| **Provider** | *(empty)* | Used when following is off — or as a fallback when the followed model is unavailable. |
| **Model** | *(empty)* | Same. |
| **Reasoning effort** | **low** | How hard the model thinks. `none` sends no effort field at all. The list is read from the chosen model's own advertised efforts, so you cannot pick one a model does not support. |
| **Max output tokens** | `4096` | Ceiling for one rewrite. `0` means no ceiling of this plugin's own: the request goes out without a `maxTokens` field. |
| **Timeout** | `60000` ms | Wall-clock ceiling for one rewrite. |
| **Send edit with** | **Ctrl+Enter** | The shortcut that sends an edited message. `Ctrl+Shift+Enter`, `Alt+Enter` and *none (button only)* are also offered. |

The shipped rewrite instruction is the five-goal prompt in
[What it does](#what-it-does). It is written by an AI, it is editable, and
replacing it is the intended way to re-aim the optimizer.

On/off settings use a capsule switch rather than a checkbox, matching the
Harness's own switch: the visual state keys off `aria-checked`, so what is drawn
and what assistive technology reports cannot disagree. The plugin draws its own
control because a client plugin may not require the Harness's client packages.

### Model resolution

Each rewrite resolves a route in this order, and reports which one it used in
the response:

1. **`session`** — the model the current conversation has selected, when
   *Follow the conversation's model* is on and that model still resolves.
2. **`config`** — the provider/model pinned on this page.
3. **Failure** — a `NO_MODEL` error telling you to pick a model for the
   conversation or name one in the settings.

The effort is validated against the resolved model's advertised efforts. If a
model does not support the requested effort, the request goes out without one
rather than failing.

---

## HTTP surface

The Client half talks to the Host half over three routes under
`/plugins/input-optimizer`. They are same-origin only.

| Route | Method | Body / query | Result |
| ----- | ------ | ------------ | ------ |
| `/optimize` | `POST` | `{ text, session?: { provider, model } }` | `{ ok, text, provider, model, routeSource, reasoningEffort, truncated }` |
| `/models` | `GET` | — | `{ ok, providers: [{ id, name, models: [{ id, name }] }] }` |
| `/models/efforts` | `GET` | `?provider=&model=` | The efforts that model advertises |

The file check behind the pencil does **not** go through this plugin: it uses
the Harness' own `workspaceFiles` service (`remote.workspaceFiles.stat`), so a
file that a message mentioned is fingerprinted by the Host itself, inside the
session's workspace, with no extra route.

Errors are JSON: `{ ok: false, error: { code, message } }`. Codes you may see:

| Code | HTTP | Meaning |
| ---- | ---- | ------- |
| `BAD_JSON` | 400 | The body was not valid JSON. |
| `EMPTY_INPUT` | 400 | The draft was empty or whitespace. |
| `CROSS_ORIGIN` | 403 | The request did not come from the Harness UI. |
| `BODY_TOO_LARGE` | 413 | Body over 512 KiB. |
| `INPUT_TOO_LARGE` | 413 | Draft over 200,000 characters. |
| `NO_MODEL` | 400 | No usable model — see [Model resolution](#model-resolution). |
| `TIMEOUT` | 504 | The rewrite exceeded the configured timeout. |
| `LLM_ERROR` | 502 | The provider refused. Carries the provider's own `llmCode`. |
| `MAX_TOKENS` | 502 | The reply hit the output ceiling before finishing. |
| `EMPTY_RESPONSE` | 502 | The model returned nothing usable. |

## Layout

```text
dsh-input-optimizer/
├── package.json         manifest: exports, dsh.bundle, dsh.client, engines
├── cordis.patch.yml     the insert row; its `id` is the settings namespace
├── icon.svg             the four-pointed star, used in plugin listings
├── locale/
│   ├── en.json          plugin title/description for the plugin list
│   └── zh.json
└── lib/
    ├── index.js         Host half: Config schema, HTTP routes, the LLM call
    └── client.js        Client half: the star, the pencil, the editor,
                         the settings page, the memory tree
```

`lib/index.js` is the Host half: it declares the configurable fields with
`schemastery`, opens the three routes, and runs the rewrite through
`ctx.llm.stream()`. `lib/client.js` is the Client half: it registers the star
into `conversation.input.right` — the seat immediately left of
`conversation.input.model` — the settings page into `settings.section`, the
shadow user bubble into `conversation.chat.node`, the editor into the
`conversation.composer` chain, the regenerate/version actions into
`conversation.chat.assistant-actions`, and the memory tree into the right sidebar.
It loads no Harness package as a module, per the client rules, and asks the
Harness' `remote.workspaceFiles` service about mentioned files instead of
opening a route of its own.

Two of those registrations are worth knowing about if you fork this:

- **The message pencil shadows the Harness's own user bubble.** The Harness gives
  user messages no slot for extra actions — the actions row is hard-coded inside
  its `UserMessageNodeView` — so the only way in is to register the same
  `conversation.chat.node` key (`user`, and `steering` for the same reason) at a
  **lower priority** and draw the whole bubble. Lowest priority renders, so this
  takes the cell; because a slot entry that throws is abdicated, a bug in the
  shadow falls back to the Harness's bubble rather than leaving a hole. The CSS
  classes are the plugin's own, and the bubble is redrawn from the same tokens
  the Harness uses, not by requiring its styles.
- **The editor replaces the composer through the `conversation.composer`
  chain.** The chain keeps the real composer mounted underneath
  (`data-chain-overlay-fallback`) and picks the first entry whose `select`
  returns non-null, so the plugin's editor only appears while its own pending
  interaction exists and everything else — approvals, questions, subagents —
  keeps working untouched.
- **The regenerate button and the version arrows share one seat.** Both live in
  `conversation.chat.assistant-actions`, which the Harness renders inside a
  reply's action row between the copy button and the branch button. That seat
  hands a plugin exactly one prop — the `messageId` of the finished answer — so
  the plugin walks back from it to the turn, then to the user message that opened
  it, to learn what to ask again and which version record to file the answer
  under.


The `id` in `cordis.patch.yml` (`input-optimizer`) is load-bearing. The Harness
settings service addresses a plugin's volatile fields by its *profile entry id*,
so the Client half hard-codes the same literal rather than importing the Host
half to learn it.

---

## Development

The plugin is a plain two-half Harness bundle. Its Host half imports
`@deepseek-ai/schemastery` and `@deepseek-ai/dsh-llm` as peer dependencies; a
profile supplies those. Its Client half declares `remote` and
`remote.workspaceFiles` (the latter carried by `dsh-api-remotes`) so the
workspace-file namespace is in the module graph before the plugin activates. To
exercise the Host half outside a profile, point a local
`node_modules/@deepseek-ai/` at the Harness installation's own copies.

```sh
# syntax check both halves, using the Harness runtime as the Node executable
ELECTRON_RUN_AS_NODE=1 "$DSH_INSTALL/DeepSeek Harness.exe" --check lib/index.js
ELECTRON_RUN_AS_NODE=1 "$DSH_INSTALL/DeepSeek Harness.exe" --check lib/client.js

# build the tarball a profile installs
pnpm pack
```

The Client half is loaded by the Harness browser loader, not by Node: it wraps
itself in `window.__ModuleLoader__.load({ id, factory })` and may `require` only
`react` and `react/jsx-runtime`.

---

## AI-generated

Every part of this repository was produced by an AI assistant in response to a
user's request:

- `lib/index.js`, `lib/client.js` — implementation, including all comments;
- `icon.svg` — the four-pointed star;
- `locale/en.json`, `locale/zh.json` — plugin listing text;
- `cordis.patch.yml`, `package.json` — the bundle wiring;
- `README.md` — this document.

The AI did not copy from an existing plugin. It read the Harness's published
skill for plugin authors, the slot and settings contracts shipped in the
installation, and the source of installed third-party plugins to learn the
conventions, then wrote this. Facts about Harness internals quoted here were
verified against the running installation.

Review it before you rely on it. It was written against Harness **0.2.0-rc.2**,
and nothing guarantees it tracks later releases.

## License

[MIT](LICENSE) © 2026 dsh-input-optimizer contributors

Install

dsh plugin --profile web add github:Xxsttb/dsh-input-optimizer

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source