# dsh-delete-message [简体中文](./README.md) | English A per-message delete plugin for DeepSeek Harness that keeps accidental or mistaken messages out of the model context. It adds a delete button to the action area of every chat message: once confirmed, the message is removed from the **model context** through the host's official surface-replace contract (an assistant reply takes its thinking, tool calls, and injected context with it) and hidden from the **visible transcript**. Raw log bytes are never rewritten and can be recovered at any time. ## Features - **All message types** — assistant replies mount in the official `conversation.chat.assistant-actions` slot; user input, machine-injected context rows, tool-call cards, THINK cards, model-retry lines, and terminal turn-error banners are enhanced via DOM augmentation (the host has no extension point for these sides). Chrome-row trash buttons (anything outside the user/assistant action strips) stay hidden by default and appear on message hover. - **Native visual consistency** — reuses the host's icon and button geometry, the primitives `Tooltip`, and the `Modal`+`Button` confirm dialog; adapts to light and dark themes automatically. - **Context-level deletion** — after confirmation, a `surfaceOp: { op: 'replace' }` placeholder node is appended (the same mechanism as the host's `/compact`); shadowed messages no longer enter `deriveMessages()`. - **Transcript-level hiding** — the host's visible transcript is append-only by design, so the plugin maintains a per-session deleted-seq ledger (persisted in localStorage), resolves row wrappers' React fiber identities to hide matching rows, and heals historically deleted rows unknown to this browser via a `/status` preflight at load time. - **Failed-turn cleanup** — turns that failed or were interrupted before any assistant reply landed (e.g. a 502 retry exhaustion) leave no assistant message, so the delete button had nowhere to mount and the injected context was stranded forever. Clicking the trash on any context row, tool-call card, or retry line replaces the **whole user-input unit** at once (injections, the reply, tool calls, and retry/error chrome go together; real user input is never touched). - **Step-level deletion** — in multi-step turns, clicking the trash on a THINK card or tool-call card removes **only that step's** reply and tool calls (`assistant/message` + paired `tool/result`); other steps in the same window survive. The confirm title and tooltip explicitly state the scope ("Delete this step" vs "Delete this entire attempt"). - **Role-aware confirmation** — the confirm dialog body is selected automatically from each mount's static message role; no user judgment required: a user message states its single-message scope, while an assistant reply states that its thinking, tool calls, and injected context are removed together with it. - **Preflight verdicts & graying** — trash targets are checked through a TTL-cached `/status` preflight on hover/click: refused targets gray out and state the localized reason instead of firing a doomed request; already-deleted rows heal silently; an unreachable host leaves icons neutral and clickable (the server always has final say). - **Delete transition feedback** — after confirming, the dialog enters a "Deleting…" pending state (spinner, with duplicate clicks and cancellation suppressed); failures show inline and can be retried in place; success collapses the affected rows with a graceful staggered leave animation (honors the system reduced-motion setting). - **Bilingual UI (zh/en)** — all UI copy (confirm dialogs, failure reasons, accessibility labels) ships with Chinese and English dictionaries, following the Language choice in system settings live; falls back to the browser languages when unset. ## Behavior Reference Clicking the trash in different locations produces different deletion scopes, dialog titles, and tooltips: | Click target | Deletion scope | Dialog title | Tooltip | |---|---|---|---| | THINK card 🗑️ | This step only | Delete this step? | Delete this step | | Tool-call card 🗑️ | This step only (owning assistant/msg + tool/results) | Delete this step? | Delete this step | | Context-injection row 🗑️ | Entire window | Delete entire attempt? | Delete entire attempt | | Retry line / error banner 🗑️ | Entire window | Delete entire attempt? | Delete entire attempt | | User message 🗑️ | That message only | Delete this message? | Delete | | Assistant slot (beside copy) 🗑️ | Entire window (reply + thinking + tools + injected context) | Delete this message? | Delete | > Step-level deletion applies to multi-step turns: only the target step's `assistant/message` and paired `tool/result` are removed; other steps in the same window survive. Whole-window cleanup is for one-shot sweeps of failed or interrupted turns. ## Screenshots ![Delete button in the message action area](docs/screenshots/delete-action.png) ![Delete confirmation dialog](docs/screenshots/confirm-dialog.png) ![Deleted message disappears from the session](docs/screenshots/deleted-hidden.png) > Screenshot files live in [docs/screenshots/](docs/screenshots/); see that directory's README for what each image should show. ## Safety - The server re-runs full validation before every deletion: only `user/message` and `assistant/message` surface nodes inside closed turns are accepted — real user input is a single-node replace, while assistant messages, machine-injected rows, and non-surface chrome anchors such as `tool/call` or `llm/retry` plan the whole user-input window as one unit; messages carrying tool calls cannot be deleted alone, already-shadowed messages and in-progress turns are refused with machine reason codes (localized in the UI). - The HTTP write path requires a dual-condition fence: loopback peer address AND a local Host header. - Zero configuration, zero runtime dependencies; the plugin never deletes files or rewrites logs. ## Installation (web profile) ```sh dsh plugin --profile web add github:viplocco/dsh-delete-message#v0.2.2 ``` After installing, **fully restart the DSH Web process** (the host-side plugin tree is read only at startup); the client bundle is served dynamically per request by the host, so updates take effect on a hard refresh. ## Installation (desktop profile) The DSH Desktop shell renders the **same web frontend** (the `@deepseek-ai/dsh-web-app` composition + the same WebServer origin), so this plugin is natively desktop-compatible — the only requirement is installing it into the desktop-owned profile and restarting DSH Desktop: ```sh # The desktop app uses a separate desktop profile; install it there too dsh plugin --profile desktop add github:viplocco/dsh-delete-message#v0.2.2 # or from a local checkout: dsh plugin --profile desktop add link: ``` After installing, **fully restart DSH Desktop** and hard-refresh the renderer. To confirm it loaded: ```sh dsh --profile desktop --dump-config # should show a "# == dsh-delete-message" section # in the renderer devtools console: "[delete-message] bundle script executing" ``` > **Why "web worked but desktop didn't":** the plugin's host half used to read `session.events`, which the current harness (`dsh-session` ≥ 0.1.2-rc.1, bundled by DSH Desktop) renamed to `session.snapshotEvents()`, so `/status` threw `Cannot read properties of undefined (reading 'find')` on desktop. v0.2.2 routes event reads through `eventsOf()`, which prefers `snapshotEvents()`, tolerates the legacy `.events` array, and returns `not-found` instead of crashing. ## Development ```sh pnpm test # node --test ``` See [docs/DESIGN.md](docs/DESIGN.md) for architecture, host contracts, and design trade-offs. Contact: viplocco@qq.com ## License MIT