xiaoshenming/dsh-next-hint

dsh-next-hint

Ghost-text next-prompt suggestions in the DeepSeek Harness composer — Tab to accept, never auto-sends. 回合结束后在输入框里推荐下一句话,按 Tab 采纳。

Uncategorizedai-coding-assistantcomposerdeepseek

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.

Updating: the plugin page's update check only compares npm-registry sources; a
github: install is skipped and always reads as "latest". For a GitHub install use
one of these, then restart the app:
```bash
cd ~/.dsh/profiles/desktop && pnpm update dsh-next-hint
`
or uninstall it on the plugin page and add github:xiaoshenming/dsh-next-hint#main
again. To use the in-app update button, install the plugin from npm once it is published.

What you get

Moment · What happens A turn settles, draft is empty · One grey line at the composer's first line, Tab accept on the right Tab with focus in the composer · The sentence lands in the draft, caret at its end, not sent You start typing · The ghost disappears You switch sessions · The ghost is dropped Two declines / timeout / no route · Nothing 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.

Key · Default · Meaning enabled · true · Master switch provider / model · empty · Empty follows the session's last route; set both or neither locale · auto · Language of the suggestion maxTurns · 3 · Exchanges sampled maxInputBytes · 8000 · Input budget for the auxiliary request maxOutputTokens · 128 · Output token cap maxHintChars · 48 · Hint length cap; a paragraph is dropped, a long sentence truncated timeoutMs · 15000 · Per-call timeout perSessionCooldownMs · 0 · Per-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

Four same-kind plugins now exist, plus one complementary tool; prefer one same-kind:

  • This plugin: official dock slot and a stateless host route; Tab accepts, the offer is

dropped on a session switch.

  • Composer predictions (dsh-composer-prediction):

same idea; overlay slot, per-session cache, Esc, settings page. From 0.7.7 it detects this plugin and pauses itself (this plugin wins).

  • Composer Predictions (dsh-composer-predictions):

same idea plus draft continuation; Alt+↑/↓ candidate cycling, Esc, owner admission; needs DSH ≥0.2.1-rc.3.

  • Reply Suggestions (dsh-reply-suggestions):

same idea; Enter-to-fill, Esc, usage/cost stats; built for rc.2.

  • 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