DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-next-hint

d

dsh-next-hint

Manifest valid

Ghost-text next-prompt suggestions in the DeepSeek Harness composer — Tab to accept, never auto-sends. After the round ends, it recommends the next sentence in the input box; press Tab to accept.

UI (client)hasBundlePatch

dsh-next-hint

When a turn ends, the composer suggests the next message by itself. Tab accepts, nothing is ever sent.

A DeepSeek Harness plugin that brings Claude Code–style prompt suggestions to DSH — as ghost text inside your own composer: visible, undoable, and never on the return key.

┌────────────────────────────────────────────────────────────┐
│ fix the two failing cases as well              Tab accept   │  ← ghost, appears on settle
└────────────────────────────────────────────────────────────┘
                     ↑ only while the draft is empty; typing dismisses it

Why install it

  • It knows what you were doing. One decoupled auxiliary request reads the last few turns of this session — the file you touched, the test that went red, where the work stopped — instead of a template like "continue?".
  • It never touches your Enter key. Accepting inserts ordinary text into the draft. Ctrl+Z undoes it; sending stays your decision. There is no send path in the code at all.
  • It stays quiet. No route, timeout, rate limit, malformed output, or a plain "not today" all end as no ghost and no error surface — at most one warn in the log.
  • It does not ship your conversation anywhere. The browser posts a sessionId; the host reads the transcript inside its own process, writes it nowhere, logs no text, and keeps it on this machine.

Install

DSH Desktop: Settings → Plugins → add a git package

github:xiaoshenming/dsh-next-hint#main

The desktop profile is owned by the app — dsh plugin --profile desktop … is refused by the CLI. For a self-managed profile such as web:

dsh plugin --profile web add "github:xiaoshenming/dsh-next-hint#main"

Restart the app once — plugins join the roster at boot. Update from the plugin page, or:

cd ~/.dsh/profiles/desktop && pnpm update dsh-next-hint

If the new version does not show up after an update, restart the app once.

What you get

MomentWhat happens
A turn settles, draft is emptyOne grey line at the composer's first line, Tab accept on the right
Tab with focus in the composerThe sentence lands in the draft, caret at its end, not sent
You start typingThe ghost disappears
You switch sessionsThe ghost is dropped
Two declines / timeout / no routeNothing at all — the correct outcome

A model that answers "nothing to suggest" is asked once more with a stricter instruction before the plugin gives up: one refusal never blanks the whole turn.

Tab only fires when no candidate menu owns it: with the / or @ menu open, Tab still belongs to the menu.

Configuration

Settings → Plugins → dsh-next-hint. Every key has a default; nothing is required.

KeyDefaultMeaning
enabledtrueMaster switch
provider / modelemptyEmpty follows the session's last route; set both or neither
localeautoLanguage of the suggestion
maxTurns3Exchanges sampled
maxInputBytes8000Input budget for the auxiliary request
maxOutputTokens128Output token cap
maxHintChars48Hint length cap; a paragraph is dropped, a long sentence truncated
timeoutMs15000Per-call timeout
perSessionCooldownMs0Per-session cooldown; default suggests every turn

How it works

A dual-face cordis plugin, the same shape as dsh-session-surgeon:

browser   session.running true → false, draft empty
            └─ POST /api/next-hint  { sessionId }        ← id only, no transcript
host      loopback + origin check → single flight + cooldown
            └─ ctx.sessions.get(id).deriveMessages()      ← recent turns
            └─ ctx.llm.prepareCall(...)                   ← one decoupled call, cheapest effort
            └─ 200 { hint } / 204 (silent, with x-next-hint-reason)
browser  paint the ghost on line one → Tab → input.actions.captureInsertion() + insertText()

Deliberate choices:

  • Decide in the browser, generate in the host. Only the browser knows whether the draft is empty, whether you are typing, and which session is on screen; asking the host to guess gets it wrong twice.
  • One request/response. No SSE, no long-lived connections, no server-side cache. Cancelling is AbortController.abort().
  • Official slot, own pixels. The component mounts in conversation.composer.dock; the ghost node is ours because the host exposes no inline-hint write path. Unmounting removes the node, the listeners, and the in-flight request.
  • The auxiliary call follows the session-title recipe: its own system instruction, its own timeout, the cheapest reasoning level, and a failure that cannot touch the main conversation.

Related work

Two neighbouring plugins exist; neither is the same tool, but both can paint on the same composer:

  • Same idea: Composer predictions (dsh-composer-prediction). Both turn "a turn settled" into one auxiliary request and a ghost line, both decide in the browser and both post to a stateless host route; the seams differ (they mount the overlay slot, ship their own settings page, cache per session and dismiss with Esc; this one uses the official dock slot and drops the offer on a session switch). Prefer one — up to 0.7.6 you would get two overlapping ghosts; from 0.7.7 it detects this plugin and pauses itself (this plugin wins). Still prefer one to avoid version drift.
  • Complementary: dsh-input-assist completes the sentence you are typing and checks typos; this plugin suggests the next message after a turn. The only overlap is an idle empty composer — if you see two ghosts there, turn one of them off.

Compatibility

  • Measured on DSH Desktop 0.2.1-rc.2 (Windows) and the same version's web profile (macOS). The host half needs ctx.llm, ctx.sessions, and ctx.webServer.register; the browser half needs the conversation.composer.dock slot and the composer input facade.
  • Still 0.x: if upstream moves those seams, the ghost breaks first. Its position relies on the editor's [data-input-scroll] structure; a missing anchor means no ghost, silently.
  • Requests from outside loopback are refused by the route.
  • If DSH ships this natively, this plugin can be disabled cleanly — nothing here assumes it is the only writer of the composer.

Development

node --test      # 49 tests, node:test only, zero runtime dependencies

Pure logic lives in src/ (no cordis, no official packages, no DOM); seams live in plugin/. Design decisions and the evidence behind them are in docs/, from the line-by-line sources in RESEARCH.md to the data flow and failure modes in ARCHITECTURE.md.

License

MIT

Comments

Loading…