PerryLink/dsh-composer-history

PerryLink/dsh-composer-history

Web 作曲器终端风格输入历史:边缘优先的方向键召回并精确还原草稿与光标、浏览器本地持久化历史、Ctrl+R 反向搜索,以及滑动上下文感知;0.5.0 再加智能输入层——跨会话片段库(/save、/load)、带变量的提示词模板、复用洞察与压缩摘要高亮。

UI 增强compactioncomposercontext-window

⌨️ dsh-composer-history

Release stamp: 0.8.8 (2026-10-04).
  • 1024 store channel: npm i -g dsh1024 once, then dsh1024 plugin --profile web add dsh-composer-history (counts toward the deepseek1024.com install ranking).

Terminal-style input history for the DeepSeek Harness Web GUI composer.

*Press ↑ like it's in a terminal — and keep your half-typed draft safe.*

English · 简体中文 · Español · Português · हिन्दी

dsh plugin --profile <name> add dsh-composer-history

📖 Ecosystem knowledge base — measured data, not marketing: plugin development guide · plugin-selection data · maintenance criteria.

Install & uninstall

The npm package ships the built bundles; a source checkout must be built first (pnpm run build) — the client-package check refuses to boot against an unbuilt bundle.

  • git channel (latest main): dsh plugin --profile web add github:PerryLink/dsh-composer-history.
  • npm channel (published releases): dsh plugin --profile web add dsh-composer-history.
  • tarball channel: pnpm pack in this repo, then dsh plugin --profile web add ./dsh-composer-history-.tgz.
  • uninstall: dsh plugin --profile web remove dsh-composer-history (or remove the row from the profile patch).

⭐ 如果它帮到了你

这个插件是 DSH 插件家族的一员(95 个,全部 Apache-2.0)。如果你在用,给个 star —— 它不会解锁任何功能,但会让下一个人在搜索里更容易找到它。

*English:* part of a 95 plugin family for DeepSeek Harness. If it is useful, a star helps the next person find it — nothing is gated behind it.

What is dsh-composer-history?

Terminal-style input history for the DeepSeek Harness Web GUI composer.

Press ↑ like it's in a terminal — and keep your half-typed draft safe.

*Workflow overview: install → open panel → act. The examples below use the real command surface.*

Compatibility

Surface · Status Harness · DeepSeek Harness dsh-v0.2.1-rc.2 (verified 2026-10-18): engines.dsh >=0.2.1-0 =24.0.0 Platforms · Web GUI only (client plugin; browser-local storage; no network, no native code) Model · Any (no model requests — pure UI behavior)

The browser half rides the published client packages (dsh-client-ui-conversation, dsh-client-ui-input-trigger, dsh-client-ui-settings) and the cordis Context; it no longer depends on the removed dsh-client-runtime package, so the client surface also lines up with 0.1.2-rc.1 hosts. Interception anchors on the web composer's DOM: the contenteditable surface div[data-composer-input] inside [data-input-scroll] (the Lexical composer shipped since 0.1.2-alpha.5 / 0.1.2-rc.1), with the legacy textarea composer inside [data-input-scroll] (harness lines up to 0.1.1-rc.2) still matched; textareas elsewhere pass through. The compat workflow's jsdom web-behavior smoke asserts this identity/text/caret face against the packed bundle. 0.1.2-rc.1 (adapted 2026-09-04): the session envelope keeps its ignorable field for stored-log read compatibility only - Session.append still cannot stamp it, so audit-gate behavior is unchanged. Verified 2026-09-06 against the dsh-v0.1.7-alpha.1 master checkout (full gate chain + profile install smoke). 0.1.5-rc.1 (adapted 2026-09-10): dependency pins move to the published 0.1.5-rc.1 line; no seam change affects this plugin's behavior. 0.1.5-rc.2 (adapted 2026-09-11): dependency pins move to the published 0.1.5-rc.2 line; no seam change affects this plugin's behavior. 0.1.7-alpha.1 (adapted 2026-10-04): the settings seam is replaced on both halves. The host half no longer registers a composer-history namespace through the removed ctx.settings.register(ns, schema, { base }) — on this line a plugin's durable settings surface IS its own live Config: every tunable is declared .volatile(), the profile entry id (composer-history, the bundle patch's row id) names the form, and apply claims the generated-form presentation (ctx.settings.configure({ auto: true }, ctx.fiber), effect-owned so a reload re-registers cleanly). The browser half reads the same entry through ctx.configForms.get('composer-history') instead of the removed ctx.settingsScope.bind({ namespace }); the snapshot/subscribe shape is unchanged, so the wiring still reinstalls on every committed change. Editable surface is preserved, not widened: every field the old namespace exposed is volatile, and no new field became editable. An existing settings.yaml section named composer-history is migrated into the profile entry of the same id by the host itself.

