@walkerxie/dsh-plugin-quote 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,19 +55,24 @@ Delete the `- insert:` block — or set `disabled: true` on the row — and refr
55
55
  ## Usage
56
56
 
57
57
  1. Select text inside a message. Selections inside the composer itself never offer the button, because that is you editing your own draft.
58
- 2. Click **引用选中文字** / **Quote selection**. It appears below the selection, right-aligned to the last selected line.
58
+ 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.
59
59
  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.
60
60
 
61
61
  Escape dismisses the control without touching your selection.
62
62
 
63
+ 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.
64
+
63
65
  ## How it works
64
66
 
65
67
  - **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.
66
- - **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.
68
+ - **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.
69
+ - **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.
70
+ - **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.
71
+ - **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.
67
72
  - **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.
68
73
  - **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.
69
74
  - **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.
70
- - **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.
75
+ - **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.
71
76
 
72
77
  ## Compatibility
73
78
 
@@ -87,7 +92,7 @@ The first three failing is loud. The DOM markers failing is silent, and the focu
87
92
 
88
93
  ## Known limits
89
94
 
90
- - **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.
95
+ - **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.
91
96
  - **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.
92
97
  - **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.
93
98
  - **Transcript only.** Selections in the trajectory or waterfall views offer nothing; they are separate surfaces.
@@ -100,8 +105,11 @@ npm install
100
105
  npm test # unit + real-cordis registration tests
101
106
  npm run typecheck
102
107
  npm run build # lib/index.js (host half) + lib/client.js (browser half)
108
+ npm run sync # build, then copy lib/ into the installed Web profile
103
109
  ```
104
110
 
111
+ `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).
112
+
105
113
  `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.
106
114
 
107
115
  ### The CSS Modules compiler
@@ -113,6 +121,16 @@ Two details of that compiler are load-bearing, and both fail silently when wrong
113
121
 
114
122
  `tests/build.client.spec.ts` asserts both against the built bundle, because neither is visible in the sources.
115
123
 
124
+ ### Probing the real page
125
+
126
+ 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):
127
+
128
+ - **`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.
129
+ - **`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.
130
+ - **`probe-quote-geometry.mjs`** — builds a synthetic transcript and loads the real bundle, for measuring the control in isolation.
131
+
132
+ `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.
133
+
116
134
  ## License
117
135
 
118
136
  MIT
package/README.zh.md CHANGED
@@ -55,19 +55,24 @@ profile 的 patch 是热重载的,刷新页面即可。如果按钮没出现
55
55
  ## 使用
56
56
 
57
57
  1. 在**消息**里选中文字。输入框内部的选区永远不会给出按钮——那是你在编辑自己的草稿。
58
- 2. 点 **引用选中文字**(英文 `Quote selection`)。它出现在选区下方,与最后一行右边缘对齐。
58
+ 2. 点 **引用选中文字**(英文 `Quote selection`)。它出现在**整个选中区域**的右上方:横向贴着选区自己的右边缘,纵向在选区上方一点(选区贴到消息区顶部时会落到下方)。为什么不贴"最后一行"——见下面的实现要点。
59
59
  3. 选区以引用块形式追加进草稿,控件消失,焦点交给输入框、光标落在草稿末尾。接着打字就行。
60
60
 
61
61
  按 Escape 可以关闭控件,且不改动你的选区。
62
62
 
63
+ 控件在屏幕上的时候,**Ctrl+Shift+C** 会把一份 JSON 读数复制到剪贴板:原始 rect 列表、它们的并集、选区两端点、周边标记、视口,以及控件自己的 rect。它是给"打不开控制台"的页面准备的,描述的是**同一时刻**——提 bug 时粘这份 JSON,而不是描述一张截图。
64
+
63
65
  ## 实现要点
64
66
 
65
67
  - **一个条目,没有 host 行为。** 插件只注册一个 `conversation.input.overlay` 条目,host 半边是空的 `apply()`。它不添加工具、不加提示词段落、不写会话事件。
66
- - **选区追踪。** 一个文档级来源监听 `selectionchange`、指针与按键手势、滚动与缩放,只在快照确实变化时发布。只有落在对话记录内的选区才可引用。锚点是最后一行中面积非零的那个盒子:浏览器会为行尾换行吐出一个零尺寸矩形,锚到它上面会让控件被钉在视口左上角。
68
+ - **选区追踪。** 一个文档级来源监听 `selectionchange`、指针与按键手势、滚动与缩放,只在快照确实变化时发布。只有落在对话记录内的选区才可引用。
69
+ - **锚点只从选中的「文字」量取,绝不用 `Range.getClientRects()` 的原始列表。** 那个方法返回的**不是**选区的矩形。Blink 从两处拼出来:每个被部分选中的文字节点的行盒,**以及每个被 range 完整包含的元素的 border-box**。后一种根本不是高亮的一部分,正是它搞坏了定位。选 agent 回复的最后一段就是典型:鼠标只能落到段落下方的空白和操作栏上,于是端点停在某个容器元素上,它后面的所有兄弟节点都被"完整包含"。这时 range 会报告操作栏上 28×28 的按钮盒和一整列宽的块盒——实测把锚点的 bottom 从 548 顶到 599,按钮于是翻到选区上方。修法是遍历与 range 相交的**文字节点**(`TreeWalker` + `range.intersectsNode`),逐个量它自己那一段,这也是选区工具条的既有做法:元素 border-box 从源头就进不来。落在控件内的矩形(`button`、`[role=button]`、表单元素)会被跳过,因为拖进操作栏时端点会落进控件的文字标签——「用量 423K tok」就是个文字节点。更早的版本挂在"最后一行"上,而**最后一行无法从 rect 列表里可靠地还原**:在 Web shell 里实测的一次选中,7 个 rect 按文档顺序返回,最宽的那个(185px,右边缘 x=1078)出现在**最上面**,而视觉上最低的一条线被切成 28px、28px、15px 三个碎片。取"最后一个 rect"会锚到一个碎片、控件落到段落中间。文字 rect 的并集没有顺序可猜错,而且对一行、折行段落、跨多个块、带行内 chip 都精确成立。
70
+ - **定位默认在选区上方,且限制在消息区内。** 选区上方那一行是你已经读过的,下方那一行是你正在读的——而控件会盖住它所在的那一侧。上方还能避开你刚选中的文字:拖选结束时鼠标停在选区底边,放下方正好压在你盯着的那一行上。上方放不下时改放下方。两个边界都重要:原来的做法夹到视口底边,控件停在输入框上方、离它引用的文字十万八千里;夹到 `innerWidth` 又会让它贴到窗口最右边。现在的区域是对话记录自己的盒子,并在输入框卡片的上边缘截断。
71
+ - **锚点跟随布局,而不只是选区。** 一个 `ResizeObserver` 盯着对话记录和输入框卡片:草稿换行、面板缩放、虚拟列表回收行——这三件事都不触发选区事件。拿着旧 rect 定位,正是"选中最后一段后按钮压在输入框上"的成因。
67
72
  - **坐标系由插件自己拥有。** 控件渲染进一个由插件创建的零尺寸宿主,它是 `<html>` 的直接子节点,并用 `position: fixed` 钉在视口原点上。其中的按钮是 `position: absolute`,因此它的坐标就是真正的视口坐标——与 `getBoundingClientRect()` 报告选区时所用的空间一致。宿主之上的任何东西都无法重新解释这些坐标:祖先上的 `position`、`transform`、`filter`、`perspective`、`contain` 都会改变 `fixed` 的解析基准,而文档元素没有祖先。宿主在首次渲染时就创建(而不是在 effect 里),所以第一帧就已经落在正确的空间里。
68
73
  - **写入草稿。** 控件调用 composer 自己的公开 `inputActions.setDraft()`。它自己不读取也不持有任何状态——草稿仍然存在它原本该在的地方。
69
74
  - **焦点交接。** `setDraft` 会把编辑器的选区留在草稿末尾,但不移动 DOM 焦点,所以控件自己聚焦 composer 的编辑器,并把光标收拢到末尾。要聚焦的卡片取自控件自己的 slot 座位,而不是在文档里搜出来的:按钮现在不在卡片里了,否则一条已结束消息的编辑器会先被找到。
70
- - **对比度固定,不跟随主题。** 控件的底色与文字硬编码为黑底白字,而不是取自主题令牌。它浮在并不属于它的对话文字之上,必须在其背后的任何内容上都保持可读;跟随主题的一对颜色会随宿主主题翻转,可能落成浅底浅字或深底深字。边框与阴影仍走主题,因为它们只负责柔化边缘。这也是为什么控件不需要宿主定义任何特定令牌就能保持可读。
75
+ - **对比度固定,不跟随主题。** 控件的底色与文字硬编码,而不是取自主题令牌。它浮在并不属于它的对话文字之上,必须在其背后的任何内容上都保持可读;跟随主题的一对颜色会随宿主主题翻转,可能落成浅底浅字或深底深字。底色用的是宿主自己的近黑(`#1b1b1c`,即它的 `neutral-bluish-900`),让这个芯片属于产品自己的色板;引号图标用品牌蓝;描边是固定的白色发丝边而不是主题令牌——在深色对话记录上,这圈边才是它和背景的分界。阴影同理固定。
71
76
 
72
77
  ## 兼容性
73
78
 
@@ -87,7 +92,7 @@ DeepSeek Harness 的插件 API 是 **pre-stable** 的:下面这些东西都不
87
92
 
88
93
  ## 已知限制
89
94
 
90
- - **控件会盖住选区下方那一行。** 定位就是相对选区最后一行的一个固定偏移,所以按钮会压在它后面的内容上。另一个选择——预留布局空间——会让对话记录在每次选区变化时重排。
95
+ - **控件会盖住相邻的那一行**(默认是上方那行,上方放不下时是下方那行),所以按钮会压在已经显示的内容上。另一个选择——预留布局空间——会让对话记录在每次选区变化时重排。
91
96
  - **只能追加。** composer 的公开 API 提供整段替换,没有"在光标处插入"的动词,所以引用总是落在草稿末尾。
92
97
  - **仅纯文本。** 引用带围栏的代码块会丢掉围栏,引用表格会丢掉单元格分隔,因为浏览器返回的是渲染后的文字。
93
98
  - **只覆盖对话记录。** trajectory 与 waterfall 视图是另外的界面,其中的选区不提供任何操作。
@@ -100,8 +105,11 @@ npm install
100
105
  npm test # 单元测试 + 真实 Cordis 上下文的注册测试
101
106
  npm run typecheck
102
107
  npm run build # lib/index.js(host 半边)+ lib/client.js(浏览器半边)
108
+ npm run sync # 构建,然后把 lib/ 复制进已安装的 Web profile
103
109
  ```
104
110
 
111
+ `npm run sync` 是为了绕开一个坑:profile 的 `cordis.patch.yml` 写的是**包名**,所以 Web shell 加载的是 profile `node_modules` 里的 `lib/client.js`,不是这个 checkout 里的。只跑 `npm run build` 页面看不到任何变化。`npm run sync` 把构建产物复制过去(默认 `~/.dsh/profiles/web`,可用 `$DSH_PROFILE_DIR` 或参数覆盖)。
112
+
105
113
  `lib/client.js` 不是普通 bundle:Web shell 通过闭包工厂契约挂载插件,所以构建会把浏览器半边包成 `window.__ModuleLoader__.load({ id, factory })`,并让 React 等平台模块走注入的 `require` 而不是内联进来。整个构建就是 `scripts/build.mjs`,前面挂了一个很小的 CSS Modules 编译器。
106
114
 
107
115
  ### CSS Modules 编译器
@@ -113,6 +121,16 @@ npm run build # lib/index.js(host 半边)+ lib/client.js(浏览器半
113
121
 
114
122
  `tests/build.client.spec.ts` 针对**构建产物**断言这两点,因为它们在源码层面都看不出来。
115
123
 
124
+ ### 真机探针
125
+
126
+ 选区几何是真实布局的事实,jsdom 不建模,所以有意思的 bug 只存在于浏览器里。`tools/` 下有三个探针,都通过 CDP 驱动真实 Chrome 打到运行中的应用(`http://127.0.0.1:3080/`),把 JSON 写到仓库根目录(已 git-ignore):
127
+
128
+ - **`probe-real-drag.mjs`** —— 用真实鼠标拖拽(`Input.dispatchMouseEvent`)划过 agent 回复的最后一段,打印 range 的原始 rect、文字 rect、以及控件自己的 rect。**这是唯一能复现该 bug 的探针**:程序化 `setStart/setEnd` 永远不会产生真实拖拽才会有的元素 border-box。
129
+ - **`dump-live-page.mjs`** —— 遍历真实会话里每个可选文字节点,比对控件位置与定位公式,标出任何不一致。用来做跨大量选区的回归检查。
130
+ - **`probe-quote-geometry.mjs`** —— 自建合成对话记录并加载真实 bundle,用于隔离测量控件。
131
+
132
+ `docs/debugging-notes.md` 记录了这些探针是为了找什么。一句话版:`Range.getClientRects()` **不是**"选区的矩形"——它还会报告每个被 range 完整包含的元素的 border-box,而"拖过段落末尾"恰好产生这种 range,定位问题因此拖了很久。
133
+
116
134
  ## 许可证
117
135
 
118
136
  MIT
package/lib/client.js CHANGED
@@ -35,12 +35,48 @@ module.exports = __toCommonJS(index_exports);
35
35
  var import_react2 = require("react");
36
36
 
37
37
  // src/client/placement.ts
