返回目录
其他 待识别

dsh-plugin-quote

Xieweikang123/dsh-plugin-quote

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

Stars
0
Forks
0
Issues
0
更新
2 天前

PROJECT TOPICS

项目标签

PROJECT README

README

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 at the top-right of the selection: flush with the selection's own right edge, just above it (or just below it when the selection reaches the top of the message area). Why not the last line — see the implementation notes.
  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.

While the control is on screen, Ctrl+Shift+C copies a JSON report of the live geometry — the raw rects, the union, both selection endpoints, the surrounding markup, the viewport, and the control's own rect — to the clipboard. It exists for pages with no console, and it describes one moment: paste it into a bug report instead of describing a screenshot.

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 measured from the selected TEXT, never from Range.getClientRects(). That method is not "the rects of the selection". Blink builds it from two sources: the line box of each partly-selected text node, and the border box of every element the range fully contains. The second kind is not part of the highlight at all, and it is what broke placement. Selecting the last paragraph of an answer is the telling case: the drag has nowhere to land but the whitespace and the action bar below it, so the end point sits on a container element and every following sibling is fully contained. The range then reports the action bar's 28×28 button boxes and a full-column rect for the block — measured live, that pushed the anchor's bottom from 548 to 599 and flipped the control above the selection. The fix is to walk the text nodes the range intersects (TreeWalker, range.intersectsNode) and measure each one's own slice, which is how selection toolbars do it: an element border box then cannot enter. Rects inside a control (button, [role=button], form fields) are skipped, because a drag into the action bar can put the endpoint in a control's label — the 用量 423K tok read-out is a text node. The first design hung the control from "the last line" instead, and that cannot be recovered reliably from the rect list: measured in the Web shell, one selection returned seven rects in document order with the widest one (185px, reaching x=1078) at the TOP, while the visually lowest line was split into 28px, 28px and 15px fragments. Taking the last rect anchored to a sliver and parked the control mid-paragraph. The union of the text rects has no order to get wrong, and it is exactly right for one line, a wrapped paragraph, several blocks, and a line with inline chips.
  • Placement is above the selection by default, inside the message area. The row above a selection has already been read, while the row below is the one the user is still reading — and the control covers whichever it sits over. It also stays clear of the text just selected, since a drag ends at the selection's bottom edge. When there is no room above, it drops below instead. Both bounds matter: clamping to the viewport's bottom edge left the control floating over the composer, next to the input box and nowhere near the text it quotes, and clamping to innerWidth let it sit at the window's right edge. The region is the transcript's own box, cut off at the composer card's top edge.
  • The anchor tracks layout, not just the selection. A ResizeObserver on the transcript and the composer card re-reads the anchor when the draft wraps, a panel resizes, or the virtual list recycles a row — none of which fire a selection event. A control placed against stale rects is how it ends up over the composer after selecting the last paragraph.
  • 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 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 fill is the host's own near-black (#1b1b1c, its neutral-bluish-900) so the chip belongs to the product's palette, the quote glyph carries the brand accent, and the border is a fixed white hairline rather than a themed token — that hairline is what separates the chip from a dark transcript. The shadow is fixed too, for the same reason.

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:

Dependency Kind
The conversation.input.overlay slot, declared by @deepseek-ai/dsh-client-ui-conversation slot name
The useInput and inputActions props that every session-scope slot receives framework contract
ctx.slots and ctx.locale services framework contract
[data-conversation-scroll], [data-composer-card], and [data-composer-input] DOM markers not a contract — internal markers
react-dom's createPortal and a document root to portal into framework 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 neighbouring line (above it by default, below it when there is no room above), so the button sits over what is already on screen. 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)
npm run sync      # build, then copy lib/ into the installed Web profile

npm run sync exists because of a trap: the profile's cordis.patch.yml names the package, so the Web shell loads lib/client.js from the profile's node_modules, not from this checkout. npm run build alone changes nothing the page can see. npm run sync copies the build across (default ~/.dsh/profiles/web, override with $DSH_PROFILE_DIR or an argument).

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.

Probing the real page

Selection geometry is a fact about real layout, which jsdom does not model, so the interesting bugs only exist in a browser. tools/ holds three probes, all of which drive a real Chrome over CDP against the running app (http://127.0.0.1:3080/) and write JSON to the repo root (git-ignored):

  • probe-real-drag.mjs — drives an actual mouse drag (Input.dispatchMouseEvent) across the last paragraph of an answer and reports the range's raw rects, the text rects, and the control's rect. This is the one that reproduces the reported bug; programmatic setStart/setEnd cannot, because it never produces the element border boxes a real drag does.
  • dump-live-page.mjs — sweeps every selectable text node in a real session, compares the control's position against the placement formula, and flags any disagreement. Use it to check for regressions across many selections.
  • probe-quote-geometry.mjs — builds a synthetic transcript and loads the real bundle, for measuring the control in isolation.

docs/debugging-notes.md records what these were written to find. The short version: Range.getClientRects() is not "the rects of the selection" — it also reports the border box of every element the range fully contains, which is what a drag past the end of a paragraph produces, and which broke placement for months.

License

MIT

CLASSIFICATION EVIDENCE

分类依据

项目类型待识别
功能分类其他
规则置信度

系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。