What you get

dsh-composer-history puts a terminal's input history into the DeepSeek Harness Web GUI composer:

  1. Edge-first arrow recall — bare ↑/↓ move the caret first; history recall triggers only when the caret sits on the first/last line. The first recall stashes {draft, caret}, and reaching the newest entry again (or pressing Esc) restores both exactly — never cleared.
  2. Persisted history — every sent message is appended to a bounded browser-local store, so recall survives page reloads and reaches across sessions.
  3. Reverse search — Ctrl+R (configurable) opens a query overlay over merged history, snippets, and templates.
  4. Smart input layer — /save//load snippets, prompt templates with {{workspace}}/{{session}}/{{draft}} variables, and browser-local reuse insights.
  5. Sliding-context aware — compaction summaries join recall and search as [compacted] … entries, and a transient notice announces each compaction with a one-click /compact fill.
  6. Versioned JSON backup — export/import all four libraries (history, snippets, templates, insights) as one schema-versioned document; download or copy on export, file pick or paste on import.

Pure UI behavior: no session events, no agent-loop changes, no model requests. Recalled text only enters the ordinary composer draft; it reaches the model only if *you* press Enter.

Quick start

dsh plugin --profile web add github:PerryLink/dsh-composer-history
# 1. install the bundle into your profile
dsh plugin --profile web add github:PerryLink/dsh-composer-history

# or from npm (published releases)
dsh plugin --profile web add dsh-composer-history

# 2. restart and verify the row
dsh --profile web --dump-config | grep -A3 'id: composer-history'

Examples

These are the plugin's real entry points; install once, then call one.

Detailed example

Command

~~~sh /save ship-check --tag=release,ops ~~~

Output

~~~text snippet saved: ship-check ~~~

*Observed from plugins/dsh-composer-history/README.md §Smart input layer (lines 199-205); code at src/client/index.ts:314-327*

Failure branch

Input

~~~text /load nope ~~~

Output

~~~text no snippet named nope (open Ctrl+R search to browse snippets) ~~~

*Observed from plugins/dsh-composer-history/src/client/index.ts:330-333; plugins/dsh-composer-history/src/client/notice.ts:62-65*

Brief examples

  • /load
  • /compact

Configuration

