DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-input-optimizer

d

dsh-input-optimizer

Manifest valid

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

UI (client)hasBundlePatch

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.
[ 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.


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:

#GoalWhat it means in practice
1Preserve intentAdds nothing you did not write. No answers, no opinions, no examples, no preamble.
2Fix languageGrammar, spelling, punctuation, wrong word choice, ambiguous pronouns, broken agreement, machine-translated phrasing. Your language and register are kept.
3Fix execution orderSteps 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.
4Surface a clear implicationIf you list steps that obviously must run in sequence, it may say so. It never invents a requirement.
5TightenRemoves 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:

AllowedBlocked
read, read_image, glob, grepwrite, edit, str_replace_editor
web_search, web_fetchpwsh, bash, and any other shell
job_list, job_output, get_goalskill, todo_write, present
acp_status, acp_cache, search_contextdecompress (it writes a file when given toFile)
load_workspace_dependenciesany 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:

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:

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:

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

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.

FieldDefaultPurpose
Rewrite instructionthe shipped promptThe system prompt sent with every rewrite. Clear the field and save to restore the default.
Follow the conversation's modelonOn: 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 effortlowHow 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 tokens4096Ceiling for one rewrite. 0 means no ceiling of this plugin's own: the request goes out without a maxTokens field.
Timeout60000 msWall-clock ceiling for one rewrite.
Send edit withCtrl+EnterThe 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. 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.

RouteMethodBody / queryResult
/optimizePOST{ text, session?: { provider, model } }{ ok, text, provider, model, routeSource, reasoningEffort, truncated }
/modelsGET—{ ok, providers: [{ id, name, models: [{ id, name }] }] }
/models/effortsGET?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:

CodeHTTPMeaning
BAD_JSON400The body was not valid JSON.
EMPTY_INPUT400The draft was empty or whitespace.
CROSS_ORIGIN403The request did not come from the Harness UI.
BODY_TOO_LARGE413Body over 512 KiB.
INPUT_TOO_LARGE413Draft over 200,000 characters.
NO_MODEL400No usable model — see Model resolution.
TIMEOUT504The rewrite exceeded the configured timeout.
LLM_ERROR502The provider refused. Carries the provider's own llmCode.
MAX_TOKENS502The reply hit the output ceiling before finishing.
EMPTY_RESPONSE502The model returned nothing usable.

Layout

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.

# 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 © 2026 dsh-input-optimizer contributors

Comments

Loading…