Back to home@Xieweikang123

dsh-plugin-quote

DeepSeek Harness Web plugin: quote a text selection into the composer as a markdown blockquote.

Stars
0
Language
TypeScript
Created
Sep 11, 2026
Updated
Sep 11, 2026

Introduction

dsh-plugin-quote

A DeepSeek Harness Web plugin. Select text in the conversation, click the button that appears, and the selection lands in the composer as a markdown blockquote.

my question

> the paragraphs you selected
> keep their line breaks

Nothing new reaches the model: the quote is ordinary user text, exactly as if you had typed it.

Requirements

  • DeepSeek Harness with the Web profile (dsh web). There is no composer in the headless, sdk, or acp profiles, so the plugin is inert there.
  • Built and tested against dsh 0.1.5-rc.1. Harness plugin APIs are pre-stable — see Compatibility.

Install

npm install @walkerxie/dsh-plugin-quote

Then add one row to your Web profile's patch file (~/.dsh/profiles/web/cordis.patch.yml), pointing at the installed package:

- insert:
    - id: ui-quote
      name: '@walkerxie/dsh-plugin-quote'

To run from a checkout instead — useful while developing the plugin itself — build it and point the row at the file:

git clone https://github.com/Xieweikang123/dsh-plugin-quote.git
cd dsh-plugin-quote
npm install
npm run build
- insert:
    - id: ui-quote
      name: 'file:///absolute/path/to/dsh-plugin-quote/lib/index.js'

The profile reloads its patch live, so a page refresh is enough. If the button does not appear, restart dsh web once so the loader picks the new row up from boot.

Uninstall

Delete the - insert: block — or set disabled: true on the row — and refresh. The plugin owns nothing else: no config, no session data, no files of its own.

Usage

  1. Select text inside a message. Selections inside the composer itself never offer the button, because that is you editing your own draft.
  2. Click 引用选中文字 / Quote selection. It appears below the selection, right-aligned to the last selected line.
  3. The selection is appended to the draft as a blockquote, the control disappears, and focus moves to the composer with the caret at the end of the draft. Keep typing.

Escape dismisses the control without touching your selection.

How it works

  • One entry, no host behavior. The plugin registers a single conversation.input.overlay entry and its host half is an empty apply(). It adds no tool, no prompt section, and no session event.
  • Selection tracking. One document-level source listens for selectionchange, pointer and key gestures, scroll, and resize, and publishes only what changed. A selection is quotable only inside the conversation transcript. The anchor is the last line's box with a non-zero area: engines emit a zero-size rect for a trailing line break, and anchoring to it would pin the control to the viewport's top-left corner.
  • The plugin owns its coordinate frame. The control renders into a zero-size host the plugin creates as a direct child of <html> and pins to the viewport origin with position: fixed. The button inside it is position: absolute, so its coordinates are true viewport coordinates — the same space getBoundingClientRect() reports the selection in. Nothing above the host can reinterpret them: position, transform, filter, perspective, and contain on any ancestor all change what fixed resolves against, and the document element has no ancestor to do that. The host is created during the first render rather than in an effect, so the very first paint is already in the right space.
  • The draft write. The control calls the composer's own public inputActions.setDraft(). It reads and writes no state of its own — the draft stays where it already lives.
  • Focus hand-off. setDraft leaves the editor's selection at the end of the draft but moves no DOM focus, so the control focuses the composer's editor itself and collapses the caret at the end. The card to focus is read from the control's own slot seat, not by searching the document, because the button now lives outside it and a settled message's editor would otherwise be found first.
  • Fixed contrast, not themed. The control's fill and text are hard-coded black on white rather than drawn from theme tokens. It floats over conversation text it does not own, so it must stay legible over whatever is behind it; a themed pair would flip with the host theme and could land light-on-light or dark-on-dark. The border and shadow are still themed, because they only soften the edge. This is also why the control stays readable without the harness defining any particular token.

Compatibility

DeepSeek Harness plugin APIs are pre-stable: nothing below is a compatibility promise, and a harness update can break this plugin silently — the quote still lands, or the button simply stops appearing.

This plugin depends on:

DependencyKind
The conversation.input.overlay slot, declared by @deepseek-ai/dsh-client-ui-conversationslot name
The useInput and inputActions props that every session-scope slot receivesframework contract
ctx.slots and ctx.locale servicesframework contract
[data-conversation-scroll], [data-composer-card], and [data-composer-input] DOM markersnot a contract — internal markers
react-dom's createPortal and a document root to portal intoframework contract

The first three failing is loud. The DOM markers failing is silent, and the focus hand-off is a documented workaround rather than an API.

Known limits

  • The control covers the line below the selection. Placement is a fixed offset below the selection's last line, so the button sits over whatever follows it. Reserving layout space instead would reflow the transcript on every selection.
  • Append only. The public composer API exposes whole-draft replacement and no insert-at-caret verb, so a quote always lands at the end of the draft.
  • Plain text only. Quoting a fenced code block drops its fences and quoting a table drops its cell separators, because the browser's text serialization returns rendered text.
  • Transcript only. Selections in the trajectory or waterfall views offer nothing; they are separate surfaces.
  • One action. This is a quote button, not a selection toolbar. Copy, search, and annotation belong to whichever surface owns those decisions.

Development

npm install
npm test          # unit + real-cordis registration tests
npm run typecheck
npm run build     # lib/index.js (host half) + lib/client.js (browser half)

lib/client.js is not a plain bundle: the Web shell mounts plugins through a closure-factory contract, so the build wraps the browser half in window.__ModuleLoader__.load({ id, factory }) and resolves React and the other platform modules through the injected require instead of inlining them. scripts/build.mjs is that whole build, with a small CSS Modules compiler in front of it.

The CSS Modules compiler

Two details of that compiler are load-bearing, and both fail silently when wrong:

  • The class map must hold plain strings. lightningcss reports each export as { name, composes, isReferenced }, and handing that object to className renders [object Object], which no selector matches — every rule in the file is dropped. The build flattens the records to names.
  • The style tag is keyed by a content hash. HMR re-runs the module factory in the live page, so a path-only key would find the previous sheet still tagged and skip injecting the new one.

tests/build.client.spec.ts asserts both against the built bundle, because neither is visible in the sources.

License

MIT