38
- function placeQuoteControl(anchor, panel, viewport, gap = 6, margin = 8) {
38
+ function quoteRegion(transcript, composer, viewport) {
39
+ const window2 = { left: 0, top: 0, right: viewport.width, bottom: viewport.height };
40
+ const measured = (box) => box !== null && box.right > box.left && box.bottom > box.top ? box : null;
41
+ const base = measured(transcript) ?? window2;
42
+ const card = measured(composer);
39
43
  return {
40
- left: clamp(anchor.right - panel.width, margin, viewport.width - panel.width - margin),
41
- top: clamp(anchor.bottom + gap, margin, viewport.height - panel.height - margin)
44
+ left: Math.max(base.left, window2.left),
45
+ right: Math.min(base.right, window2.right),
46
+ top: Math.max(base.top, window2.top),
47
+ bottom: Math.min(card === null ? base.bottom : card.top, window2.bottom)
42
48
  };
43
49
  }
50
+ var MIN_RECT_WIDTH = 1;
51
+ var MIN_RECT_HEIGHT = 4;
52
+ function selectionBox(rects) {
53
+ let left = Infinity;
54
+ let right = -Infinity;
55
+ let top = Infinity;
56
+ let bottom = -Infinity;
57
+ let any = false;
58
+ for (const rect of rects) {
59
+ if (rect.right - rect.left < MIN_RECT_WIDTH) continue;
60
+ if (rect.bottom - rect.top < MIN_RECT_HEIGHT) continue;
61
+ any = true;
62
+ if (rect.left < left) left = rect.left;
63
+ if (rect.right > right) right = rect.right;
64
+ if (rect.top < top) top = rect.top;
65
+ if (rect.bottom > bottom) bottom = rect.bottom;
66
+ }
67
+ return any ? { left, right, top, bottom } : null;
68
+ }
69
+ function placeQuoteControl(anchor, panel, viewport, region = { left: 0, top: 0, right: viewport.width, bottom: viewport.height }, gap = 6, margin = 8) {
70
+ return {
71
+ left: clamp(anchor.right - panel.width, region.left + margin, region.right - panel.width - margin),
72
+ top: clamp(vertical(anchor, panel, region, gap, margin), region.top + margin, region.bottom - panel.height - margin)
73
+ };
74
+ }
75
+ function vertical(anchor, panel, region, gap, margin) {
76
+ const above = anchor.top - gap - panel.height;
77
+ if (above >= region.top + margin) return above;
78
+ return anchor.bottom + gap;
79
+ }
44
80
  function clamp(value, min, max) {
45
81
  return Math.min(Math.max(value, min), Math.max(min, max));
46
82
  }
@@ -94,10 +130,226 @@ function ViewportLayer({ children }) {
94
130
  return (0, import_react_dom.createPortal)(children, host);
95
131
  }
96
132
 
133
+ // src/client/selection.ts
134
+ var NO_SELECTION = { text: "", anchor: null };
135
+ function short(value) {
136
+ return `${Math.round(value * 10) / 10}`;
137
+ }
138
+ var TRANSCRIPT = "[data-conversation-scroll]";
139
+ var COMPOSER = "[data-composer-card]";
140
+ function elementOf(node) {
141
+ return node instanceof Element ? node : node.parentElement;
142
+ }
143
+ var CONTROL = 'button, [role="button"], input, select, textarea, summary';
144
+ var SHOW_TEXT = 4;
145
+ function textRectsOf(range, doc) {
146
+ const probe = doc.createRange();
147
+ if (typeof probe.getClientRects !== "function" || typeof range.intersectsNode !== "function") return null;
148
+ const root = elementOf(range.commonAncestorContainer);
149
+ if (root === null) return null;
150
+ const out = [];
151
+ const walker = doc.createTreeWalker(root, SHOW_TEXT);
152
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
153
+ if (!range.intersectsNode(node)) continue;
154
+ const parent = node.parentElement;
155
+ if (parent === null) continue;
156
+ if (parent.closest(CONTROL) !== null) continue;
157
+ if (parent.closest(COMPOSER) !== null) continue;
158
+ const slice = doc.createRange();
159
+ slice.selectNodeContents(node);
160
+ if (node === range.startContainer) slice.setStart(node, range.startOffset);
161
+ if (node === range.endContainer) slice.setEnd(node, range.endOffset);
162
+ for (const rect of slice.getClientRects()) out.push(rect);
163
+ }
164
+ return out;
165
+ }
166
+ function anchorOf(range, doc) {
167
+ const text = textRectsOf(range, doc);
168
+ if (text !== null) {
169
+ const box = selectionBox(text);
170
+ if (box !== null) return box;
171
+ }
172
+ const rects = typeof range.getClientRects === "function" ? range.getClientRects() : null;
173
+ if (rects !== null) {
174
+ const box = selectionBox(Array.from(rects));
175
+ if (box !== null) return box;
176
+ }
177
+ const bound = typeof range.getBoundingClientRect === "function" ? range.getBoundingClientRect() : null;
178
+ return bound === null ? null : selectionBox([bound]);
179
+ }
180
+ function sameAnchor(left, right) {
181
+ return left.left === right.left && left.right === right.right && left.top === right.top && left.bottom === right.bottom;
182
+ }
183
+ function sameSelection(left, right) {
184
+ if (left.text !== right.text) return false;
185
+ if (left.anchor === null || right.anchor === null) return left.anchor === right.anchor;
186
+ return sameAnchor(left.anchor, right.anchor);
187
+ }
188
+ function readSelection(doc) {
189
+ const selection = doc.getSelection();
190
+ if (selection === null || selection.isCollapsed || selection.rangeCount === 0) return NO_SELECTION;
191
+ const text = selection.toString().trim();
192
+ if (text === "") return NO_SELECTION;
193
+ const range = selection.getRangeAt(0);
194
+ const host = elementOf(range.commonAncestorContainer);
195
+ if (host === null) return NO_SELECTION;
196
+ if (host.closest(TRANSCRIPT) === null) return NO_SELECTION;
197
+ if (host.closest(COMPOSER) !== null) return NO_SELECTION;
198
+ const anchor = anchorOf(range, doc);
199
+ if (anchor === null) return NO_SELECTION;
200
+ return { text, anchor };
201
+ }
202
+ var REPORT_HTML_LIMIT = 1200;
203
+ var REPORT_DEPTH = 6;
204
+ function selectionReport(doc, control) {
205
+ const selection = doc.getSelection();
206
+ const report = {};
207
+ if (selection === null || selection.rangeCount === 0) {
208
+ report.selection = null;
209
+ } else {
210
+ const range = selection.getRangeAt(0);
211
+ const rects = typeof range.getClientRects === "function" ? Array.from(range.getClientRects()) : [];
212
+ const bound = typeof range.getBoundingClientRect === "function" ? range.getBoundingClientRect() : null;
213
+ const ancestor = elementOf(range.commonAncestorContainer);
214
+ report.selection = {
215
+ text: selection.toString(),
216
+ // The raw engine list, for comparison against `textUnion`: when they
217
+ // disagree, `getClientRects` reported an element's border box because the
218
+ // range fully contains it, and that is the shape that breaks placement.
219
+ rects: rects.map((rect) => ({
220
+ left: short(rect.left),
221
+ right: short(rect.right),
222
+ top: short(rect.top),
223
+ bottom: short(rect.bottom),
224
+ width: short(rect.right - rect.left),
225
+ height: short(rect.bottom - rect.top)
226
+ })),
227
+ bounding: bound === null ? null : {
228
+ left: short(bound.left),
229
+ right: short(bound.right),
230
+ top: short(bound.top),
231
+ bottom: short(bound.bottom)
232
+ },
233
+ union: selectionBox(rects),
234
+ // The union of the TEXT rects, which is what the anchor actually uses.
235
+ textUnion: selectionBox(textRectsOf(range, doc) ?? []),
236
+ // The visual end of the selection as the engine places a caret there, for
237
+ // comparison against the union: a disagreement between the two is a
238
+ // phantom rect inside the union.
239
+ endCaret: caretBox(range),
240
+ start: endpoint(range.startContainer, range.startOffset),
241
+ end: endpoint(range.endContainer, range.endOffset),
242
+ ancestors: ancestorsOf(ancestor),
243
+ html: ancestor === null ? null : ancestor.outerHTML.slice(0, REPORT_HTML_LIMIT)
244
+ };
245
+ }
246
+ report.control = control === null ? null : controlBox(control);
247
+ const view = doc.defaultView;
248
+ report.viewport = view === null ? null : { width: view.innerWidth, height: view.innerHeight };
249
+ return JSON.stringify(report, null, 2);
250
+ }
251
+ function caretBox(range) {
252
+ try {
253
+ const collapsed = range.cloneRange();
254
+ collapsed.collapse(false);
255
+ const rects = typeof collapsed.getClientRects === "function" ? collapsed.getClientRects() : null;
256
+ const first = rects === null ? void 0 : rects[0];
257
+ if (first === void 0) return null;
258
+ return {
259
+ left: short(first.left),
260
+ right: short(first.right),
261
+ top: short(first.top),
262
+ bottom: short(first.bottom)
263
+ };
264
+ } catch {
265
+ return null;
266
+ }
267
+ }
268
+ function endpoint(node, offset) {
269
+ if (node.nodeType === 3) {
270
+ return {
271
+ node: "#text",
272
+ offset,
273
+ tail: (node.nodeValue ?? "").slice(-24),
274
+ atEnd: offset === (node.nodeValue ?? "").length
275
+ };
276
+ }
277
+ return {
278
+ node: node.nodeName,
279
+ offset,
280
+ tail: null,
281
+ atEnd: offset === node.childNodes.length
282
+ };
283
+ }
284
+ function ancestorsOf(element) {
285
+ const chain = [];
286
+ for (let node = element; node !== null && chain.length < REPORT_DEPTH; node = node.parentElement) {
287
+ const marker = Array.from(node.attributes).filter((attribute) => attribute.name.startsWith("data-") || attribute.name === "class").map((attribute) => `${attribute.name}="${attribute.value}"`).join(" ");
288
+ chain.push(`${node.tagName.toLowerCase()}${marker === "" ? "" : ` ${marker}`}`);
289
+ }
290
+ return chain;
291
+ }
292
+ function controlBox(control) {
293
+ const rect = control.getBoundingClientRect();
294
+ return {
295
+ rect: {
296
+ left: short(rect.left),
297
+ right: short(rect.right),
298
+ top: short(rect.top),
299
+ bottom: short(rect.bottom)
300
+ },
301
+ inline: control instanceof HTMLElement ? { left: control.style.left, top: control.style.top } : null
302
+ };
303
+ }
304
+ function createQuoteSelectionSource(doc) {
305
+ let snapshot = NO_SELECTION;
306
+ const listeners = /* @__PURE__ */ new Set();
307
+ const publish = () => {
308
+ const next = readSelection(doc);
309
+ if (sameSelection(snapshot, next)) return;
310
+ snapshot = next;
311
+ for (const listener of listeners) listener();
312
+ };
313
+ const win = doc.defaultView;
314
+ doc.addEventListener("selectionchange", publish);
315
+ doc.addEventListener("pointerup", publish);
316
+ doc.addEventListener("keyup", publish);
317
+ win?.addEventListener("scroll", publish, true);
318
+ win?.addEventListener("resize", publish);
319
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
320
+ publish();
321
+ });
322
+ for (const selector of [TRANSCRIPT, COMPOSER]) {
323
+ const element = doc.querySelector(selector);
324
+ if (element !== null) observer?.observe(element);
325
+ }
326
+ return {
327
+ source: {
328
+ getSnapshot: () => snapshot,
329
+ subscribe: (listener) => {
330
+ listeners.add(listener);
331
+ return () => {
332
+ listeners.delete(listener);
333
+ };
334
+ }
335
+ },
336
+ dispose: () => {
337
+ doc.removeEventListener("selectionchange", publish);
338
+ doc.removeEventListener("pointerup", publish);
339
+ doc.removeEventListener("keyup", publish);
340
+ win?.removeEventListener("scroll", publish, true);
341
+ win?.removeEventListener("resize", publish);
342
+ observer?.disconnect();
343
+ listeners.clear();
344
+ snapshot = NO_SELECTION;
345
+ }
346
+ };
347
+ }
348
+
97
349
  // src/client/QuoteControl.module.css
98
- var css = "._3OiQSq_control{z-index:60;border:.5px solid var(--dsw-alias-border-l1);color:#fff;white-space:nowrap;cursor:pointer;box-shadow:0 4px 12px var(--dsw-alias-bg-mask-2);background:#000;border-radius:8px;align-items:center;padding:5px 10px;font-family:inherit;font-size:12px;line-height:16px;transition:transform .12s,box-shadow .12s;display:inline-flex;position:absolute}._3OiQSq_control:hover{box-shadow:0 8px 20px var(--dsw-alias-bg-mask-2);transform:translateY(-2px)}";
350
+ var css = "._3OiQSq_control{z-index:60;color:#fff;letter-spacing:.01em;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;background:#1b1b1c;border:1px solid #ffffff24;border-radius:8px;align-items:center;gap:4px;min-height:28px;padding:0 10px 0 7px;font-family:inherit;font-size:12px;font-weight:500;line-height:16px;transition:transform .12s,box-shadow .12s,background-color .12s;display:inline-flex;position:absolute;box-shadow:0 1px 2px #0000003d,0 6px 20px #00000047}._3OiQSq_control>._3OiQSq_mark{color:var(--dsw-static-deepseek-400,#679efe);flex:none}._3OiQSq_control:hover{background:#2c2c2e;transform:translateY(-2px);box-shadow:0 1px 2px #0000003d,0 10px 28px #00000057}._3OiQSq_control:active{background:#353638;transform:translateY(0);box-shadow:0 1px 2px #0000003d,0 2px 8px #0000003d}._3OiQSq_control:focus-visible{outline:2px solid var(--dsw-static-deepseek-400,#679efe);outline-offset:2px}@media (prefers-reduced-motion:reduce){._3OiQSq_control{transition:none}._3OiQSq_control:hover,._3OiQSq_control:active{transform:none}}";
99
351
  var styleKey = "@walkerxie/dsh-plugin-quote/QuoteControl.module.css";
100
- var tagId = "@walkerxie/dsh-plugin-quote/QuoteControl.module.css#aa03c92b";
352
+ var tagId = "@walkerxie/dsh-plugin-quote/QuoteControl.module.css#8edd12ad";
101
353
  if (typeof document !== "undefined") {
102
354
  for (const stale of document.querySelectorAll("style[data-plugin-css]")) {
103
355
  const id = stale.dataset.pluginCss ?? "";
@@ -111,7 +363,7 @@ if (typeof document !== "undefined" && document.querySelector('style[data-plugin
111
363
  tag.textContent = css;
112
364
  document.head.appendChild(tag);
113
365
  }
114
- var QuoteControl_default = { "control": "_3OiQSq_control" };
366
+ var QuoteControl_default = { "control": "_3OiQSq_control", "mark": "_3OiQSq_mark" };
115
367
 
116
368
  // src/client/QuoteControl.tsx
117
369
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -145,21 +397,41 @@ function QuoteControl({ useQuoteSelection, useInput, inputActions, t }) {
145
397
  document.removeEventListener("keydown", onKeyDown);
146
398
  };
147
399
  }, [shown, selection]);
400
+ (0, import_react2.useEffect)(() => {
401
+ if (!shown || panelRef.current === null) return;
402
+ const button = panelRef.current;
403
+ const onKeyDown = (event) => {
404
+ if (event.key.toLowerCase() !== "c" || !event.ctrlKey || !event.shiftKey) return;
405
+ event.preventDefault();
406
+ const json = selectionReport(document, button);
407
+ try {
408
+ void navigator.clipboard?.writeText(json);
409
+ } catch {
410
+ }
411
+ };
412
+ document.addEventListener("keydown", onKeyDown);
413
+ return () => {
414
+ document.removeEventListener("keydown", onKeyDown);
415
+ };
416
+ }, [shown]);
148
417
  if (!shown || anchor === null) return null;
149
- const position = placeQuoteControl(anchor, size, {
150
- width: window.innerWidth,
151
- height: window.innerHeight
152
- });
418
+ const position = placeQuoteControl(
419
+ anchor,
420
+ size,
421
+ { width: window.innerWidth, height: window.innerHeight },
422
+ messageRegion()
423
+ );
153
424
  const style = { ...position, pointerEvents: "auto" };
154
425
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
155
426
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ref: seatRef, hidden: true }),
156
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewportLayer, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
427
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewportLayer, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
157
428
  "button",
158
429
  {
159
430
  ref: panelRef,
160
431
  type: "button",
161
432
  className: QuoteControl_default.control,
162
433
  style,
434
+ "data-quote-control": "",
163
435
  "aria-label": t("action.quote"),
164
436
  onMouseDown: (event) => {
165
437
  event.preventDefault();
@@ -169,11 +441,40 @@ function QuoteControl({ useQuoteSelection, useInput, inputActions, t }) {
169
441
  window.getSelection()?.removeAllRanges();
170
442
  focusComposerEnd(seatRef.current?.closest("[data-composer-card]") ?? null);
171
443
  },
172
- children: t("action.quote")
444
+ children: [
445
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
446
+ "svg",
447
+ {
448
+ className: QuoteControl_default.mark,
449
+ width: "13",
450
+ height: "13",
451
+ viewBox: "0 0 16 16",
452
+ "aria-hidden": "true",
453
+ focusable: "false",
454
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
455
+ "path",
456
+ {
457
+ fill: "currentColor",
458
+ d: "M6.2 2.8C3.8 4 2.5 5.9 2.5 8.5V13h5.1V8.2H4.9c.1-1.5.8-2.6 2.1-3.4l-.8-2Zm7 0C10.8 4 9.5 5.9 9.5 8.5V13h5.1V8.2h-2.7c.1-1.5.8-2.6 2.1-3.4l-.8-2Z"
459
+ }
460
+ )
461
+ }
462
+ ),
463
+ t("action.quote")
464
+ ]
173
465
  }
174
466
  ) })
175
467
  ] });
176
468
  }