All tunables are Schemastery Config fields (changeable from cordis.yml and the entry's settings form; the profile patch is the persisted document). An id-targeted override replaces the whole row — restate every key you need. Invalid enum values fail the whole dsh boot loudly, and a settings-form edit is validated by the host before it persists.

Key · Default · Meaning recallWithDraft · 'save' · Recall mode (save / gate): save stashes a non-empty draft before recall; gate recalls only an empty draft (Claude/Codex-style gating) restoreOnEscape · true · Restore the stashed draft when Esc ends browsing edgeMode · 'logical' · Edge detection mode (logical / visual): by \n lines or by measured wrapped lines enableCtrlAlias · true · Let Ctrl+↑/↓ behave like the bare arrows restoreCaret · true · Also restore the stashed caret on bottom-out / Esc upKey · 'ArrowUp' · KeyboardEvent.key that recalls upward; '' disables downKey · 'ArrowDown' · KeyboardEvent.key that walks newer / restores; '' disables escapeKey · 'Escape' · KeyboardEvent.key that escapes browsing; '' disables maxHistory · 500 · Maximum recalled entries (newest kept); 0 = unlimited includeKinds · ['user'] · Conversation node kinds admitted into the history (add 'steering' to include steer messages) historyScope · 'session' · History scope (session / workspace): workspace prepends other listed sessions' user messages before the current session's persistHistory · true · Append sent messages to the browser-local store maxPersisted · 200 · Maximum persisted entries; 0 = unlimited enableSearch · true · Enable the Ctrl+R reverse-search overlay searchKeys · ['Ctrl+R'] · Chord specs opening the search (modifiers Ctrl/Alt/Meta/Shift + a key name); a malformed spec fails the browser fiber loudly searchCaseSensitive · false · Whether search matching distinguishes letter case includeCompactionSummaries · true · Admit [compacted] … checkpoint summaries into recall and search showCompactionNotice · true · Show a transient notice when a compaction checkpoint lands compactCommandText · '/compact' · Slash command the notice's "Compact now" action fills into the composer; '' hides the action enableSnippets · true · Enable the snippet library (/save, /load, search-panel picking) maxSnippets · 200 · Maximum stored snippets; 0 = unlimited enableTemplates · true · Enable the prompt-template library (variables fill at insertion) enableInsights · true · Enable the reuse-insight hint (local usage statistics) insightMinUses · 2 · Minimum uses before the reuse hint shows enableCompactionHighlight · true · Badge [compacted] … summaries distinctly in the search panel

Tools & surfaces

Surface · Kind · Notes ArrowUp · keybinding · Edge-first ↑/↓ recall — stashes {draft, caret}, restores both exactly on bottom-out or Esc Ctrl+R · keybinding · Reverse-search overlay over merged history, snippets, and templates /save · command · Save the current draft as a named, tagged snippet /load · command · Insert a saved snippet at the caret templates · UI · Versioned JSON backup export/import of history, snippets, templates, and insights (explicit click only) composer-history · settings form · The profile entry's own live Config form — carries the resolved config (cordis.yml base + profile overrides) into the browser half

Keybindings

Key · State · Behavior ↑ · IDLE, caret on first line · stash {draft, caret}, fill newest entry, caret to end (no history → pass) ↑ · BROWSING, caret on first line · older entry; hold at the oldest (intercept, no mutation) ↑ · caret not on first line · fully released (browser moves the caret) ↓ · IDLE · always released (plain caret movement) ↓ · BROWSING, caret on last line · newer entry; at newest → restore savedDraft + savedCaret → IDLE ↓ · caret not on last line · fully released Esc · BROWSING (restoreOnEscape: true) · restore savedDraft + savedCaret → IDLE, intercepted Esc · otherwise · released (menu/popup Escape semantics untouched) Ctrl+↑/↓ · enableCtrlAlias: true · same as bare arrows searchKeys chord · composer focused, plain phase, no menu/selection/IME · open reverse search; browsing ends, the shown text becomes the draft Shift/Alt/Meta+arrows, IME, selection · any · always released

upKey/downKey/escapeKey/searchKeys rename the keys above; the modifier policy (and the search chord's exact-modifier match) is unchanged. Inside the search overlay: ↑/↓ move the match selection (the selected row scrolls into view), Enter fills, Esc cancels, a click picks, a press outside cancels; matched substrings are highlighted in every row.

Reverse search

  • Open: the searchKeys chord while the composer is focused and the input is plain (a Ctrl+R here also stops the browser's page reload — the key is consumed only inside the composer).
  • Filter: substring match over the merged history (current session + persisted + workspace entries); case sensitivity per searchCaseSensitive; matched substrings are highlighted in each row.
  • Pick: Enter fills the draft and moves the caret to the end — the same single setDraft write path as ordinary recall. Recalled text reaches the model only if you press Enter afterwards.
  • Cancel: Esc or a press outside the panel; the draft is untouched.

Smart input layer

On top of the terminal-style history, three browser-local libraries turn the composer into a reusable input surface. Everything below lives in localStorage (keys dsh.composer-history.snippets.v1, .templates.v1, .insights.v1), never touches the network, and every switch is a Config field.

Snippets (cross-session command library)

/save ship-check --tag=release,ops
check the build, run the smoke suite, tag the release        ← the rest of the draft is the snippet
/save ship-check                                             → "snippet saved: ship-check"
/load ship-check                                             → the snippet fills the composer
Ctrl+R → search panel lists snippets (green badge = name) alongside history
  • /save consumes the Enter, stores the draft (minus the command line) under a kebab-case name with optional tags, and clears the composer. Nothing to save → an error notice, the command never sends.
  • /load inserts the snippet at the caret (whole-draft replace, caret to end) and counts the use.
  • Scope: snippets saved with a workspace cwd are workspace-scoped; snippets saved without one are global. maxSnippets bounds the library; same-name saves replace.
  • The plugin never sends: every fill lands in the ordinary draft and your Enter stays yours.

Prompt templates with variables

Templates are stored prompt texts with {{variable}} placeholders. The search panel lists them with a purple badge; picking one fills the variables from the live session and inserts the result. Built-in variables: {{workspace}} (the session's cwd), {{session}} (the session id), {{draft}} (the current draft). A template referencing an unknown variable fails loudly with the missing list — a half-filled prompt is worse than an error.

The whole library — history, snippets, templates, and insights — exports to and imports from a single versioned JSON document through the panel's Export JSON / Import file actions; see Backup export & import below. An explicit user action; the plugin never writes files on its own.

Reuse insights

Every newly committed user message (and every snippet load) lands one browser-local usage record keyed by exact text. While you type, a small hint under the composer reports used M× in N sessions · 在 N 个会话里用过 M 次 once the draft matches a prompt used in at least insightMinUses (default 2) sessions. Toggle with enableInsights; the statistics contain only the deduped texts and counters.

Compaction summary highlight

Ctrl+R marks [compacted] … summaries with an amber badge (history stays unbadged), snippets green, templates purple — the panel's provenance is visible at a glance. Toggle with enableCompactionHighlight.

Backup export & import

All four browser-local libraries export to and import from one versioned JSON document. The panel's Export JSON / Copy JSON / Import file / Paste JSON actions run entirely in the browser: download or copy to clipboard on export, file pick or pasted text on import — nothing is uploaded and no host RPC or network call is involved.

Document shape

{
  "schemaVersion": 1,
  "exportedAt": 1735689600000,
  "data": {
    "history": ["…"],
    "snippets": [{ "name": "…", "text": "…", "tags": [], "scope": "global", "createdAt": 0, "updatedAt": 0, "useCount": 0, "lastUsedAt": 0 }],
    "templates": [{ "name": "…", "text": "…", "description": "", "updatedAt": 0 }],
    "insights": [{ "text": "…", "sessions": [], "uses": 0, "lastUsedAt": 0 }]
  }
}

Merge & conflict strategy

  • History entries are plain strings and deduplicate by exact text; a duplicate or blank entry is skipped, never overwritten.
  • Snippets and templates key on name; insights key on exact text. On a same-key conflict the newest timestamp wins (updatedAt for snippets/templates, lastUsedAt for insights); an older or equal-timestamp import is skipped, and new keys are appended.
  • The import respects maxPersisted and maxSnippets; templates and insights cap at their fixed protocol limits (500 each).
  • The result notice reports how many records were written and how many were skipped (older/duplicate).

Schema versioning

schemaVersion starts at 1. Imports run a stepwise migration (one version at a time) so future formats can upgrade old documents in place. A document whose schemaVersion is newer than the one this build understands is rejected with an error — it is never silently dropped or partially merged; a version too old to migrate is rejected the same way.

Sliding context

The harness core gives every dsh session a sliding context window, the same workflow Claude Code and Codex ship: when a conversation approaches the model's context limit (or the provider reports an overflow), the harness auto-compacts — older turns are summarized behind a compaction checkpoint marker that stays visible in the transcript, the model keeps only the summary plus the recent tail, and the session continues. /compact triggers the same compaction on demand, and the marker renders as an expandable "Context compacted" row.

dsh-composer-history plugs the composer into that workflow so the window slide never costs you your typing history:

  • Recall survives compaction — shadowed turns stay in the finalized conversation the Chat view publishes, so ↑ still walks every message you sent before and after a checkpoint.
  • Summaries join the history — each checkpoint's summary text enters ↑ recall and Ctrl+R search as a [compacted] … entry (toggle: includeCompactionSummaries), so context the model no longer sees verbatim stays one keystroke away.
  • Compaction notice — when a checkpoint lands while the page is open, a transient snackbar announces it (the Claude Code "Auto-compacting conversation…" moment) with the summary snippet and a one-click Fill `/compact` action (showCompactionNotice, compactCommandText); the fill lands in the ordinary draft, and only your Enter sends it.
  • Search counts — the Ctrl+R panel now shows a live N entries / N matches status line, and long entries are clamped to two lines.
Compaction itself (thresholds, summary model, /compact) is owned by the harness core's compaction plugins — this plugin only observes the checkpoint markers the Chat view's conversation projection already publishes (uiConversation.binding(id).target('chat').legacy.nodes), so it works without any agent-loop or model-request changes.

Permissions & data