Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add dsh-suggest-ghost
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
Screenshots
README
dsh-suggest-ghost
English | 简体中文
Input prediction plugin for DeepSeek Harness Web: after each turn completes, one auxiliary LLM call predicts your next prompt and renders it as ghost text in the input box when the draft is empty; while you type, it completes from the current conversation history by prefix match. Tab accepts the whole suggestion, → accepts word by word.
What it is
A pure plugin mount (host listens to turn events + client-side DOM overlay) — no changes to any DSH core code. Two modes switch automatically:
- LLM next-suggestion (empty draft): after every completed turn, the last round of conversation is sanitized and sent to the suggestion model (inherits the main request route by default, no extra config) to produce "the sentence you are most likely to type next" — the same experience as Claude Code.
- History prefix completion (non-empty draft): zsh-autosuggestions style — finds prefix-matching entries in conversation history, scores by recency first, with in-session frequency and cross-session hotness as secondary signals, and renders the remaining part in gray. Full/half-width punctuation, whitespace, and case differences do not affect matching.
Ghost text is rendered as a DOM overlay approximation (follows the input's font and scroll), not depending on the official setGhost input capability, so it works on rc.6; keystrokes are not intercepted during IME composition.
Preview

① History prefix completion (non-empty draft) — type "list do" and the ghost completes the remainder of that historical message:

② LLM next-suggestion (empty draft, auto-predicts your next step after a turn ends):

Dark text = what you typed; gray = ghost suggestion. Tab accepts the whole suggestion, → accepts word by word. Ghost text follows the input's font and scroll rendering — it never overlaps or steals focus; if you don't like it, just ignore it and keep typing and it disappears — zero cost.
Installation
From npm (recommended — prebuilt, no approval needed):
dsh plugin --profile web add dsh-suggest-ghost
One-line install from GitHub (build artifacts are committed to the repo, no local build needed):
dsh plugin --profile web add "github:WuJiaoJue/dsh-suggest-ghost"
Or from source:
git clone https://github.com/WuJiaoJue/dsh-suggest-ghost.git
cd dsh-suggest-ghost && pnpm install && pnpm run build
dsh plugin --profile web add .
After installing, restart dsh web and hard-refresh the page with Ctrl+Shift+R. Works out of the box with no configuration required.
Configuration
All settings live in the Suggest ghost card in Settings → Plugins and take effect immediately after saving: card copy (field labels/hints, buttons, badges) follows the host DSH UI language (中文 / English) and updates in real time when the language is switched in Settings, without a refresh; hosts without a locale service fall back to Chinese.
| Group | Fields |
|---|---|
| LLM next-suggestion | enable toggle, max output tokens, max suggestion chars, reference turns, transcript char budget, timeout (ms), accept key, provider / model route (leave empty to inherit the main request) |
| History prefix completion | enable toggle, cross-session search, max history entries, min input chars, word-by-word accept |
| Hotness management | frequent cross-session phrases list: pin (exempt from eviction, always leads candidates), delete, manual add (pinned by default), clear all — takes effect immediately, no save needed |
You can also override by id in cordis.patch.yml (as initial values for the above):
- id: suggest-ghost
config:
maxInputBytes: 4096 # framed user prompt byte cap
maxOutputTokens: 512 # suggestion output token cap
timeoutMs: 60000 # auxiliary request deadline (ms)
maxRecentTurns: 1 # most recent completed turns sent to the suggestion model
maxTranscriptChars: 12000 # transcript char budget
maxSuggestionChars: 240 # visible suggestion char cap
acceptKey: Tab # accept shortcut
llmEnabled: true # LLM suggestion toggle
Security
- Only the last round of conversation is sent to the suggestion model, with common credentials auto-masked before sending; output is purified and unqualified replies are silently discarded
- Fully bounded end to end: input bytes / output tokens / timeout capped; re-entry guarded within the same turn, new turns invalidate stale generations, unloading aborts in-flight requests
Compatibility
Verified on both kernel generations (2026-09-07):
| DSH kernel | host entry link | runtime symbols | real boot (headless) | real boot (web) |
|---|---|---|---|---|
0.1.1-rc.2 |
✅ | ✅ 8/8 | ✅ past the plugin stage | ✅ client bundle HTTP 200 |
0.1.2-rc.1 |
✅ | ✅ 8/8 | ✅ past the plugin stage | ✅ client bundle HTTP 200 |
Peer deps are written as an explicit generation list (^0.1.1-rc.2 || ^0.1.2-rc.1) rather than >=0.1.1-rc.2: node-semver excludes prereleases from every range unless a comparator carries the same [major.minor.patch] tuple, so ^0.1.1-rc.2 is false for 0.1.2-rc.1, and so is * — there is no range form that spans generations. Every time upstream ships a new rc generation, add another entry here, otherwise installation fails the peer check.
Three deliberate choices keep one codebase working across generations:
- No
settingsNamespace(): the helper was removed in0.1.2, and importing it at runtime makes the whole module fail to link. It only validated and returned the branded string unchanged, so the code asserts the literal instead ('suggest-ghost' as SettingsNamespace); format validation still happens insidectx.settings.register()on both generations. - No
deepFreezeimport:0.1.1exports it fromdsh-llm,0.1.2moved it into the newdsh-util-valuesand stopped re-exporting it — and that package does not exist on0.1.1, so switching the import source would only break the other generation.src/generate.tsships an equivalent implementation (iterative traversal, cycle-safe, skipsAbortSignal). - The client bundle takes no types from
@deepseek-ai/dsh-client-runtime: that package is0.1.1-only and was split out in0.1.2.lib/client.jswas measured to have zero kernel imports, so it is generation-independent.
How to reproduce: build an isolated profile for the kernel under test (point $DSH_HOME at a temp directory and hardlink its node_modules to that kernel tree with cp -al, so upward resolution cannot leak into the other generation), then check manifest composition with dsh --profile <p> --dump-config, and watch plugin loading via dsh --profile <p> "say hi" and dsh --profile <p> -- --no-open --port <p>. Both headless runs stop at MISSING_CREDENTIAL (the temp home has no credentials) — a point after plugins have loaded.
Development
src/index.ts host entry: turn/end(completed) → bounded suggestion generation
src/coldstart.ts cold-start semantics (pure): ring seeding from the log, turn-based staleness, _push read-back, suggestion tracking
src/generate.ts transcript extraction → sanitization → ctx.llm.stream → purification
src/transcript.ts pure transcript logic: char + UTF-8 byte dual-budget trimming (unit-testable)
src/sanitize.ts sanitize / purify / semantic filter / truncation (pure functions)
src/settings.ts settings namespace + host→client push (_push) and client→host ops channel (_ops, tail-write coalescing)
src/hotness.ts cross-session hotness table (incremental dedup, min-heap eviction, bounded memory, pin/manage APIs)
src/hotness-store.ts hotness persistence (storageDomain unit, turn-boundary coalesced writes, restore-on-restart, ops application)
src/projection.ts suggestGhost projection last-wins fold
src/client/ ghost rendering, history matching, word splitting, shortcuts, settings card
scripts/ smoke tests and session log replay
Cold-start semantics (why it works right after a restart)
"No data after a restart" is a misreading: the suggestion lives in settings.yaml's _push, and history lives in the session log.
The old design used time as the criterion (invalidate on restart, seed only when an event arrives), which left two gaps.
It now uses semantics:
- The history ring is a cache of the session log: on a pull miss it is recomputed from the log (
ringFor), so there is no "seeding moment" and therefore no race where a pull arrives before the first event. Live sessions maintain it incrementally. - A suggestion is valid iff its turn is still the session's last completed turn (
suggestionIsCurrent). On startup the persisted_pushis read back and checked: still valid → restored as-is (your last suggestion is there when the page opens, no need to wait for a new turn); provably stale → cleared; session not in the store yet (lazy restore pending) → left untouched and reconciled by the first pull. - All three push paths (startup reconciliation / pull reply / hotness-restore re-push) share one state assembler
(
statePushOf), and the client pulls at the single point where a session binding succeeds.
pnpm run build # tsc compiles host + esbuild bundles client → lib/
pnpm run test:smoke # pure-function smoke tests (incl. hotness persistence and cold-start semantics)
pnpm run test:e2e # end-to-end over the real stacks: hotness persist→restore; cold-start reconciliation/pull
pnpm run replay # replay the completion pipeline with real session logs
Known limitations
- The cross-session hotness table is now persisted (host storage domain, lands in
~/.dsh/storages/suggest_ghost_hotness.json): frequencies survive restarts instead of accumulating from zero; on hosts without the storage domain (older versions) it automatically degrades to in-memory only - After a restart, history completion and the previous suggestion are available immediately (the criterion is semantic, not restart timing); a suggestion for a new turn still requires that turn to finish — that is inherent to "predict the next prompt", not a startup delay
- The management panel lists and filters the pushed top-K snapshot (50 entries by default; "N total" shows the full count); host-side search beyond the top-K is not implemented
- Delete only clears the current tally — typing the same text again re-counts; pin is the "never evicted" semantic
- LLM suggestions cover only the current session; to include text from other sessions as candidates, enable "cross-session search"
- Every completed turn triggers one suggestion model call (regardless of whether the input box has content); disable it in settings to save tokens if not needed
License
MIT © wujue. The security and generation pipelines are implemented with reference to dsh-suggest-prompt (MIT).
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.