469
+ function messageRegion() {
470
+ const transcript = document.querySelector("[data-conversation-scroll]");
471
+ const composer = document.querySelector("[data-composer-card]");
472
+ return quoteRegion(
473
+ transcript === null ? null : transcript.getBoundingClientRect(),
474
+ composer === null ? null : composer.getBoundingClientRect(),
475
+ { width: window.innerWidth, height: window.innerHeight }
476
+ );
477
+ }
177
478
  function focusComposerEnd(card) {
178
479
  const editable = card?.querySelector("[data-composer-input]");
179
480
  if (editable === null || editable === void 0) return;
@@ -186,90 +487,6 @@ function focusComposerEnd(card) {
186
487
  selection?.addRange(range);
187
488
  }
188
489
 
189
- // src/client/selection.ts
190
- var NO_SELECTION = { text: "", anchor: null };
191
- var TRANSCRIPT = "[data-conversation-scroll]";
192
- var COMPOSER = "[data-composer-card]";
193
- function elementOf(node) {
194
- return node instanceof Element ? node : node.parentElement;
195
- }
196
- function boxOf(rect) {
197
- return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom };
198
- }
199
- function lastRect(range) {
200
- const rects = typeof range.getClientRects === "function" ? range.getClientRects() : null;
201
- let last = null;
202
- if (rects !== null) {
203
- for (const rect of rects) {
204
- if (rect.width <= 0 && rect.height <= 0) continue;
205
- last = boxOf(rect);
206
- }
207
- }
208
- if (last !== null) return last;
209
- if (typeof range.getBoundingClientRect !== "function") return null;
210
- const bound = range.getBoundingClientRect();
211
- if (bound.width <= 0 && bound.height <= 0) return null;
212
- return boxOf(bound);
213
- }
214
- function sameAnchor(left, right) {
215
- return left.left === right.left && left.right === right.right && left.top === right.top && left.bottom === right.bottom;
216
- }
217
- function sameSelection(left, right) {
218
- if (left.text !== right.text) return false;
219
- if (left.anchor === null || right.anchor === null) return left.anchor === right.anchor;
220
- return sameAnchor(left.anchor, right.anchor);
221
- }
222
- function readSelection(doc) {
223
- const selection = doc.getSelection();
224
- if (selection === null || selection.isCollapsed || selection.rangeCount === 0) return NO_SELECTION;
225
- const text = selection.toString().trim();
226
- if (text === "") return NO_SELECTION;
227
- const range = selection.getRangeAt(0);
228
- const host = elementOf(range.commonAncestorContainer);
229
- if (host === null) return NO_SELECTION;
230
- if (host.closest(TRANSCRIPT) === null) return NO_SELECTION;
231
- if (host.closest(COMPOSER) !== null) return NO_SELECTION;
232
- const anchor = lastRect(range);
233
- if (anchor === null) return NO_SELECTION;
234
- return { text, anchor };
235
- }
236
- function createQuoteSelectionSource(doc) {
237
- let snapshot = NO_SELECTION;
238
- const listeners = /* @__PURE__ */ new Set();
239
- const publish = () => {
240
- const next = readSelection(doc);
241
- if (sameSelection(snapshot, next)) return;
242
- snapshot = next;
243
- for (const listener of listeners) listener();
244
- };
245
- const win = doc.defaultView;
246
- doc.addEventListener("selectionchange", publish);
247
- doc.addEventListener("pointerup", publish);
248
- doc.addEventListener("keyup", publish);
249
- win?.addEventListener("scroll", publish, true);
250
- win?.addEventListener("resize", publish);
251
- return {
252
- source: {
253
- getSnapshot: () => snapshot,
254
- subscribe: (listener) => {
255
- listeners.add(listener);
256
- return () => {
257
- listeners.delete(listener);
258
- };
259
- }
260
- },
261
- dispose: () => {
262
- doc.removeEventListener("selectionchange", publish);
263
- doc.removeEventListener("pointerup", publish);
264
- doc.removeEventListener("keyup", publish);
265
- win?.removeEventListener("scroll", publish, true);
266
- win?.removeEventListener("resize", publish);
267
- listeners.clear();
268
- snapshot = NO_SELECTION;
269
- }
270
- };
271
- }
272
-
273
490
  // src/client/locales.ts
274
491
  var zh = {
275
492
  "action.quote": "\u5F15\u7528\u9009\u4E2D\u6587\u5B57"
package/lib/client.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/client/index.ts", "../src/client/QuoteControl.tsx", "../src/client/placement.ts", "../src/client/quote-text.ts", "../src/client/layer.tsx", "../src/client/QuoteControl.module.css", "../src/client/selection.ts", "../src/client/locales.ts"],
4
- "sourcesContent": ["/**\n * Quote surface plugin, browser half: one control in the composer overlay that\n * turns the current conversation text selection into a markdown blockquote\n * appended to the draft. The selection is tracked once for the plugin fiber and\n * published as an inject-face hook source; the draft is read and written\n * through the framework's Session standard shares, so this plugin owns no\n * composer state and adds no model-visible vocabulary.\n */\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: pulls the conversation overlay slot, the Session standard shares\n// (useInput/inputActions), and their declaration merges into the Client program.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the renderer-owned slots service.\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type { QuoteInjected } from './slots.ts'\nimport { QuoteControl } from './QuoteControl.tsx'\nimport { createQuoteSelectionSource } from './selection.ts'\nimport { en, zh, type QuoteKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The quote control's copy. */\n quote: QuoteKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'quote'\n\n/** Required services: the slot registry and the plugin's own dictionaries. */\nexport const inject = ['slots', 'locale']\n\n/**\n * Client plugin body: the overlay entry over one document-level selection source.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-quote: dictionaries')\n\n // One source per plugin fiber, not per session binding: the selection is a\n // document fact, and a per-session source would leak a listener set per\n // session switch.\n const selection = createQuoteSelectionSource(document)\n ctx.effect(() => selection.dispose, 'ui-quote: selection tracking')\n\n ctx.slots.inject('conversation.input.overlay', () => ctx.slots.register({\n name: 'conversation.input.overlay',\n id: 'quote',\n order: 20,\n locale: NS,\n inject: (): QuoteInjected => ({ hooks: { quoteSelection: selection.source } }),\n }, QuoteControl))\n}\n", "/**\n * The floating quote control: a button placed beside the conversation text\n * selection. Visibility, placement, and the appended blockquote all derive from\n * the injected selection hook and the framework's own composer shares; the\n * component holds no subscription machinery and never touches ctx. The button\n * itself renders into the viewport layer, because the composer card this slot\n * lives in is a containing block for fixed positioning.\n */\n\nimport { useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { QuoteInjected } from './slots.ts'\nimport type { QuoteAnchor, PanelSize } from './placement.ts'\nimport { placeQuoteControl } from './placement.ts'\nimport { composeQuote } from './quote-text.ts'\nimport { ViewportLayer } from './layer.tsx'\nimport css from './QuoteControl.module.css'\n\n/** Full props of the overlay entry: overlay owner share + injected selection hook + the locale seat. */\nexport type QuoteControlProps =\n import('@deepseek-ai/dsh-client-ui-slots').PropsRuntime<'conversation.input.overlay'>\n & InjectFace<QuoteInjected>\n & PropsLocale<'quote'>\n\n/** Size published before the first layout pass, where the clamps are skipped. */\nconst UNMEASURED: PanelSize = { width: 0, height: 0 }\n\n/**\n * The quote control. It renders nothing while the selection is empty, while\n * the user has dismissed the current selection, or on hosts whose composer\n * exposes no draft actions \u2014 the button never appears as a dead control.\n * @param props - the overlay entry's derived shares plus the injected hook.\n */\nexport function QuoteControl({ useQuoteSelection, useInput, inputActions, t }: QuoteControlProps) {\n const selection = useQuoteSelection(snapshot => snapshot)\n const draft = useInput(state => state.draft)\n const panelRef = useRef<HTMLButtonElement | null>(null)\n // The button renders in the viewport layer, so the composer card it belongs to\n // is no longer an ancestor of it. The card is resolved through this seat, which\n // stays in the slot's own position: React attaches refs only after it has moved\n // the button into the portal, so by then the button's ancestor chain is already\n // the document root.\n const seatRef = useRef<HTMLSpanElement | null>(null)\n const [size, setSize] = useState<PanelSize>(UNMEASURED)\n // Dismissal is keyed by snapshot identity: the source republishes only when\n // the selection moves, so a new selection shows the control again.\n const [dismissed, setDismissed] = useState<unknown>(null)\n\n const anchor: QuoteAnchor | null = selection.anchor\n const quotable = selection.text !== '' && anchor !== null\n const shown = quotable && dismissed !== selection\n\n // The panel is measured in the same commit that mounts it, so the clamps use\n // real dimensions before anything paints. jsdom reports zero offset sizes,\n // which leaves the unmeasured size in place and is why the clamps themselves\n // are covered through placeQuoteControl. The effect re-runs on every snapshot\n // while the panel is still unmeasured, because a size of zero is not a\n // measurement: it would right-align the control to the selection's right edge\n // and skip the bottom clamp entirely.\n useLayoutEffect(() => {\n if (!shown) return\n const panel = panelRef.current\n /* v8 ignore next -- the panel is mounted in the same commit that reveals the control. */\n if (panel === null) return\n const width = panel.offsetWidth\n const height = panel.offsetHeight\n if (width === 0 && height === 0) return\n if (width !== size.width || height !== size.height) setSize({ width, height })\n }, [shown, selection, size.width, size.height])\n\n // Escape hides the control without clearing the browser selection, which is\n // the user's own to keep.\n useEffect(() => {\n if (!shown) return\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') setDismissed(selection)\n }\n document.addEventListener('keydown', onKeyDown)\n return () => { document.removeEventListener('keydown', onKeyDown) }\n }, [shown, selection])\n\n if (!shown || anchor === null) return null\n\n const position = placeQuoteControl(anchor, size, {\n width: window.innerWidth,\n height: window.innerHeight,\n })\n\n // The layer host is a pure coordinate frame with `pointer-events: none`, so the\n // control re-enables them inline: this must hold even if the plugin's\n // stylesheet fails to load or is overridden.\n const style = { ...position, pointerEvents: 'auto' as const }\n\n return (\n <>\n {/* The seat: this component's own position in the composer card, the one\n place the owning card can still be read from once the button is\n portalled away. It carries no box, so the card's layout is untouched. */}\n <span ref={seatRef} hidden />\n <ViewportLayer>\n <button\n ref={panelRef}\n type=\"button\"\n className={css.control}\n style={style}\n aria-label={t('action.quote')}\n // The press must not collapse the selection: the click that follows would\n // otherwise land on a control that a selectionchange already unmounted.\n onMouseDown={event => { event.preventDefault() }}\n onClick={() => {\n inputActions.setDraft(composeQuote(draft, selection.text))\n // Drop the selection so the control leaves with it instead of lingering\n // over text that already reached the draft.\n window.getSelection()?.removeAllRanges()\n focusComposerEnd(seatRef.current?.closest('[data-composer-card]') ?? null)\n }}\n >\n {t('action.quote')}\n </button>\n </ViewportLayer>\n </>\n )\n}\n\n/**\n * Hand DOM focus to the composer that owns this control and collapse the caret\n * at the end of the draft. `setDraft` already put Lexical's selection there,\n * but Lexical skips DOM selection reconciliation while its editor is unfocused,\n * and a browser focusing an editor whose DOM selection sits elsewhere puts the\n * caret at the element's start.\n * @param card - the composer card holding this control's slot seat, resolved\n * through the seat because the button itself now lives in the viewport layer.\n */\nfunction focusComposerEnd(card: Element | null): void {\n const editable = card?.querySelector<HTMLElement>('[data-composer-input]')\n if (editable === null || editable === undefined) return\n editable.focus({ preventScroll: true })\n const range = document.createRange()\n range.selectNodeContents(editable)\n range.collapse(false)\n const selection = window.getSelection()\n selection?.removeAllRanges()\n selection?.addRange(range)\n}\n", "/**\n * Placement math for the floating quote control. The anchor is a selection\n * rect, not an element, so the shared element-anchored position hook does not\n * apply; this keeps the arithmetic pure so jsdom (which reports no layout) can\n * still cover the viewport clamps.\n *\n * The result is in VIEWPORT coordinates, matching the rect the anchor came\n * from: the control is absolutely positioned inside a layer host pinned to the\n * viewport origin, so no scroll offset is involved.\n * @module @deepseek-ai/dsh-client-ui-quote/placement\n */\n\n/** Viewport-relative box of the selection's last line, as the DOM reports it. */\nexport interface QuoteAnchor {\n readonly left: number\n readonly right: number\n readonly top: number\n readonly bottom: number\n}\n\n/** Measured size of the floating control. */\nexport interface PanelSize {\n readonly width: number\n readonly height: number\n}\n\n/** Viewport extent in CSS pixels. */\nexport interface ViewportSize {\n readonly width: number\n readonly height: number\n}\n\n/** Resolved viewport coordinates. */\nexport interface QuotePlacement {\n readonly left: number\n readonly top: number\n}\n\n/**\n * Place the control below the selection's last line, right edges aligned to it,\n * clamped inside the viewport on both axes, and expressed in document\n * coordinates by adding the scroll offset.\n *\n * The result is in VIEWPORT coordinates, matching the rect the anchor came\n * from. The control is absolutely positioned inside a layer host that is itself\n * pinned to the viewport origin, so viewport coordinates are the right space and\n * no scroll offset is involved.\n * @param anchor - viewport rect of the selection's last line.\n * @param panel - measured panel size; zero before the first layout pass.\n * @param viewport - current viewport extent.\n * @param gap - distance kept between the anchor's bottom edge and the panel.\n * @param margin - distance kept between the panel and each viewport edge.\n * @returns the control's viewport coordinates.\n */\nexport function placeQuoteControl(\n anchor: QuoteAnchor,\n panel: PanelSize,\n viewport: ViewportSize,\n gap = 6,\n margin = 8,\n): QuotePlacement {\n return {\n left: clamp(anchor.right - panel.width, margin, viewport.width - panel.width - margin),\n top: clamp(anchor.bottom + gap, margin, viewport.height - panel.height - margin),\n }\n}\n\n/** Clamp `value` into `[min, max]`, tolerating a max below min (an oversized panel). */\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), Math.max(min, max))\n}\n", "/**\n * Quote text composition: the one transformation between a conversation text\n * selection and the composer draft. Pure so the wire-visible result (the\n * model reads the draft as plain user text) is testable without a DOM.\n * @module @deepseek-ai/dsh-client-ui-quote/quote-text\n */\n\n/** Markdown blockquote line prefix. */\nconst QUOTE_PREFIX = '> '\n\n/**\n * Append one selection to the draft as a markdown blockquote, leaving a blank\n * line after it so the caret's line is the continuation the user types next.\n * Every selected line keeps its own prefix, and a blank selected line becomes a\n * bare `>` \u2014 the blockquote form that keeps the paragraph break inside the\n * quote instead of ending it.\n * @param draft - current composer draft, already in wire (plain text) form.\n * @param selection - selected conversation text.\n * @returns the replacement draft.\n */\nexport function composeQuote(draft: string, selection: string): string {\n const quoted = selection\n .split('\\n')\n .map(line => `${QUOTE_PREFIX}${line}`.trimEnd())\n .join('\\n')\n const head = draft.trimEnd()\n return head === '' ? `${quoted}\\n\\n` : `${head}\\n\\n${quoted}\\n\\n`\n}\n", "/**\n * Viewport layer for the floating quote control.\n *\n * The control must be positioned against a coordinate space the plugin owns,\n * because every coordinate it computes comes from `getBoundingClientRect()`,\n * which is viewport-relative. Two host behaviours make that hard, and both are\n * outside the plugin's control:\n *\n * - an ancestor with `transform`, `filter`, `perspective`, `contain`, or a\n * non-static `position` becomes the containing block for `position: fixed`,\n * silently reinterpreting every viewport coordinate; and\n * - the document body is the host's element, so its `position` is the host's\n * business and may change under the plugin at any time.\n *\n * The fix is to own the containing block outright. The portal target is a\n * dedicated, plugin-created element that is itself `position: fixed` at the\n * viewport origin with no size, so it is pinned to (0, 0) regardless of what\n * any ancestor does. Children positioned absolutely inside it are therefore\n * placed in true viewport coordinates, and the document body is never touched.\n * @module @deepseek-ai/dsh-client-ui-quote/layer\n */\n\nimport { useEffect, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { ReactNode } from 'react'\n\n/** Attribute identifying the plugin's own layer host element. */\nconst HOST_ATTRIBUTE = 'data-quote-layer'\n\n/** Inline styles for the layer host: a zero-size box pinned to the viewport origin. */\nconst HOST_STYLE = {\n position: 'fixed',\n top: '0',\n left: '0',\n width: '0',\n height: '0',\n // Above the composer and the transcript, below modal surfaces the host owns.\n zIndex: '60',\n // The host is a pure coordinate frame: it must never intercept a pointer.\n pointerEvents: 'none',\n} as const\n\n/**\n * Find or create the plugin's layer host. It is created on demand and reused\n * across mounts, so a remount does not stack hosts, and it is attached to the\n * document element rather than the body so a body-level layout change cannot\n * move it.\n * @returns the host element, or null when no document is available.\n */\nfunction ensureLayerHost(): HTMLElement | null {\n if (typeof document === 'undefined') return null\n const existing = document.querySelector<HTMLElement>(`[${HOST_ATTRIBUTE}]`)\n if (existing !== null) return existing\n const host = document.createElement('div')\n host.setAttribute(HOST_ATTRIBUTE, '')\n Object.assign(host.style, HOST_STYLE)\n // `documentElement`, not `body`: the host's own containing block only needs to\n // be the initial one, and nothing above the document element can exist.\n document.documentElement.appendChild(host)\n return host\n}\n\n/**\n * Render children in the plugin's own viewport layer.\n *\n * The host is resolved during the first render rather than in an effect, so the\n * control is portalled into its final coordinate frame in the very commit that\n * mounts it; a layer introduced one commit later would place the first paint in\n * the wrong space.\n * @param props - the control to lift out of the composer subtree.\n * @returns the portalled children, or null when the host cannot host a portal.\n */\nexport function ViewportLayer({ children }: { children: ReactNode }) {\n const [host, setHost] = useState(ensureLayerHost)\n\n // A host that leaves the document (an HMR shell swap, a test teardown) would\n // strand the control, so it is re-created when that happens.\n useEffect(() => {\n if (host !== null && host.isConnected) return\n setHost(ensureLayerHost())\n }, [host])\n\n if (host === null) return null\n return createPortal(children, host)\n}\n", "const css = \"._3OiQSq_control{z-index:60;border:.5px solid var(--dsw-alias-border-l1);color:#fff;white-space:nowrap;cursor:pointer;box-shadow:0 4px 12px var(--dsw-alias-bg-mask-2);background:#000;border-radius:8px;align-items:center;padding:5px 10px;font-family:inherit;font-size:12px;line-height:16px;transition:transform .12s,box-shadow .12s;display:inline-flex;position:absolute}._3OiQSq_control:hover{box-shadow:0 8px 20px var(--dsw-alias-bg-mask-2);transform:translateY(-2px)}\";\nconst styleKey = \"@walkerxie/dsh-plugin-quote/QuoteControl.module.css\";\nconst tagId = \"@walkerxie/dsh-plugin-quote/QuoteControl.module.css#aa03c92b\";\nif (typeof document !== 'undefined') {\n for (const stale of document.querySelectorAll('style[data-plugin-css]')) {\n const id = stale.dataset.pluginCss ?? '';\n if (id !== tagId && (id === styleKey || id.startsWith(styleKey + '#'))) stale.remove();\n }\n}\nif (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=\"' + tagId + '\"]') === null) {\n const tag = document.createElement('style');\n tag.dataset.plugin = \"@walkerxie/dsh-plugin-quote\";\n tag.dataset.pluginCss = tagId;\n tag.textContent = css;\n document.head.appendChild(tag);\n}\nexport default {\"control\":\"_3OiQSq_control\"};", "/**\n * Conversation selection tracking: the registrant-private source behind the\n * floating quote control. The selection is a document-level fact that changes\n * outside React, so it is published as an observable the renderer binds to\n * `useQuoteSelection` rather than mirrored into component state.\n * @module @deepseek-ai/dsh-client-ui-quote/selection\n */\n\nimport type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { QuoteAnchor } from './placement.ts'\n\n/** The current quotable selection. */\nexport interface QuoteSelection {\n /** Selected text, trimmed; empty when nothing is quotable. */\n readonly text: string\n /** Viewport rect of the selection's last line; null when nothing is quotable. */\n readonly anchor: QuoteAnchor | null\n}\n\n/** Published while no quotable selection exists. */\nconst NO_SELECTION: QuoteSelection = { text: '', anchor: null }\n\n/** The conversation transcript's scrollport marker, rendered by the conversation root. */\nconst TRANSCRIPT = '[data-conversation-scroll]'\n\n/**\n * The composer card's marker, rendered by the input bar: a selection inside the\n * draft is the user editing their own text, never a thing to quote.\n */\nconst COMPOSER = '[data-composer-card]'\n\n/** Selection source handed to the slot component through the inject face. */\nexport interface QuoteSelectionSource {\n /** The observable the renderer binds to `useQuoteSelection`. */\n readonly source: HostObservable<QuoteSelection>\n /** Detach every listener and reset the published snapshot. */\n dispose(): void\n}\n\n/** The element a range endpoint belongs to, or null when it has left the document. */\nfunction elementOf(node: Node): Element | null {\n return node instanceof Element ? node : node.parentElement\n}\n\n/** Project one DOM rect onto the anchor fields the placement reads. */\nfunction boxOf(rect: { left: number, right: number, top: number, bottom: number }): QuoteAnchor {\n return { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom }\n}\n\n/**\n * The selection's last line box: the control hangs below it, and\n * `getBoundingClientRect()` spans every line of a multi-line selection, which\n * would hang it from a box taller than the text. An engine that implements\n * neither rect API reports no geometry, and a selection with no geometry has\n * nothing to hang the control from.\n *\n * Empty rects are skipped rather than trusted: engines emit a zero-size rect\n * for a trailing line break, and taking one as the anchor would clamp the\n * control to the viewport's top-left corner instead of the selection. A rect\n * with no area is not a line of text.\n * @param range - the selected range.\n * @returns the last non-empty line's box, or null when the range reports none.\n */\nfunction lastRect(range: Range): QuoteAnchor | null {\n const rects = typeof range.getClientRects === 'function' ? range.getClientRects() : null\n let last: QuoteAnchor | null = null\n if (rects !== null) {\n for (const rect of rects) {\n if (rect.width <= 0 && rect.height <= 0) continue\n last = boxOf(rect)\n }\n }\n if (last !== null) return last\n // Every rect was empty, so the bounding box is the only remaining opinion \u2014\n // and an empty bounding box is still no anchor at all.\n if (typeof range.getBoundingClientRect !== 'function') return null\n const bound = range.getBoundingClientRect()\n if (bound.width <= 0 && bound.height <= 0) return null\n return boxOf(bound)\n}\n\n/** Whether two anchors describe the same box, so snapshot identity can stay stable. */\nfunction sameAnchor(left: QuoteAnchor, right: QuoteAnchor): boolean {\n return left.left === right.left\n && left.right === right.right\n && left.top === right.top\n && left.bottom === right.bottom\n}\n\n/** Whether two snapshots describe the same selection. */\nfunction sameSelection(left: QuoteSelection, right: QuoteSelection): boolean {\n if (left.text !== right.text) return false\n if (left.anchor === null || right.anchor === null) return left.anchor === right.anchor\n return sameAnchor(left.anchor, right.anchor)\n}\n\n/** Read the document selection and decide whether it is quotable. */\nfunction readSelection(doc: Document): QuoteSelection {\n const selection = doc.getSelection()\n if (selection === null || selection.isCollapsed || selection.rangeCount === 0) return NO_SELECTION\n const text = selection.toString().trim()\n if (text === '') return NO_SELECTION\n const range = selection.getRangeAt(0)\n const host = elementOf(range.commonAncestorContainer)\n if (host === null) return NO_SELECTION\n if (host.closest(TRANSCRIPT) === null) return NO_SELECTION\n if (host.closest(COMPOSER) !== null) return NO_SELECTION\n const anchor = lastRect(range)\n if (anchor === null) return NO_SELECTION\n return { text, anchor }\n}\n\n/**\n * Track the document selection and publish each distinct quotable snapshot.\n * @param doc - the document whose selection is tracked.\n * @returns the source plus its disposer; the disposer detaches every listener.\n */\nexport function createQuoteSelectionSource(doc: Document): QuoteSelectionSource {\n let snapshot = NO_SELECTION\n const listeners = new Set<() => void>()\n const publish = (): void => {\n const next = readSelection(doc)\n if (sameSelection(snapshot, next)) return\n snapshot = next\n for (const listener of listeners) listener()\n }\n const win = doc.defaultView\n doc.addEventListener('selectionchange', publish)\n // Pointer and key gestures end a selection without a further selectionchange\n // in some engines, and a scroll moves the anchor rects under a selection that\n // itself never changed.\n doc.addEventListener('pointerup', publish)\n doc.addEventListener('keyup', publish)\n win?.addEventListener('scroll', publish, true)\n win?.addEventListener('resize', publish)\n return {\n source: {\n getSnapshot: () => snapshot,\n subscribe: (listener: () => void) => {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n },\n dispose: () => {\n doc.removeEventListener('selectionchange', publish)\n doc.removeEventListener('pointerup', publish)\n doc.removeEventListener('keyup', publish)\n win?.removeEventListener('scroll', publish, true)\n win?.removeEventListener('resize', publish)\n listeners.clear()\n snapshot = NO_SELECTION\n },\n }\n}\n", "/** `quote` namespace dictionaries. */\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'action.quote': '\u5F15\u7528\u9009\u4E2D\u6587\u5B57',\n} satisfies Record<string, string>\n\n/** The quote namespace key union. */\nexport type QuoteKey = keyof typeof zh\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en = {\n 'action.quote': 'Quote selection',\n} satisfies Record<QuoteKey, string>\n"],
5
- "mappings": "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",
6
- "names": ["import_react"]
3
+ "sources": ["../src/client/index.ts", "../src/client/QuoteControl.tsx", "../src/client/placement.ts", "../src/client/quote-text.ts", "../src/client/layer.tsx", "../src/client/selection.ts", "../src/client/QuoteControl.module.css", "../src/client/locales.ts"],
4
+ "sourcesContent": ["/**\n * Quote surface plugin, browser half: one control in the composer overlay that\n * turns the current conversation text selection into a markdown blockquote\n * appended to the draft. The selection is tracked once for the plugin fiber and\n * published as an inject-face hook source; the draft is read and written\n * through the framework's Session standard shares, so this plugin owns no\n * composer state and adds no model-visible vocabulary.\n */\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: pulls the conversation overlay slot, the Session standard shares\n// (useInput/inputActions), and their declaration merges into the Client program.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the renderer-owned slots service.\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type { QuoteInjected } from './slots.ts'\nimport { QuoteControl } from './QuoteControl.tsx'\nimport { createQuoteSelectionSource } from './selection.ts'\nimport { en, zh, type QuoteKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The quote control's copy. */\n quote: QuoteKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'quote'\n\n/** Required services: the slot registry and the plugin's own dictionaries. */\nexport const inject = ['slots', 'locale']\n\n/**\n * Client plugin body: the overlay entry over one document-level selection source.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-quote: dictionaries')\n\n // One source per plugin fiber, not per session binding: the selection is a\n // document fact, and a per-session source would leak a listener set per\n // session switch.\n const selection = createQuoteSelectionSource(document)\n ctx.effect(() => selection.dispose, 'ui-quote: selection tracking')\n\n ctx.slots.inject('conversation.input.overlay', () => ctx.slots.register({\n name: 'conversation.input.overlay',\n id: 'quote',\n order: 20,\n locale: NS,\n inject: (): QuoteInjected => ({ hooks: { quoteSelection: selection.source } }),\n }, QuoteControl))\n}\n", "/**\n * The floating quote control: a button placed beside the conversation text\n * selection. Visibility, placement, and the appended blockquote all derive from\n * the injected selection hook and the framework's own composer shares; the\n * component holds no subscription machinery and never touches ctx. The button\n * itself renders into the viewport layer, because the composer card this slot\n * lives in is a containing block for fixed positioning.\n */\n\nimport { useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { QuoteInjected } from './slots.ts'\nimport type { QuoteAnchor, PanelSize } from './placement.ts'\nimport { placeQuoteControl, quoteRegion, type Region } from './placement.ts'\nimport { composeQuote } from './quote-text.ts'\nimport { ViewportLayer } from './layer.tsx'\nimport { selectionReport } from './selection.ts'\nimport css from './QuoteControl.module.css'\n\n/** Full props of the overlay entry: overlay owner share + injected selection hook + the locale seat. */\nexport type QuoteControlProps =\n import('@deepseek-ai/dsh-client-ui-slots').PropsRuntime<'conversation.input.overlay'>\n & InjectFace<QuoteInjected>\n & PropsLocale<'quote'>\n\n/** Size published before the first layout pass, where the clamps are skipped. */\nconst UNMEASURED: PanelSize = { width: 0, height: 0 }\n\n/**\n * The quote control. It renders nothing while the selection is empty, while\n * the user has dismissed the current selection, or on hosts whose composer\n * exposes no draft actions \u2014 the button never appears as a dead control.\n * @param props - the overlay entry's derived shares plus the injected hook.\n */\nexport function QuoteControl({ useQuoteSelection, useInput, inputActions, t }: QuoteControlProps) {\n const selection = useQuoteSelection(snapshot => snapshot)\n const draft = useInput(state => state.draft)\n const panelRef = useRef<HTMLButtonElement | null>(null)\n // The button renders in the viewport layer, so the composer card it belongs to\n // is no longer an ancestor of it. The card is resolved through this seat, which\n // stays in the slot's own position: React attaches refs only after it has moved\n // the button into the portal, so by then the button's ancestor chain is already\n // the document root.\n const seatRef = useRef<HTMLSpanElement | null>(null)\n const [size, setSize] = useState<PanelSize>(UNMEASURED)\n // Dismissal is keyed by snapshot identity: the source republishes only when\n // the selection moves, so a new selection shows the control again.\n const [dismissed, setDismissed] = useState<unknown>(null)\n\n const anchor: QuoteAnchor | null = selection.anchor\n const quotable = selection.text !== '' && anchor !== null\n const shown = quotable && dismissed !== selection\n\n // The panel is measured in the same commit that mounts it, so the clamps use\n // real dimensions before anything paints. jsdom reports zero offset sizes,\n // which leaves the unmeasured size in place and is why the clamps themselves\n // are covered through placeQuoteControl. The effect re-runs on every snapshot\n // while the panel is still unmeasured, because a size of zero is not a\n // measurement: it would right-align the control to the selection's right edge\n // and skip the bottom clamp entirely.\n useLayoutEffect(() => {\n if (!shown) return\n const panel = panelRef.current\n /* v8 ignore next -- the panel is mounted in the same commit that reveals the control. */\n if (panel === null) return\n const width = panel.offsetWidth\n const height = panel.offsetHeight\n if (width === 0 && height === 0) return\n if (width !== size.width || height !== size.height) setSize({ width, height })\n }, [shown, selection, size.width, size.height])\n\n // Escape hides the control without clearing the browser selection, which is\n // the user's own to keep.\n useEffect(() => {\n if (!shown) return\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') setDismissed(selection)\n }\n document.addEventListener('keydown', onKeyDown)\n return () => { document.removeEventListener('keydown', onKeyDown) }\n }, [shown, selection])\n\n // Selection reporting, for a page whose only read-out is a screenshot. The\n // report is built on the keypress, never during render: it reads live geometry\n // and the control's own rect, and a read-out that recomputed per render is the\n // self-sustaining loop this plugin already lost a round to. The control's own\n // measurement comes from the live ref at keypress time, so this effect never\n // re-runs on a measurement.\n useEffect(() => {\n if (!shown || panelRef.current === null) return\n const button = panelRef.current\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key.toLowerCase() !== 'c' || !event.ctrlKey || !event.shiftKey) return\n // Keep the selection: the report has to describe it, and the shortcut is\n // not a browser one that needs the default.\n event.preventDefault()\n const json = selectionReport(document, button)\n try {\n void navigator.clipboard?.writeText(json)\n } catch {\n // A clipboard-denied frame is not a reason to lose the selection: the\n // report is a diagnostic, and the selection is the user's.\n }\n }\n document.addEventListener('keydown', onKeyDown)\n return () => { document.removeEventListener('keydown', onKeyDown) }\n }, [shown])\n\n if (!shown || anchor === null) return null\n\n // The control is clamped to the MESSAGE area, not the window. Both bounds are\n // read live rather than from the snapshot: the composer card changes height as\n // the draft wraps, and the transcript box changes as the sidebar or panels\n // resize, neither of which is a selection change.\n const position = placeQuoteControl(\n anchor,\n size,\n { width: window.innerWidth, height: window.innerHeight },\n messageRegion(),\n )\n\n // The layer host is a pure coordinate frame with `pointer-events: none`, so the\n // control re-enables them inline: this must hold even if the plugin's\n // stylesheet fails to load or is overridden.\n const style = { ...position, pointerEvents: 'auto' as const }\n\n return (\n <>\n {/* The seat: this component's own position in the composer card, the one\n place the owning card can still be read from once the button is\n portalled away. It carries no box, so the card's layout is untouched. */}\n <span ref={seatRef} hidden />\n <ViewportLayer>\n <button\n ref={panelRef}\n type=\"button\"\n className={css.control}\n style={style}\n // Identity for tooling: the aria-label is a translation and must not be\n // the only way to find this control (probes and tests both did).\n data-quote-control=\"\"\n aria-label={t('action.quote')}\n // The press must not collapse the selection: the click that follows would\n // otherwise land on a control that a selectionchange already unmounted.\n onMouseDown={event => { event.preventDefault() }}\n onClick={() => {\n inputActions.setDraft(composeQuote(draft, selection.text))\n // Drop the selection so the control leaves with it instead of lingering\n // over text that already reached the draft.\n window.getSelection()?.removeAllRanges()\n focusComposerEnd(seatRef.current?.closest('[data-composer-card]') ?? null)\n }}\n >\n {/* The quote glyph. Decorative \u2014 the button's accessible name is the\n `aria-label` \u2014 so it is hidden from assistive tech and sized by the\n stylesheet. It is what makes the control read as \"quote\" at a\n glance, where the label alone reads as a generic button. */}\n <svg\n className={css.mark}\n width=\"13\"\n height=\"13\"\n viewBox=\"0 0 16 16\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path\n fill=\"currentColor\"\n d=\"M6.2 2.8C3.8 4 2.5 5.9 2.5 8.5V13h5.1V8.2H4.9c.1-1.5.8-2.6 2.1-3.4l-.8-2Zm7 0C10.8 4 9.5 5.9 9.5 8.5V13h5.1V8.2h-2.7c.1-1.5.8-2.6 2.1-3.4l-.8-2Z\"\n />\n </svg>\n {t('action.quote')}\n </button>\n </ViewportLayer>\n </>\n )\n}\n\n/**\n * The area the control may occupy, resolved from the live DOM at render time:\n * the transcript's own box, cut off at the composer card when one is present.\n * The card overlays the bottom of the transcript, so this is what keeps the\n * control off the input box instead of pinned above it.\n * @returns the region the placement clamps into.\n */\nfunction messageRegion(): Region {\n const transcript = document.querySelector('[data-conversation-scroll]')\n const composer = document.querySelector('[data-composer-card]')\n return quoteRegion(\n transcript === null ? null : transcript.getBoundingClientRect(),\n composer === null ? null : composer.getBoundingClientRect(),\n { width: window.innerWidth, height: window.innerHeight },\n )\n}\n\n/**\n * Hand DOM focus to the composer that owns this control and collapse the caret\n * at the end of the draft. `setDraft` already put Lexical's selection there,\n * but Lexical skips DOM selection reconciliation while its editor is unfocused,\n * and a browser focusing an editor whose DOM selection sits elsewhere puts the\n * caret at the element's start.\n * @param card - the composer card holding this control's slot seat, resolved\n * through the seat because the button itself now lives in the viewport layer.\n */\nfunction focusComposerEnd(card: Element | null): void {\n const editable = card?.querySelector<HTMLElement>('[data-composer-input]')\n if (editable === null || editable === undefined) return\n editable.focus({ preventScroll: true })\n const range = document.createRange()\n range.selectNodeContents(editable)\n range.collapse(false)\n const selection = window.getSelection()\n selection?.removeAllRanges()\n selection?.addRange(range)\n}\n", "/**\n * Placement math for the floating quote control. The anchor is a selection\n * box, not an element, so the shared element-anchored position hook does not\n * apply; this keeps the arithmetic pure so jsdom (which reports no layout) can\n * still cover the region clamps and the vertical flip.\n *\n * The result is in VIEWPORT coordinates, matching the rect the anchor came\n * from: the control is absolutely positioned inside a layer host pinned to the\n * viewport origin, so no scroll offset is involved.\n * @module @deepseek-ai/dsh-client-ui-quote/placement\n */\n\n/** Viewport-relative box of the selection, as the DOM reports it. */\nexport interface QuoteAnchor {\n readonly left: number\n readonly right: number\n readonly top: number\n readonly bottom: number\n}\n\n/** One rect the engine reported, in the subset the anchor reads. */\nexport interface SelectionRect {\n readonly left: number\n readonly right: number\n readonly top: number\n readonly bottom: number\n}\n\n/**\n * The area the control is allowed to occupy, in viewport coordinates. It is the\n * MESSAGE area, not the window: the composer card overlays the bottom of the\n * transcript and the message column is narrower than the viewport, so clamping\n * to `innerWidth`/`innerHeight` let the control land under the composer and\n * hard against the window's right edge.\n */\nexport interface Region {\n readonly left: number\n readonly right: number\n readonly top: number\n readonly bottom: number\n}\n\n/** Measured size of the floating control. */\nexport interface PanelSize {\n readonly width: number\n readonly height: number\n}\n\n/** Viewport extent in CSS pixels. */\nexport interface ViewportSize {\n readonly width: number\n readonly height: number\n}\n\n/** Resolved viewport coordinates. */\nexport interface QuotePlacement {\n readonly left: number\n readonly top: number\n}\n\n/** A viewport-relative rect, as `getBoundingClientRect()` reports one. */\nexport interface ScreenBox {\n readonly left: number\n readonly right: number\n readonly top: number\n readonly bottom: number\n}\n\n/**\n * The area the control may occupy: the transcript's own box, cut off at the\n * composer card when one is present, and intersected with the viewport.\n *\n * Both cuts are load-bearing. The composer card overlays the bottom of the\n * transcript, so a control clamped to the window's bottom edge lands ON the\n * composer \u2014 the \"button floating above the input box\" report. And the message\n * column is narrower than the window, so clamping to `innerWidth` lets the\n * control sit at the window's right edge, far from the text it quotes.\n * @param transcript - the transcript scrollport's box, or null when absent.\n * @param composer - the composer card's box, or null when absent.\n * @param viewport - viewport extent, the fallback region and the outer bound.\n * @returns the region, always a valid box inside the viewport.\n */\nexport function quoteRegion(\n transcript: ScreenBox | null,\n composer: ScreenBox | null,\n viewport: ViewportSize,\n): Region {\n const window = { left: 0, top: 0, right: viewport.width, bottom: viewport.height }\n // A box with no area is not evidence of a box: jsdom reports zero-size rects\n // for every element, and a hidden element reports one too. Treating it as a\n // real edge would collapse the region to a point.\n const measured = (box: ScreenBox | null): ScreenBox | null =>\n box !== null && box.right > box.left && box.bottom > box.top ? box : null\n const base = measured(transcript) ?? window\n const card = measured(composer)\n return {\n left: Math.max(base.left, window.left),\n right: Math.min(base.right, window.right),\n top: Math.max(base.top, window.top),\n bottom: Math.min(card === null ? base.bottom : card.top, window.bottom),\n }\n}\n\n/**\n * Smallest width a rect needs before it can describe the highlight's edge.\n *\n * A zero-width rect is a phantom: the engine emits one for a caret, for a block\n * boundary, and for the trailing cursor a block-level range produces. Its\n * `left` and `right` are the same number, and for a block that number is the\n * column's edge \u2014 so counting it in the union dragged the control's horizontal\n * anchor to the far right of the message area. It is discarded, not kept.\n */\nconst MIN_RECT_WIDTH = 1\n\n/**\n * Smallest height a rect needs before it can describe a line of text.\n *\n * A zero-height rect is a block boundary, and the engine reports it spanning the\n * whole column. Both degenerate shapes are non-paintable: the browser draws no\n * highlight for them, so they are not part of what the user selected.\n */\nconst MIN_RECT_HEIGHT = 4\n\n/**\n * The selection's box: the union of the TEXT rects it covers.\n *\n * The caller supplies rects measured from text nodes, so an element's border box\n * cannot be in the list. What remains can still hold a degenerate rect \u2014 a caret\n * or a block boundary \u2014 and those are dropped: the browser paints no highlight\n * for them, so they are not what the user selected. A single zero-width phantom\n * at the column's edge is enough to move the control to the far side of the\n * message area.\n *\n * Document order is deliberately not consulted. It is not reading order, and\n * every rule that picked \"the last rect\" anchored to a fragment.\n * @param rects - text rects, in any order.\n * @returns the union box, or null when the list holds no paintable rect.\n */\nexport function selectionBox(rects: Iterable<SelectionRect>): QuoteAnchor | null {\n let left = Infinity\n let right = -Infinity\n let top = Infinity\n let bottom = -Infinity\n let any = false\n for (const rect of rects) {\n if (rect.right - rect.left < MIN_RECT_WIDTH) continue\n if (rect.bottom - rect.top < MIN_RECT_HEIGHT) continue\n any = true\n if (rect.left < left) left = rect.left\n if (rect.right > right) right = rect.right\n if (rect.top < top) top = rect.top\n if (rect.bottom > bottom) bottom = rect.bottom\n }\n return any ? { left, right, top, bottom } : null\n}\n\n/**\n * Place the control below the selection, right edges aligned to it, flipping\n * above the selection when there is no room below, and clamped inside `region`.\n *\n * The flip and the region are both why the control never detaches from its\n * text: clamping to the window's bottom edge left it floating above the\n * composer, and clamping to the window's width left it at the far right of the\n * message area.\n * @param anchor - viewport box of the selection.\n * @param panel - measured panel size; zero before the first layout pass.\n * @param viewport - current viewport extent, used when no region is given.\n * @param region - the message area the control must stay inside.\n * @param gap - distance kept between the selection edge and the panel.\n * @param margin - distance kept between the panel and the region edge.\n * @returns the control's viewport coordinates.\n */\nexport function placeQuoteControl(\n anchor: QuoteAnchor,\n panel: PanelSize,\n viewport: ViewportSize,\n region: Region = { left: 0, top: 0, right: viewport.width, bottom: viewport.height },\n gap = 6,\n margin = 8,\n): QuotePlacement {\n return {\n left: clamp(anchor.right - panel.width, region.left + margin, region.right - panel.width - margin),\n top: clamp(vertical(anchor, panel, region, gap, margin), region.top + margin, region.bottom - panel.height - margin),\n }\n}\n\n/**\n * The vertical edge to hang the control from: ABOVE the selection when it fits\n * inside the region, otherwise below it.\n *\n * Above is the default because the row above the selection is the one the user\n * has already read, while the row below is the one they are still reading \u2014 and\n * the control covers whichever it sits over. It also keeps the control clear of\n * the text just selected: a drag ends at the selection's bottom edge, so a\n * control placed below lands on the last line the user is looking at.\n *\n * When neither side fits, the panel is taller than the space around the\n * selection and the caller's clamp is the only answer left.\n * @param anchor - viewport box of the selection.\n * @param panel - measured panel size.\n * @param region - the message area the control must stay inside.\n * @param gap - distance kept between the selection edge and the panel.\n * @param margin - distance kept between the panel and the region edge.\n * @returns the unclamped top edge.\n */\nfunction vertical(\n anchor: QuoteAnchor,\n panel: PanelSize,\n region: Region,\n gap: number,\n margin: number,\n): number {\n const above = anchor.top - gap - panel.height\n if (above >= region.top + margin) return above\n return anchor.bottom + gap\n}\n\n/** Clamp `value` into `[min, max]`, tolerating a max below min (an oversized panel). */\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), Math.max(min, max))\n}\n", "/**\n * Quote text composition: the one transformation between a conversation text\n * selection and the composer draft. Pure so the wire-visible result (the\n * model reads the draft as plain user text) is testable without a DOM.\n * @module @deepseek-ai/dsh-client-ui-quote/quote-text\n */\n\n/** Markdown blockquote line prefix. */\nconst QUOTE_PREFIX = '> '\n\n/**\n * Append one selection to the draft as a markdown blockquote, leaving a blank\n * line after it so the caret's line is the continuation the user types next.\n * Every selected line keeps its own prefix, and a blank selected line becomes a\n * bare `>` \u2014 the blockquote form that keeps the paragraph break inside the\n * quote instead of ending it.\n * @param draft - current composer draft, already in wire (plain text) form.\n * @param selection - selected conversation text.\n * @returns the replacement draft.\n */\nexport function composeQuote(draft: string, selection: string): string {\n const quoted = selection\n .split('\\n')\n .map(line => `${QUOTE_PREFIX}${line}`.trimEnd())\n .join('\\n')\n const head = draft.trimEnd()\n return head === '' ? `${quoted}\\n\\n` : `${head}\\n\\n${quoted}\\n\\n`\n}\n", "/**\n * Viewport layer for the floating quote control.\n *\n * The control must be positioned against a coordinate space the plugin owns,\n * because every coordinate it computes comes from `getBoundingClientRect()`,\n * which is viewport-relative. Two host behaviours make that hard, and both are\n * outside the plugin's control:\n *\n * - an ancestor with `transform`, `filter`, `perspective`, `contain`, or a\n * non-static `position` becomes the containing block for `position: fixed`,\n * silently reinterpreting every viewport coordinate; and\n * - the document body is the host's element, so its `position` is the host's\n * business and may change under the plugin at any time.\n *\n * The fix is to own the containing block outright. The portal target is a\n * dedicated, plugin-created element that is itself `position: fixed` at the\n * viewport origin with no size, so it is pinned to (0, 0) regardless of what\n * any ancestor does. Children positioned absolutely inside it are therefore\n * placed in true viewport coordinates, and the document body is never touched.\n * @module @deepseek-ai/dsh-client-ui-quote/layer\n */\n\nimport { useEffect, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { ReactNode } from 'react'\n\n/** Attribute identifying the plugin's own layer host element. */\nconst HOST_ATTRIBUTE = 'data-quote-layer'\n\n/** Inline styles for the layer host: a zero-size box pinned to the viewport origin. */\nconst HOST_STYLE = {\n position: 'fixed',\n top: '0',\n left: '0',\n width: '0',\n height: '0',\n // Above the composer and the transcript, below modal surfaces the host owns.\n zIndex: '60',\n // The host is a pure coordinate frame: it must never intercept a pointer.\n pointerEvents: 'none',\n} as const\n\n/**\n * Find or create the plugin's layer host. It is created on demand and reused\n * across mounts, so a remount does not stack hosts, and it is attached to the\n * document element rather than the body so a body-level layout change cannot\n * move it.\n * @returns the host element, or null when no document is available.\n */\nfunction ensureLayerHost(): HTMLElement | null {\n if (typeof document === 'undefined') return null\n const existing = document.querySelector<HTMLElement>(`[${HOST_ATTRIBUTE}]`)\n if (existing !== null) return existing\n const host = document.createElement('div')\n host.setAttribute(HOST_ATTRIBUTE, '')\n Object.assign(host.style, HOST_STYLE)\n // `documentElement`, not `body`: the host's own containing block only needs to\n // be the initial one, and nothing above the document element can exist.\n document.documentElement.appendChild(host)\n return host\n}\n\n/**\n * Render children in the plugin's own viewport layer.\n *\n * The host is resolved during the first render rather than in an effect, so the\n * control is portalled into its final coordinate frame in the very commit that\n * mounts it; a layer introduced one commit later would place the first paint in\n * the wrong space.\n * @param props - the control to lift out of the composer subtree.\n * @returns the portalled children, or null when the host cannot host a portal.\n */\nexport function ViewportLayer({ children }: { children: ReactNode }) {\n const [host, setHost] = useState(ensureLayerHost)\n\n // A host that leaves the document (an HMR shell swap, a test teardown) would\n // strand the control, so it is re-created when that happens.\n useEffect(() => {\n if (host !== null && host.isConnected) return\n setHost(ensureLayerHost())\n }, [host])\n\n if (host === null) return null\n return createPortal(children, host)\n}\n", "/**\n * Conversation selection tracking: the registrant-private source behind the\n * floating quote control. The selection is a document-level fact that changes\n * outside React, so it is published as an observable the renderer binds to\n * `useQuoteSelection` rather than mirrored into component state.\n * @module @deepseek-ai/dsh-client-ui-quote/selection\n */\n\nimport type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { QuoteAnchor, SelectionRect } from './placement.ts'\nimport { selectionBox } from './placement.ts'\n\n/** The current quotable selection. */\nexport interface QuoteSelection {\n /** Selected text, trimmed; empty when nothing is quotable. */\n readonly text: string\n /** Viewport box of the whole selection; null when nothing is quotable. */\n readonly anchor: QuoteAnchor | null\n}\n\n/** Published while no quotable selection exists. */\nconst NO_SELECTION: QuoteSelection = { text: '', anchor: null }\n\n/** Shorten a measured value for the report. */\nfunction short(value: number): string {\n return `${Math.round(value * 10) / 10}`\n}\n\n/** The conversation transcript's scrollport marker, rendered by the conversation root. */\nconst TRANSCRIPT = '[data-conversation-scroll]'\n\n/**\n * The composer card's marker, rendered by the input bar: a selection inside the\n * draft is the user editing their own text, never a thing to quote.\n */\nconst COMPOSER = '[data-composer-card]'\n\n/** Selection source handed to the slot component through the inject face. */\nexport interface QuoteSelectionSource {\n /** The observable the renderer binds to `useQuoteSelection`. */\n readonly source: HostObservable<QuoteSelection>\n /** Detach every listener and reset the published snapshot. */\n dispose(): void\n}\n\n/** The element a range endpoint belongs to, or null when it has left the document. */\nfunction elementOf(node: Node): Element | null {\n return node instanceof Element ? node : node.parentElement\n}\n\n/**\n * Elements whose interior is a control, never quoted prose. A drag that\n * overshoots the last line of an answer takes the selection into the message\n * action bar below it (copy, like, the usage read-out); the read-out's label is\n * a text node, so only this check keeps it out of the anchor.\n *\n * `a` and `svg` are deliberately absent: a link's text is ordinary quotable\n * prose, and a bare icon is always inside a `button`.\n */\nconst CONTROL = 'button, [role=\"button\"], input, select, textarea, summary'\n\n/**\n * `NodeFilter.SHOW_TEXT`. Passed as its value rather than the constructor\n * property so this works in a document whose window cannot be reached.\n */\nconst SHOW_TEXT = 4\n\n/**\n * The rects of the TEXT the range covers.\n *\n * `Range.getClientRects()` is not \"the rects of the selected text\". Blink builds\n * it from two sources (its `GetBorderAndTextQuads`): the line box of each\n * partially-selected text node, AND the border box of every element the range\n * FULLY CONTAINS. The second kind is not what the browser paints as the\n * highlight, and it is what breaks placement: a range that fully contains a\n * `<button>` reports that button's whole 28x28 box, and a range that fully\n * contains a block element reports a rect spanning the entire column.\n *\n * That shape is not exotic \u2014 it is exactly what selecting the LAST paragraph of\n * an answer produces, because the drag has nowhere to land but the whitespace\n * and controls below it, so the end point sits on a container element and every\n * following sibling is fully contained.\n *\n * So the rects are gathered the way a selection toolbar must: by walking the\n * text nodes the range intersects and measuring each one's own slice. Element\n * border boxes then cannot enter, and the anchor is the text the user sees.\n *\n * The walk starts at the range's common ancestor, and the transcript is\n * virtualized, so the node count is bounded by what is on screen.\n * @param range - the selected range.\n * @param doc - the document the selection lives in.\n * @returns the text rects, or null when the engine cannot measure sub-ranges.\n */\nfunction textRectsOf(range: Range, doc: Document): SelectionRect[] | null {\n // A sub-range has to be measurable for this approach to mean anything. jsdom\n // reports no rect API at all, and there the caller's own rect source is the\n // only geometry there is.\n const probe = doc.createRange()\n if (typeof probe.getClientRects !== 'function' || typeof range.intersectsNode !== 'function') return null\n const root = elementOf(range.commonAncestorContainer)\n if (root === null) return null\n\n const out: SelectionRect[] = []\n const walker = doc.createTreeWalker(root, SHOW_TEXT)\n for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {\n if (!range.intersectsNode(node)) continue\n const parent = node.parentElement\n if (parent === null) continue\n if (parent.closest(CONTROL) !== null) continue\n if (parent.closest(COMPOSER) !== null) continue\n const slice = doc.createRange()\n slice.selectNodeContents(node)\n // The two endpoint nodes are only partly selected; every other intersecting\n // text node is fully inside the range and needs no clipping.\n if (node === range.startContainer) slice.setStart(node, range.startOffset)\n if (node === range.endContainer) slice.setEnd(node, range.endOffset)\n for (const rect of slice.getClientRects()) out.push(rect)\n }\n return out\n}\n\n/**\n * The selection's anchor: the union of the highlighted text's rects, which is\n * the blue region the user points at.\n * @param range - the selected range.\n * @param doc - the document the selection lives in.\n * @returns the anchor, or null when the range reports no geometry.\n */\nfunction anchorOf(range: Range, doc: Document): QuoteAnchor | null {\n const text = textRectsOf(range, doc)\n if (text !== null) {\n const box = selectionBox(text)\n if (box !== null) return box\n }\n // An engine with no sub-range measurement, or a selection with no text at all\n // (an image, a widget): the range's own rects are the only geometry offered.\n const rects = typeof range.getClientRects === 'function' ? range.getClientRects() : null\n if (rects !== null) {\n const box = selectionBox(Array.from(rects))\n if (box !== null) return box\n }\n const bound = typeof range.getBoundingClientRect === 'function' ? range.getBoundingClientRect() : null\n return bound === null ? null : selectionBox([bound])\n}\n\n/** Whether two anchors describe the same box, so snapshot identity can stay stable. */\nfunction sameAnchor(left: QuoteAnchor, right: QuoteAnchor): boolean {\n return left.left === right.left\n && left.right === right.right\n && left.top === right.top\n && left.bottom === right.bottom\n}\n\n/** Whether two snapshots describe the same selection. */\nfunction sameSelection(left: QuoteSelection, right: QuoteSelection): boolean {\n if (left.text !== right.text) return false\n if (left.anchor === null || right.anchor === null) return left.anchor === right.anchor\n return sameAnchor(left.anchor, right.anchor)\n}\n\n/** Read the document selection and decide whether it is quotable. */\nfunction readSelection(doc: Document): QuoteSelection {\n const selection = doc.getSelection()\n if (selection === null || selection.isCollapsed || selection.rangeCount === 0) return NO_SELECTION\n const text = selection.toString().trim()\n if (text === '') return NO_SELECTION\n const range = selection.getRangeAt(0)\n const host = elementOf(range.commonAncestorContainer)\n if (host === null) return NO_SELECTION\n if (host.closest(TRANSCRIPT) === null) return NO_SELECTION\n if (host.closest(COMPOSER) !== null) return NO_SELECTION\n const anchor = anchorOf(range, doc)\n if (anchor === null) return NO_SELECTION\n return { text, anchor }\n}\n\n/** How much of a node's markup the report carries: enough to describe the block, not the page. */\nconst REPORT_HTML_LIMIT = 1200\n\n/** How far up the ancestor chain the report walks. */\nconst REPORT_DEPTH = 6\n\n/**\n * A paste-ready description of the live selection, for a page with no console.\n *\n * It answers, at ONE moment, every question a placement bug raises: the raw rect\n * list the union was taken from, the selection box, both endpoints, the markup\n * the selection sits in, the viewport, and \u2014 when the control is mounted \u2014 the\n * rect and inline coordinates it actually got. The user copies it and pastes it;\n * nothing here needs DevTools, and none of it runs during render.\n * @param doc - the document to read.\n * @param control - the mounted control, or null when it is not on screen.\n * @returns the report as pretty-printed JSON.\n */\nexport function selectionReport(doc: Document, control: Element | null): string {\n const selection = doc.getSelection()\n const report: Record<string, unknown> = {}\n if (selection === null || selection.rangeCount === 0) {\n report.selection = null\n } else {\n const range = selection.getRangeAt(0)\n const rects = typeof range.getClientRects === 'function' ? Array.from(range.getClientRects()) : []\n const bound = typeof range.getBoundingClientRect === 'function' ? range.getBoundingClientRect() : null\n const ancestor = elementOf(range.commonAncestorContainer)\n report.selection = {\n text: selection.toString(),\n // The raw engine list, for comparison against `textUnion`: when they\n // disagree, `getClientRects` reported an element's border box because the\n // range fully contains it, and that is the shape that breaks placement.\n rects: rects.map(rect => ({\n left: short(rect.left),\n right: short(rect.right),\n top: short(rect.top),\n bottom: short(rect.bottom),\n width: short(rect.right - rect.left),\n height: short(rect.bottom - rect.top),\n })),\n bounding: bound === null ? null : {\n left: short(bound.left),\n right: short(bound.right),\n top: short(bound.top),\n bottom: short(bound.bottom),\n },\n union: selectionBox(rects),\n // The union of the TEXT rects, which is what the anchor actually uses.\n textUnion: selectionBox(textRectsOf(range, doc) ?? []),\n // The visual end of the selection as the engine places a caret there, for\n // comparison against the union: a disagreement between the two is a\n // phantom rect inside the union.\n endCaret: caretBox(range),\n start: endpoint(range.startContainer, range.startOffset),\n end: endpoint(range.endContainer, range.endOffset),\n ancestors: ancestorsOf(ancestor),\n html: ancestor === null ? null : ancestor.outerHTML.slice(0, REPORT_HTML_LIMIT),\n }\n }\n report.control = control === null ? null : controlBox(control)\n const view = doc.defaultView\n report.viewport = view === null ? null : { width: view.innerWidth, height: view.innerHeight }\n return JSON.stringify(report, null, 2)\n}\n\n/**\n * The box of a caret at the selection's logical end. The union can be inflated\n * by phantom rects; this cannot, so the two together say whether the anchor is\n * describing the highlight or a boundary.\n * @param range - the selected range.\n * @returns the end caret's box, or null when the engine reports none.\n */\nfunction caretBox(range: Range): Record<string, string> | null {\n try {\n const collapsed = range.cloneRange()\n collapsed.collapse(false)\n const rects = typeof collapsed.getClientRects === 'function' ? collapsed.getClientRects() : null\n const first = rects === null ? undefined : rects[0]\n if (first === undefined) return null\n return {\n left: short(first.left),\n right: short(first.right),\n top: short(first.top),\n bottom: short(first.bottom),\n }\n } catch {\n return null\n }\n}\n\n/**\n * One range endpoint, as node name, offset, and trailing text. An ELEMENT\n * endpoint is the diagnostic that matters: it means the range fully contains\n * that element, and the engine reports the element's border box as part of the\n * selection. `atEnd` says the offset is the element's last child, the shape a\n * drag into the whitespace below the text produces.\n * @param node - the endpoint node.\n * @param offset - the endpoint offset.\n * @returns the endpoint description.\n */\nfunction endpoint(node: Node, offset: number): Record<string, unknown> {\n if (node.nodeType === 3) {\n return {\n node: '#text',\n offset,\n tail: (node.nodeValue ?? '').slice(-24),\n atEnd: offset === (node.nodeValue ?? '').length,\n }\n }\n return {\n node: node.nodeName,\n offset,\n tail: null,\n atEnd: offset === node.childNodes.length,\n }\n}\n\n/** The marked-up ancestor chain around a node, outermost last. */\nfunction ancestorsOf(element: Element | null): string[] {\n const chain: string[] = []\n for (let node = element; node !== null && chain.length < REPORT_DEPTH; node = node.parentElement) {\n const marker = Array.from(node.attributes)\n .filter(attribute => attribute.name.startsWith('data-') || attribute.name === 'class')\n .map(attribute => `${attribute.name}=\"${attribute.value}\"`)\n .join(' ')\n chain.push(`${node.tagName.toLowerCase()}${marker === '' ? '' : ` ${marker}`}`)\n }\n return chain\n}\n\n/** The control's own rect plus the inline coordinates it was given. */\nfunction controlBox(control: Element): Record<string, unknown> {\n const rect = control.getBoundingClientRect()\n return {\n rect: {\n left: short(rect.left),\n right: short(rect.right),\n top: short(rect.top),\n bottom: short(rect.bottom),\n },\n inline: control instanceof HTMLElement ? { left: control.style.left, top: control.style.top } : null,\n }\n}\n\n/**\n * Track the document selection and publish each distinct quotable snapshot.\n * @param doc - the document whose selection is tracked.\n * @returns the source plus its disposer; the disposer detaches every listener.\n */\nexport function createQuoteSelectionSource(doc: Document): QuoteSelectionSource {\n let snapshot = NO_SELECTION\n const listeners = new Set<() => void>()\n const publish = (): void => {\n const next = readSelection(doc)\n if (sameSelection(snapshot, next)) return\n snapshot = next\n for (const listener of listeners) listener()\n }\n const win = doc.defaultView\n doc.addEventListener('selectionchange', publish)\n // Pointer and key gestures end a selection without a further selectionchange\n // in some engines, and a scroll moves the anchor rects under a selection that\n // itself never changed.\n doc.addEventListener('pointerup', publish)\n doc.addEventListener('keyup', publish)\n win?.addEventListener('scroll', publish, true)\n win?.addEventListener('resize', publish)\n\n // Layout moves the anchor rects without changing the selection at all: the\n // composer card grows as the draft wraps and the transcript's content box\n // changes as the virtual list recycles rows or a sidebar resizes. None of\n // those fire `selectionchange`, `scroll`, or `resize`, so a control placed\n // against the old rects would sit at stale coordinates \u2014 which is how it ends\n // up over the composer when the last paragraph is selected. A ResizeObserver\n // on the two boxes that decide the anchor's position covers them.\n //\n // ResizeObserver notifications are delivered after layout and before paint, so\n // the rects read here are already the settled ones. Publishing goes through the\n // SAME comparison as every other trigger: a resize that did not move the anchor\n // changes nothing downstream and cannot loop.\n const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(() => { publish() })\n for (const selector of [TRANSCRIPT, COMPOSER]) {\n const element = doc.querySelector(selector)\n if (element !== null) observer?.observe(element)\n }\n\n return {\n source: {\n getSnapshot: () => snapshot,\n subscribe: (listener: () => void) => {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n },\n dispose: () => {\n doc.removeEventListener('selectionchange', publish)\n doc.removeEventListener('pointerup', publish)\n doc.removeEventListener('keyup', publish)\n win?.removeEventListener('scroll', publish, true)\n win?.removeEventListener('resize', publish)\n observer?.disconnect()\n listeners.clear()\n snapshot = NO_SELECTION\n },\n }\n}\n", "const css = \"._3OiQSq_control{z-index:60;color:#fff;letter-spacing:.01em;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;background:#1b1b1c;border:1px solid #ffffff24;border-radius:8px;align-items:center;gap:4px;min-height:28px;padding:0 10px 0 7px;font-family:inherit;font-size:12px;font-weight:500;line-height:16px;transition:transform .12s,box-shadow .12s,background-color .12s;display:inline-flex;position:absolute;box-shadow:0 1px 2px #0000003d,0 6px 20px #00000047}._3OiQSq_control>._3OiQSq_mark{color:var(--dsw-static-deepseek-400,#679efe);flex:none}._3OiQSq_control:hover{background:#2c2c2e;transform:translateY(-2px);box-shadow:0 1px 2px #0000003d,0 10px 28px #00000057}._3OiQSq_control:active{background:#353638;transform:translateY(0);box-shadow:0 1px 2px #0000003d,0 2px 8px #0000003d}._3OiQSq_control:focus-visible{outline:2px solid var(--dsw-static-deepseek-400,#679efe);outline-offset:2px}@media (prefers-reduced-motion:reduce){._3OiQSq_control{transition:none}._3OiQSq_control:hover,._3OiQSq_control:active{transform:none}}\";\nconst styleKey = \"@walkerxie/dsh-plugin-quote/QuoteControl.module.css\";\nconst tagId = \"@walkerxie/dsh-plugin-quote/QuoteControl.module.css#8edd12ad\";\nif (typeof document !== 'undefined') {\n for (const stale of document.querySelectorAll('style[data-plugin-css]')) {\n const id = stale.dataset.pluginCss ?? '';\n if (id !== tagId && (id === styleKey || id.startsWith(styleKey + '#'))) stale.remove();\n }\n}\nif (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=\"' + tagId + '\"]') === null) {\n const tag = document.createElement('style');\n tag.dataset.plugin = \"@walkerxie/dsh-plugin-quote\";\n tag.dataset.pluginCss = tagId;\n tag.textContent = css;\n document.head.appendChild(tag);\n}\nexport default {\"control\":\"_3OiQSq_control\",\"mark\":\"_3OiQSq_mark\"};", "/** `quote` namespace dictionaries. */\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'action.quote': '\u5F15\u7528\u9009\u4E2D\u6587\u5B57',\n} satisfies Record<string, string>\n\n/** The quote namespace key union. */\nexport type QuoteKey = keyof typeof zh\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en = {\n 'action.quote': 'Quote selection',\n} satisfies Record<QuoteKey, string>\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSA,IAAAA,gBAA6D;;;ACyEtD,SAAS,YACd,YACA,UACA,UACQ;AACR,QAAMC,UAAS,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,SAAS,OAAO,QAAQ,SAAS,OAAO;AAIjF,QAAM,WAAW,CAAC,QAChB,QAAQ,QAAQ,IAAI,QAAQ,IAAI,QAAQ,IAAI,SAAS,IAAI,MAAM,MAAM;AACvE,QAAM,OAAO,SAAS,UAAU,KAAKA;AACrC,QAAM,OAAO,SAAS,QAAQ;AAC9B,SAAO;AAAA,IACL,MAAM,KAAK,IAAI,KAAK,MAAMA,QAAO,IAAI;AAAA,IACrC,OAAO,KAAK,IAAI,KAAK,OAAOA,QAAO,KAAK;AAAA,IACxC,KAAK,KAAK,IAAI,KAAK,KAAKA,QAAO,GAAG;AAAA,IAClC,QAAQ,KAAK,IAAI,SAAS,OAAO,KAAK,SAAS,KAAK,KAAKA,QAAO,MAAM;AAAA,EACxE;AACF;AAWA,IAAM,iBAAiB;AASvB,IAAM,kBAAkB;AAiBjB,SAAS,aAAa,OAAoD;AAC/E,MAAI,OAAO;AACX,MAAI,QAAQ;AACZ,MAAI,MAAM;AACV,MAAI,SAAS;AACb,MAAI,MAAM;AACV,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,QAAQ,KAAK,OAAO,eAAgB;AAC7C,QAAI,KAAK,SAAS,KAAK,MAAM,gBAAiB;AAC9C,UAAM;AACN,QAAI,KAAK,OAAO,KAAM,QAAO,KAAK;AAClC,QAAI,KAAK,QAAQ,MAAO,SAAQ,KAAK;AACrC,QAAI,KAAK,MAAM,IAAK,OAAM,KAAK;AAC/B,QAAI,KAAK,SAAS,OAAQ,UAAS,KAAK;AAAA,EAC1C;AACA,SAAO,MAAM,EAAE,MAAM,OAAO,KAAK,OAAO,IAAI;AAC9C;AAkBO,SAAS,kBACd,QACA,OACA,UACA,SAAiB,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,SAAS,OAAO,QAAQ,SAAS,OAAO,GACnF,MAAM,GACN,SAAS,GACO;AAChB,SAAO;AAAA,IACL,MAAM,MAAM,OAAO,QAAQ,MAAM,OAAO,OAAO,OAAO,QAAQ,OAAO,QAAQ,MAAM,QAAQ,MAAM;AAAA,IACjG,KAAK,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,MAAM,GAAG,OAAO,MAAM,QAAQ,OAAO,SAAS,MAAM,SAAS,MAAM;AAAA,EACrH;AACF;AAqBA,SAAS,SACP,QACA,OACA,QACA,KACA,QACQ;AACR,QAAM,QAAQ,OAAO,MAAM,MAAM,MAAM;AACvC,MAAI,SAAS,OAAO,MAAM,OAAQ,QAAO;AACzC,SAAO,OAAO,SAAS;AACzB;AAGA,SAAS,MAAM,OAAe,KAAa,KAAqB;AAC9D,SAAO,KAAK,IAAI,KAAK,IAAI,OAAO,GAAG,GAAG,KAAK,IAAI,KAAK,GAAG,CAAC;AAC1D;;;ACpNA,IAAM,eAAe;AAYd,SAAS,aAAa,OAAe,WAA2B;AACrE,QAAM,SAAS,UACZ,MAAM,IAAI,EACV,IAAI,UAAQ,GAAG,YAAY,GAAG,IAAI,GAAG,QAAQ,CAAC,EAC9C,KAAK,IAAI;AACZ,QAAM,OAAO,MAAM,QAAQ;AAC3B,SAAO,SAAS,KAAK,GAAG,MAAM;AAAA;AAAA,IAAS,GAAG,IAAI;AAAA;AAAA,EAAO,MAAM;AAAA;AAAA;AAC7D;;;ACLA,mBAAoC;AACpC,uBAA6B;AAI7B,IAAM,iBAAiB;AAGvB,IAAM,aAAa;AAAA,EACjB,UAAU;AAAA,EACV,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AAAA;AAAA,EAER,QAAQ;AAAA;AAAA,EAER,eAAe;AACjB;AASA,SAAS,kBAAsC;AAC7C,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,WAAW,SAAS,cAA2B,IAAI,cAAc,GAAG;AAC1E,MAAI,aAAa,KAAM,QAAO;AAC9B,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,aAAa,gBAAgB,EAAE;AACpC,SAAO,OAAO,KAAK,OAAO,UAAU;AAGpC,WAAS,gBAAgB,YAAY,IAAI;AACzC,SAAO;AACT;AAYO,SAAS,cAAc,EAAE,SAAS,GAA4B;AACnE,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,eAAe;AAIhD,8BAAU,MAAM;AACd,QAAI,SAAS,QAAQ,KAAK,YAAa;AACvC,YAAQ,gBAAgB,CAAC;AAAA,EAC3B,GAAG,CAAC,IAAI,CAAC;AAET,MAAI,SAAS,KAAM,QAAO;AAC1B,aAAO,+BAAa,UAAU,IAAI;AACpC;;;AC/DA,IAAM,eAA+B,EAAE,MAAM,IAAI,QAAQ,KAAK;AAG9D,SAAS,MAAM,OAAuB;AACpC,SAAO,GAAG,KAAK,MAAM,QAAQ,EAAE,IAAI,EAAE;AACvC;AAGA,IAAM,aAAa;AAMnB,IAAM,WAAW;AAWjB,SAAS,UAAU,MAA4B;AAC7C,SAAO,gBAAgB,UAAU,OAAO,KAAK;AAC/C;AAWA,IAAM,UAAU;AAMhB,IAAM,YAAY;AA4BlB,SAAS,YAAY,OAAc,KAAuC;AAIxE,QAAM,QAAQ,IAAI,YAAY;AAC9B,MAAI,OAAO,MAAM,mBAAmB,cAAc,OAAO,MAAM,mBAAmB,WAAY,QAAO;AACrG,QAAM,OAAO,UAAU,MAAM,uBAAuB;AACpD,MAAI,SAAS,KAAM,QAAO;AAE1B,QAAM,MAAuB,CAAC;AAC9B,QAAM,SAAS,IAAI,iBAAiB,MAAM,SAAS;AACnD,WAAS,OAAO,OAAO,SAAS,GAAG,SAAS,MAAM,OAAO,OAAO,SAAS,GAAG;AAC1E,QAAI,CAAC,MAAM,eAAe,IAAI,EAAG;AACjC,UAAM,SAAS,KAAK;AACpB,QAAI,WAAW,KAAM;AACrB,QAAI,OAAO,QAAQ,OAAO,MAAM,KAAM;AACtC,QAAI,OAAO,QAAQ,QAAQ,MAAM,KAAM;AACvC,UAAM,QAAQ,IAAI,YAAY;AAC9B,UAAM,mBAAmB,IAAI;AAG7B,QAAI,SAAS,MAAM,eAAgB,OAAM,SAAS,MAAM,MAAM,WAAW;AACzE,QAAI,SAAS,MAAM,aAAc,OAAM,OAAO,MAAM,MAAM,SAAS;AACnE,eAAW,QAAQ,MAAM,eAAe,EAAG,KAAI,KAAK,IAAI;AAAA,EAC1D;AACA,SAAO;AACT;AASA,SAAS,SAAS,OAAc,KAAmC;AACjE,QAAM,OAAO,YAAY,OAAO,GAAG;AACnC,MAAI,SAAS,MAAM;AACjB,UAAM,MAAM,aAAa,IAAI;AAC7B,QAAI,QAAQ,KAAM,QAAO;AAAA,EAC3B;AAGA,QAAM,QAAQ,OAAO,MAAM,mBAAmB,aAAa,MAAM,eAAe,IAAI;AACpF,MAAI,UAAU,MAAM;AAClB,UAAM,MAAM,aAAa,MAAM,KAAK,KAAK,CAAC;AAC1C,QAAI,QAAQ,KAAM,QAAO;AAAA,EAC3B;AACA,QAAM,QAAQ,OAAO,MAAM,0BAA0B,aAAa,MAAM,sBAAsB,IAAI;AAClG,SAAO,UAAU,OAAO,OAAO,aAAa,CAAC,KAAK,CAAC;AACrD;AAGA,SAAS,WAAW,MAAmB,OAA6B;AAClE,SAAO,KAAK,SAAS,MAAM,QACtB,KAAK,UAAU,MAAM,SACrB,KAAK,QAAQ,MAAM,OACnB,KAAK,WAAW,MAAM;AAC7B;AAGA,SAAS,cAAc,MAAsB,OAAgC;AAC3E,MAAI,KAAK,SAAS,MAAM,KAAM,QAAO;AACrC,MAAI,KAAK,WAAW,QAAQ,MAAM,WAAW,KAAM,QAAO,KAAK,WAAW,MAAM;AAChF,SAAO,WAAW,KAAK,QAAQ,MAAM,MAAM;AAC7C;AAGA,SAAS,cAAc,KAA+B;AACpD,QAAM,YAAY,IAAI,aAAa;AACnC,MAAI,cAAc,QAAQ,UAAU,eAAe,UAAU,eAAe,EAAG,QAAO;AACtF,QAAM,OAAO,UAAU,SAAS,EAAE,KAAK;AACvC,MAAI,SAAS,GAAI,QAAO;AACxB,QAAM,QAAQ,UAAU,WAAW,CAAC;AACpC,QAAM,OAAO,UAAU,MAAM,uBAAuB;AACpD,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI,KAAK,QAAQ,UAAU,MAAM,KAAM,QAAO;AAC9C,MAAI,KAAK,QAAQ,QAAQ,MAAM,KAAM,QAAO;AAC5C,QAAM,SAAS,SAAS,OAAO,GAAG;AAClC,MAAI,WAAW,KAAM,QAAO;AAC5B,SAAO,EAAE,MAAM,OAAO;AACxB;AAGA,IAAM,oBAAoB;AAG1B,IAAM,eAAe;AAcd,SAAS,gBAAgB,KAAe,SAAiC;AAC9E,QAAM,YAAY,IAAI,aAAa;AACnC,QAAM,SAAkC,CAAC;AACzC,MAAI,cAAc,QAAQ,UAAU,eAAe,GAAG;AACpD,WAAO,YAAY;AAAA,EACrB,OAAO;AACL,UAAM,QAAQ,UAAU,WAAW,CAAC;AACpC,UAAM,QAAQ,OAAO,MAAM,mBAAmB,aAAa,MAAM,KAAK,MAAM,eAAe,CAAC,IAAI,CAAC;AACjG,UAAM,QAAQ,OAAO,MAAM,0BAA0B,aAAa,MAAM,sBAAsB,IAAI;AAClG,UAAM,WAAW,UAAU,MAAM,uBAAuB;AACxD,WAAO,YAAY;AAAA,MACjB,MAAM,UAAU,SAAS;AAAA;AAAA;AAAA;AAAA,MAIzB,OAAO,MAAM,IAAI,WAAS;AAAA,QACxB,MAAM,MAAM,KAAK,IAAI;AAAA,QACrB,OAAO,MAAM,KAAK,KAAK;AAAA,QACvB,KAAK,MAAM,KAAK,GAAG;AAAA,QACnB,QAAQ,MAAM,KAAK,MAAM;AAAA,QACzB,OAAO,MAAM,KAAK,QAAQ,KAAK,IAAI;AAAA,QACnC,QAAQ,MAAM,KAAK,SAAS,KAAK,GAAG;AAAA,MACtC,EAAE;AAAA,MACF,UAAU,UAAU,OAAO,OAAO;AAAA,QAChC,MAAM,MAAM,MAAM,IAAI;AAAA,QACtB,OAAO,MAAM,MAAM,KAAK;AAAA,QACxB,KAAK,MAAM,MAAM,GAAG;AAAA,QACpB,QAAQ,MAAM,MAAM,MAAM;AAAA,MAC5B;AAAA,MACA,OAAO,aAAa,KAAK;AAAA;AAAA,MAEzB,WAAW,aAAa,YAAY,OAAO,GAAG,KAAK,CAAC,CAAC;AAAA;AAAA;AAAA;AAAA,MAIrD,UAAU,SAAS,KAAK;AAAA,MACxB,OAAO,SAAS,MAAM,gBAAgB,MAAM,WAAW;AAAA,MACvD,KAAK,SAAS,MAAM,cAAc,MAAM,SAAS;AAAA,MACjD,WAAW,YAAY,QAAQ;AAAA,MAC/B,MAAM,aAAa,OAAO,OAAO,SAAS,UAAU,MAAM,GAAG,iBAAiB;AAAA,IAChF;AAAA,EACF;AACA,SAAO,UAAU,YAAY,OAAO,OAAO,WAAW,OAAO;AAC7D,QAAM,OAAO,IAAI;AACjB,SAAO,WAAW,SAAS,OAAO,OAAO,EAAE,OAAO,KAAK,YAAY,QAAQ,KAAK,YAAY;AAC5F,SAAO,KAAK,UAAU,QAAQ,MAAM,CAAC;AACvC;AASA,SAAS,SAAS,OAA6C;AAC7D,MAAI;AACF,UAAM,YAAY,MAAM,WAAW;AACnC,cAAU,SAAS,KAAK;AACxB,UAAM,QAAQ,OAAO,UAAU,mBAAmB,aAAa,UAAU,eAAe,IAAI;AAC5F,UAAM,QAAQ,UAAU,OAAO,SAAY,MAAM,CAAC;AAClD,QAAI,UAAU,OAAW,QAAO;AAChC,WAAO;AAAA,MACL,MAAM,MAAM,MAAM,IAAI;AAAA,MACtB,OAAO,MAAM,MAAM,KAAK;AAAA,MACxB,KAAK,MAAM,MAAM,GAAG;AAAA,MACpB,QAAQ,MAAM,MAAM,MAAM;AAAA,IAC5B;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYA,SAAS,SAAS,MAAY,QAAyC;AACrE,MAAI,KAAK,aAAa,GAAG;AACvB,WAAO;AAAA,MACL,MAAM;AAAA,MACN;AAAA,MACA,OAAO,KAAK,aAAa,IAAI,MAAM,GAAG;AAAA,MACtC,OAAO,YAAY,KAAK,aAAa,IAAI;AAAA,IAC3C;AAAA,EACF;AACA,SAAO;AAAA,IACL,MAAM,KAAK;AAAA,IACX;AAAA,IACA,MAAM;AAAA,IACN,OAAO,WAAW,KAAK,WAAW;AAAA,EACpC;AACF;AAGA,SAAS,YAAY,SAAmC;AACtD,QAAM,QAAkB,CAAC;AACzB,WAAS,OAAO,SAAS,SAAS,QAAQ,MAAM,SAAS,cAAc,OAAO,KAAK,eAAe;AAChG,UAAM,SAAS,MAAM,KAAK,KAAK,UAAU,EACtC,OAAO,eAAa,UAAU,KAAK,WAAW,OAAO,KAAK,UAAU,SAAS,OAAO,EACpF,IAAI,eAAa,GAAG,UAAU,IAAI,KAAK,UAAU,KAAK,GAAG,EACzD,KAAK,GAAG;AACX,UAAM,KAAK,GAAG,KAAK,QAAQ,YAAY,CAAC,GAAG,WAAW,KAAK,KAAK,IAAI,MAAM,EAAE,EAAE;AAAA,EAChF;AACA,SAAO;AACT;AAGA,SAAS,WAAW,SAA2C;AAC7D,QAAM,OAAO,QAAQ,sBAAsB;AAC3C,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,MAAM,MAAM,KAAK,IAAI;AAAA,MACrB,OAAO,MAAM,KAAK,KAAK;AAAA,MACvB,KAAK,MAAM,KAAK,GAAG;AAAA,MACnB,QAAQ,MAAM,KAAK,MAAM;AAAA,IAC3B;AAAA,IACA,QAAQ,mBAAmB,cAAc,EAAE,MAAM,QAAQ,MAAM,MAAM,KAAK,QAAQ,MAAM,IAAI,IAAI;AAAA,EAClG;AACF;AAOO,SAAS,2BAA2B,KAAqC;AAC9E,MAAI,WAAW;AACf,QAAM,YAAY,oBAAI,IAAgB;AACtC,QAAM,UAAU,MAAY;AAC1B,UAAM,OAAO,cAAc,GAAG;AAC9B,QAAI,cAAc,UAAU,IAAI,EAAG;AACnC,eAAW;AACX,eAAW,YAAY,UAAW,UAAS;AAAA,EAC7C;AACA,QAAM,MAAM,IAAI;AAChB,MAAI,iBAAiB,mBAAmB,OAAO;AAI/C,MAAI,iBAAiB,aAAa,OAAO;AACzC,MAAI,iBAAiB,SAAS,OAAO;AACrC,OAAK,iBAAiB,UAAU,SAAS,IAAI;AAC7C,OAAK,iBAAiB,UAAU,OAAO;AAcvC,QAAM,WAAW,OAAO,mBAAmB,cAAc,OAAO,IAAI,eAAe,MAAM;AAAE,YAAQ;AAAA,EAAE,CAAC;AACtG,aAAW,YAAY,CAAC,YAAY,QAAQ,GAAG;AAC7C,UAAM,UAAU,IAAI,cAAc,QAAQ;AAC1C,QAAI,YAAY,KAAM,WAAU,QAAQ,OAAO;AAAA,EACjD;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,MACN,aAAa,MAAM;AAAA,MACnB,WAAW,CAAC,aAAyB;AACnC,kBAAU,IAAI,QAAQ;AACtB,eAAO,MAAM;AAAE,oBAAU,OAAO,QAAQ;AAAA,QAAE;AAAA,MAC5C;AAAA,IACF;AAAA,IACA,SAAS,MAAM;AACb,UAAI,oBAAoB,mBAAmB,OAAO;AAClD,UAAI,oBAAoB,aAAa,OAAO;AAC5C,UAAI,oBAAoB,SAAS,OAAO;AACxC,WAAK,oBAAoB,UAAU,SAAS,IAAI;AAChD,WAAK,oBAAoB,UAAU,OAAO;AAC1C,gBAAU,WAAW;AACrB,gBAAU,MAAM;AAChB,iBAAW;AAAA,IACb;AAAA,EACF;AACF;;;AC9XA,IAAM,MAAM;AACZ,IAAM,WAAW;AACjB,IAAM,QAAQ;AACd,IAAI,OAAO,aAAa,aAAa;AACnC,aAAW,SAAS,SAAS,iBAAiB,wBAAwB,GAAG;AACvE,UAAM,KAAK,MAAM,QAAQ,aAAa;AACtC,QAAI,OAAO,UAAU,OAAO,YAAY,GAAG,WAAW,WAAW,GAAG,GAAI,OAAM,OAAO;AAAA,EACvF;AACF;AACA,IAAI,OAAO,aAAa,eAAe,SAAS,cAAc,4BAA4B,QAAQ,IAAI,MAAM,MAAM;AAChH,QAAM,MAAM,SAAS,cAAc,OAAO;AAC1C,MAAI,QAAQ,SAAS;AACrB,MAAI,QAAQ,YAAY;AACxB,MAAI,cAAc;AAClB,WAAS,KAAK,YAAY,GAAG;AAC/B;AACA,IAAO,uBAAQ,EAAC,WAAU,mBAAkB,QAAO,eAAc;;;AL+G7D;AArGJ,IAAM,aAAwB,EAAE,OAAO,GAAG,QAAQ,EAAE;AAQ7C,SAAS,aAAa,EAAE,mBAAmB,UAAU,cAAc,EAAE,GAAsB;AAChG,QAAM,YAAY,kBAAkB,cAAY,QAAQ;AACxD,QAAM,QAAQ,SAAS,WAAS,MAAM,KAAK;AAC3C,QAAM,eAAW,sBAAiC,IAAI;AAMtD,QAAM,cAAU,sBAA+B,IAAI;AACnD,QAAM,CAAC,MAAM,OAAO,QAAI,wBAAoB,UAAU;AAGtD,QAAM,CAAC,WAAW,YAAY,QAAI,wBAAkB,IAAI;AAExD,QAAM,SAA6B,UAAU;AAC7C,QAAM,WAAW,UAAU,SAAS,MAAM,WAAW;AACrD,QAAM,QAAQ,YAAY,cAAc;AASxC,qCAAgB,MAAM;AACpB,QAAI,CAAC,MAAO;AACZ,UAAM,QAAQ,SAAS;AAEvB,QAAI,UAAU,KAAM;AACpB,UAAM,QAAQ,MAAM;AACpB,UAAM,SAAS,MAAM;AACrB,QAAI,UAAU,KAAK,WAAW,EAAG;AACjC,QAAI,UAAU,KAAK,SAAS,WAAW,KAAK,OAAQ,SAAQ,EAAE,OAAO,OAAO,CAAC;AAAA,EAC/E,GAAG,CAAC,OAAO,WAAW,KAAK,OAAO,KAAK,MAAM,CAAC;AAI9C,+BAAU,MAAM;AACd,QAAI,CAAC,MAAO;AACZ,UAAM,YAAY,CAAC,UAA+B;AAChD,UAAI,MAAM,QAAQ,SAAU,cAAa,SAAS;AAAA,IACpD;AACA,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM;AAAE,eAAS,oBAAoB,WAAW,SAAS;AAAA,IAAE;AAAA,EACpE,GAAG,CAAC,OAAO,SAAS,CAAC;AAQrB,+BAAU,MAAM;AACd,QAAI,CAAC,SAAS,SAAS,YAAY,KAAM;AACzC,UAAM,SAAS,SAAS;AACxB,UAAM,YAAY,CAAC,UAA+B;AAChD,UAAI,MAAM,IAAI,YAAY,MAAM,OAAO,CAAC,MAAM,WAAW,CAAC,MAAM,SAAU;AAG1E,YAAM,eAAe;AACrB,YAAM,OAAO,gBAAgB,UAAU,MAAM;AAC7C,UAAI;AACF,aAAK,UAAU,WAAW,UAAU,IAAI;AAAA,MAC1C,QAAQ;AAAA,MAGR;AAAA,IACF;AACA,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM;AAAE,eAAS,oBAAoB,WAAW,SAAS;AAAA,IAAE;AAAA,EACpE,GAAG,CAAC,KAAK,CAAC;AAEV,MAAI,CAAC,SAAS,WAAW,KAAM,QAAO;AAMtC,QAAM,WAAW;AAAA,IACf;AAAA,IACA;AAAA,IACA,EAAE,OAAO,OAAO,YAAY,QAAQ,OAAO,YAAY;AAAA,IACvD,cAAc;AAAA,EAChB;AAKA,QAAM,QAAQ,EAAE,GAAG,UAAU,eAAe,OAAgB;AAE5D,SACE,4EAIE;AAAA,gDAAC,UAAK,KAAK,SAAS,QAAM,MAAC;AAAA,IAC3B,4CAAC,iBACC;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,WAAW,qBAAI;AAAA,QACf;AAAA,QAGA,sBAAmB;AAAA,QACnB,cAAY,EAAE,cAAc;AAAA,QAG5B,aAAa,WAAS;AAAE,gBAAM,eAAe;AAAA,QAAE;AAAA,QAC/C,SAAS,MAAM;AACb,uBAAa,SAAS,aAAa,OAAO,UAAU,IAAI,CAAC;AAGzD,iBAAO,aAAa,GAAG,gBAAgB;AACvC,2BAAiB,QAAQ,SAAS,QAAQ,sBAAsB,KAAK,IAAI;AAAA,QAC3E;AAAA,QAMA;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW,qBAAI;AAAA,cACf,OAAM;AAAA,cACN,QAAO;AAAA,cACP,SAAQ;AAAA,cACR,eAAY;AAAA,cACZ,WAAU;AAAA,cAEV;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,GAAE;AAAA;AAAA,cACJ;AAAA;AAAA,UACF;AAAA,UACC,EAAE,cAAc;AAAA;AAAA;AAAA,IACnB,GACF;AAAA,KACF;AAEJ;AASA,SAAS,gBAAwB;AAC/B,QAAM,aAAa,SAAS,cAAc,4BAA4B;AACtE,QAAM,WAAW,SAAS,cAAc,sBAAsB;AAC9D,SAAO;AAAA,IACL,eAAe,OAAO,OAAO,WAAW,sBAAsB;AAAA,IAC9D,aAAa,OAAO,OAAO,SAAS,sBAAsB;AAAA,IAC1D,EAAE,OAAO,OAAO,YAAY,QAAQ,OAAO,YAAY;AAAA,EACzD;AACF;AAWA,SAAS,iBAAiB,MAA4B;AACpD,QAAM,WAAW,MAAM,cAA2B,uBAAuB;AACzE,MAAI,aAAa,QAAQ,aAAa,OAAW;AACjD,WAAS,MAAM,EAAE,eAAe,KAAK,CAAC;AACtC,QAAM,QAAQ,SAAS,YAAY;AACnC,QAAM,mBAAmB,QAAQ;AACjC,QAAM,SAAS,KAAK;AACpB,QAAM,YAAY,OAAO,aAAa;AACtC,aAAW,gBAAgB;AAC3B,aAAW,SAAS,KAAK;AAC3B;;;AMlNO,IAAM,KAAK;AAAA,EAChB,gBAAgB;AAClB;AAMO,IAAM,KAAK;AAAA,EAChB,gBAAgB;AAClB;;;APgBA,IAAM,KAAK;AAGJ,IAAM,SAAS,CAAC,SAAS,QAAQ;AAMjC,SAAS,MAAM,KAA0B;AAC9C,MAAI,OAAO,MAAM,IAAI,OAAO,SAAS,IAAI,EAAE,IAAI,GAAG,CAAC,GAAG,wBAAwB;AAK9E,QAAM,YAAY,2BAA2B,QAAQ;AACrD,MAAI,OAAO,MAAM,UAAU,SAAS,8BAA8B;AAElE,MAAI,MAAM,OAAO,8BAA8B,MAAM,IAAI,MAAM,SAAS;AAAA,IACtE,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ,OAAsB,EAAE,OAAO,EAAE,gBAAgB,UAAU,OAAO,EAAE;AAAA,EAC9E,GAAG,YAAY,CAAC;AAClB;",
6
+ "names": ["import_react", "window"]
7
7
  }
@@ -1,21 +1,41 @@
1
1
  /**
2
2
  * Placement math for the floating quote control. The anchor is a selection
3
- * rect, not an element, so the shared element-anchored position hook does not
3
+ * box, not an element, so the shared element-anchored position hook does not
4
4
  * apply; this keeps the arithmetic pure so jsdom (which reports no layout) can
5
- * still cover the viewport clamps.
5
+ * still cover the region clamps and the vertical flip.
6
6
  *
7
7
  * The result is in VIEWPORT coordinates, matching the rect the anchor came
8
8
  * from: the control is absolutely positioned inside a layer host pinned to the
9
9
  * viewport origin, so no scroll offset is involved.
10
10
  * @module @deepseek-ai/dsh-client-ui-quote/placement
11
11
  */
12
- /** Viewport-relative box of the selection's last line, as the DOM reports it. */
12
+ /** Viewport-relative box of the selection, as the DOM reports it. */
13
13
  export interface QuoteAnchor {
14
14
  readonly left: number;
15
15
  readonly right: number;
16
16
  readonly top: number;
17
17
  readonly bottom: number;
18
18
  }
19
+ /** One rect the engine reported, in the subset the anchor reads. */
20
+ export interface SelectionRect {
21
+ readonly left: number;
22
+ readonly right: number;
23
+ readonly top: number;
24
+ readonly bottom: number;
25
+ }
26
+ /**
27
+ * The area the control is allowed to occupy, in viewport coordinates. It is the
28
+ * MESSAGE area, not the window: the composer card overlays the bottom of the
29
+ * transcript and the message column is narrower than the viewport, so clamping
30
+ * to `innerWidth`/`innerHeight` let the control land under the composer and
31
+ * hard against the window's right edge.
32
+ */
33
+ export interface Region {
34
+ readonly left: number;
35
+ readonly right: number;
36
+ readonly top: number;
37
+ readonly bottom: number;
38
+ }
19
39
  /** Measured size of the floating control. */
20
40
  export interface PanelSize {
21
41
  readonly width: number;
@@ -31,21 +51,59 @@ export interface QuotePlacement {
31
51
  readonly left: number;
32
52
  readonly top: number;
33
53
  }
54
+ /** A viewport-relative rect, as `getBoundingClientRect()` reports one. */
55
+ export interface ScreenBox {
56
+ readonly left: number;
57
+ readonly right: number;
58
+ readonly top: number;
59
+ readonly bottom: number;
60
+ }
34
61
  /**
35
- * Place the control below the selection's last line, right edges aligned to it,
36
- * clamped inside the viewport on both axes, and expressed in document
37
- * coordinates by adding the scroll offset.
62
+ * The area the control may occupy: the transcript's own box, cut off at the
63
+ * composer card when one is present, and intersected with the viewport.
38
64
  *
39
- * The result is in VIEWPORT coordinates, matching the rect the anchor came
40
- * from. The control is absolutely positioned inside a layer host that is itself
41
- * pinned to the viewport origin, so viewport coordinates are the right space and
42
- * no scroll offset is involved.
43
- * @param anchor - viewport rect of the selection's last line.
65
+ * Both cuts are load-bearing. The composer card overlays the bottom of the
66
+ * transcript, so a control clamped to the window's bottom edge lands ON the
67
+ * composer the "button floating above the input box" report. And the message
68
+ * column is narrower than the window, so clamping to `innerWidth` lets the
69
+ * control sit at the window's right edge, far from the text it quotes.
70
+ * @param transcript - the transcript scrollport's box, or null when absent.
71
+ * @param composer - the composer card's box, or null when absent.
72
+ * @param viewport - viewport extent, the fallback region and the outer bound.
73
+ * @returns the region, always a valid box inside the viewport.
74
+ */
75
+ export declare function quoteRegion(transcript: ScreenBox | null, composer: ScreenBox | null, viewport: ViewportSize): Region;
76
+ /**
77
+ * The selection's box: the union of the TEXT rects it covers.
78
+ *
79
+ * The caller supplies rects measured from text nodes, so an element's border box
80
+ * cannot be in the list. What remains can still hold a degenerate rect — a caret
81
+ * or a block boundary — and those are dropped: the browser paints no highlight
82
+ * for them, so they are not what the user selected. A single zero-width phantom
83
+ * at the column's edge is enough to move the control to the far side of the
84
+ * message area.
85
+ *
86
+ * Document order is deliberately not consulted. It is not reading order, and
87
+ * every rule that picked "the last rect" anchored to a fragment.
88
+ * @param rects - text rects, in any order.
89
+ * @returns the union box, or null when the list holds no paintable rect.
90
+ */
91
+ export declare function selectionBox(rects: Iterable<SelectionRect>): QuoteAnchor | null;
92
+ /**
93
+ * Place the control below the selection, right edges aligned to it, flipping
94
+ * above the selection when there is no room below, and clamped inside `region`.
95
+ *
96
+ * The flip and the region are both why the control never detaches from its
97
+ * text: clamping to the window's bottom edge left it floating above the
98
+ * composer, and clamping to the window's width left it at the far right of the
99
+ * message area.
100
+ * @param anchor - viewport box of the selection.
44
101
  * @param panel - measured panel size; zero before the first layout pass.
45
- * @param viewport - current viewport extent.
46
- * @param gap - distance kept between the anchor's bottom edge and the panel.
47
- * @param margin - distance kept between the panel and each viewport edge.
102
+ * @param viewport - current viewport extent, used when no region is given.
103
+ * @param region - the message area the control must stay inside.
104
+ * @param gap - distance kept between the selection edge and the panel.
105
+ * @param margin - distance kept between the panel and the region edge.
48
106
  * @returns the control's viewport coordinates.
49
107
  */
50
- export declare function placeQuoteControl(anchor: QuoteAnchor, panel: PanelSize, viewport: ViewportSize, gap?: number, margin?: number): QuotePlacement;
108
+ export declare function placeQuoteControl(anchor: QuoteAnchor, panel: PanelSize, viewport: ViewportSize, region?: Region, gap?: number, margin?: number): QuotePlacement;
51
109
  //# sourceMappingURL=placement.d.ts.map
@@ -11,7 +11,7 @@ import type { QuoteAnchor } from './placement.ts';
11
11
  export interface QuoteSelection {
12
12
  /** Selected text, trimmed; empty when nothing is quotable. */
13
13
  readonly text: string;
14
- /** Viewport rect of the selection's last line; null when nothing is quotable. */
14
+ /** Viewport box of the whole selection; null when nothing is quotable. */
15
15
  readonly anchor: QuoteAnchor | null;
16
16
  }
17
17
  /** Selection source handed to the slot component through the inject face. */
@@ -21,6 +21,19 @@ export interface QuoteSelectionSource {
21
21
  /** Detach every listener and reset the published snapshot. */
22
22
  dispose(): void;
23
23
  }
24
+ /**
25
+ * A paste-ready description of the live selection, for a page with no console.
26
+ *
27
+ * It answers, at ONE moment, every question a placement bug raises: the raw rect
28
+ * list the union was taken from, the selection box, both endpoints, the markup
29
+ * the selection sits in, the viewport, and — when the control is mounted — the
30
+ * rect and inline coordinates it actually got. The user copies it and pastes it;
31
+ * nothing here needs DevTools, and none of it runs during render.
32
+ * @param doc - the document to read.
33
+ * @param control - the mounted control, or null when it is not on screen.
34
+ * @returns the report as pretty-printed JSON.
35
+ */
36
+ export declare function selectionReport(doc: Document, control: Element | null): string;
24
37
  /**
25
38
  * Track the document selection and publish each distinct quotable snapshot.
26
39
  * @param doc - the document whose selection is tracked.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkerxie/dsh-plugin-quote",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Select text in the DeepSeek Harness Web conversation and append it to the composer as a markdown blockquote.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,6 +38,7 @@
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "node scripts/build.mjs && tsc --emitDeclarationOnly",
41
+ "sync": "npm run build && node scripts/sync-profile.mjs",
41
42
  "prepublishOnly": "npm run build",
42
43
  "typecheck": "tsc --noEmit",
43
44
  "test": "vitest run"
@@ -47,7 +48,8 @@
47
48
  "dsh",
48
49
  "dsh-plugin",
49
50
  "cordis",
50
- "quote"
51
+ "quote",
52
+ "dsh-plugin-quote"
51
53
  ],
52
54
  "repository": {
53
55
  "type": "git",