dsh-client-ui-quote 0.11.1 → 0.11.2

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,7 +55,7 @@ DSH 会把包记进 profile 的依赖并加入组合包列表,装完在侧边
55
55
 
56
56
  ### 方式三:npm 包(已上架)
57
57
 
58
- 已发布到 npm:`dsh-client-ui-quote@0.11.1`(0.10.0 起上架)。发布由 GitHub Actions 完成(`.github/workflows/publish.yml`):推 `v*` 标签或手动 Run `publish` workflow,`prepublishOnly` 的 290 项断言会先在 runner 上跑完,任何一项失败都不会发出去。`npm pack --dry-run` 可核对内容(0.10.0 = 14 个文件 / 97.6 kB)。
58
+ 已发布到 npm:`dsh-client-ui-quote`(0.10.0 起上架;每个 `v*` 标签由下面的 workflow 自动发布,本仓库当前 0.11.2)。发布由 GitHub Actions 完成(`.github/workflows/publish.yml`):推 `v*` 标签或手动 Run `publish` workflow,`prepublishOnly` 的 293 项断言会先在 runner 上跑完,任何一项失败都不会发出去。`npm pack --dry-run` 可核对内容(0.10.0 = 14 个文件 / 97.6 kB)。
59
59
 
60
60
  装:
61
61
 
@@ -227,7 +227,7 @@ transcript 侧的 `.dshq-cap` 胶囊:一颗胶囊同时装引用与评论,`
227
227
  1. **语义而不是机制**:引用与评论是同一颗节点(对齐 Kimi 的 `quote` 节点 `{text, comment}`),一颗芯片 → 一个块 → 一颗胶囊 → 一张卡片 → 一次复制;机制(reference chip、DOM 增强)谁都能抄,这条链子不好抄。
228
228
  2. **引用是带坐标的活对象**:`跳回原文` 在自己的会话面板里找行、按宿主的方式对齐到 24px,行被虚拟列表藏起来或没挂载都能找回来;坐标不随引用文本发给模型(不污染 prompt)。
229
229
  3. **引用有下落**:清单页(搜索 / 跳转 / 导出 Markdown)+ 侧边线程回流(结论写回主会话草稿 / 导出线程),这两件事在 4471 条里没有对应物。
230
- 4. **工程化**:290 项离线断言(含真跑 `sessions.retain` / `setDraft` / 下载链路)、每个宿主契约都在 README 里写明出处、每个版本都有可回读的 release。同类里多数是一两百行、没有测试的插件。
230
+ 4. **工程化**:293 项离线断言(含真跑 `sessions.retain` / `setDraft` / 下载链路)、每个宿主契约都在 README 里写明出处、每个版本都有可回读的 release。同类里多数是一两百行、没有测试的插件。
231
231
 
232
232
  反过来也要说清楚:**本插件不做的**——不做翻译/解释类选区动作(81 条同类在做)、不做多选合并引用(7 条有)、不做选区折叠/行内 diff 这类渲染花样。它只把「引用」这一件事做穿。
233
233
 
@@ -260,7 +260,7 @@ locale/zh.json 插件页卡片文案(中文)
260
260
  locale/en.json 插件页卡片文案(英文)
261
261
  test/check.mjs 离线自检(清单 + 组件契约)
262
262
  test/dom.mjs 给离线自检用的最小 DOM / MutationObserver 垫片
263
- test/capsule.mjs 离线自检(发送后胶囊 + 悬停卡片 + 跳回原文 + 键位 + 改这条引用 + 代码引用 + 文件路径 + 逐条回应的回复徽章,194 项断言)
263
+ test/capsule.mjs 离线自检(发送后胶囊 + 悬停卡片 + 跳回原文 + 键位 + 改这条引用 + 代码引用 + 文件路径 + 逐条回应的回复徽章,197 项断言)
264
264
  test/panel.mjs 离线自检(引用清单:解析 / 搜索 / Markdown / 读取 / 导出 / 页面注册 + 侧边线程回流,96 项断言)
265
265
  test/capsule.html 真浏览器里跑同一套胶囊断言(可选)
266
266
  ```
@@ -308,7 +308,7 @@ React 重渲染不会出现第二颗、引用变了就地重建、消息被删
308
308
  卡片给出 `跳回原文` 与 `改这条引用` 两个动作、点后者把栏切到评论模式并**预填旧评论与旧正文**、
309
309
  按确认后 `insertReference` 恰好一次(新芯片的 `ref` 带改后的评论)、旧引用被记为「已修订」并落进 `localStorage`、
310
310
  旧胶囊挂上 `已修订` 徽标、它的卡片显示改后的评论(连正文改过的连改后原文一起显示)、空修订不算修订、
311
- 修订不改写引用原文本身、清空引用正文时确认按钮禁用、改后的正文随新芯片发出(共 194 项,含文件路径与逐条回应的线上形态、回复徽章的建立/重建/卸载与悬停卡片复用)。
311
+ 修订不改写引用原文本身、清空引用正文时确认按钮禁用、改后的正文随新芯片发出(共 197 项,含文件路径与逐条回应的线上形态、回复徽章的建立/重建/卸载与悬停卡片复用,以及评论框两个输入区的自适应高度——引用正文那格曾经在被挂进栏之前就量高,读到 0 后被写成 `height:0`,首行因此被裁掉)。
312
312
  `test/capsule.html` 是同一套断言的真浏览器版,用 `file://` 打开即可(页面会把结果写进标题与 `#probe-out`)。
313
313
 
314
314
  `test/panel.mjs` 单独跑引用清单页与侧边线程回流(96 项):只认**持久**的 `user/message`(`transient`、助手消息、`data: null` 都跳过)、
@@ -327,7 +327,7 @@ Markdown 的标题/编号/块引用/评论行/时间戳与空列表兜底、超
327
327
  `bringThreadBack` 把结论写成**草稿**(原有草稿保留、空行隔开)、外壳没有 `setDraft` 时如实失败、没有线程时不乱写;
328
328
  `exportThread` 的文件名与整条线程内容、以及 MIME 是 Markdown。
329
329
 
330
- 合计 **290 项**(check.mjs 的契约断言 + capsule 194 + panel 96),`npm test` 全绿。
330
+ 合计 **293 项**(check.mjs 的契约断言 + capsule 197 + panel 96),`npm test` 全绿。
331
331
 
332
332
  ## 已知限制
333
333
 
package/lib/client.js CHANGED
@@ -2160,6 +2160,29 @@ code's own spacing — collapsed keeps one clipped line, expanded scrolls. */
2160
2160
  * its wording, not just its comment. Both halves map onto the wire form's
2161
2161
  * two marks, top to bottom.
2162
2162
  */
2163
+ /**
2164
+ * Grow one field to its content. A textarea that is not laid out yet
2165
+ * reports a scrollHeight of 0 — writing that back as `0px` collapsed the
2166
+ * quote half of the box and clipped its first line, so an unmeasurable
2167
+ * field is left on `auto` until the bar is on screen.
2168
+ */
2169
+ function growField(node) {
2170
+ node.style.height = "auto";
2171
+ const next = node.scrollHeight;
2172
+ if (!Number.isFinite(next) || next <= 0) return;
2173
+ node.style.height = Math.min(next, COMMENT_MAX_HEIGHT) + "px";
2174
+ }
2175
+
2176
+ /** Every field the bar is currently showing, re-measured. */
2177
+ function growInputs() {
2178
+ const bar = state.bar;
2179
+ if (bar === null || bar === undefined || typeof bar.querySelectorAll !== "function") return;
2180
+ for (const field of Array.from(bar.querySelectorAll(".dshq-input"))) {
2181
+ if (String(field.tagName).toLowerCase() !== "textarea") continue;
2182
+ growField(field);
2183
+ }
2184
+ }
2185
+
2163
2186
  function renderComment(bar) {
2164
2187
  const doc = bar.ownerDocument;
2165
2188
  bar.setAttribute("role", "dialog");
@@ -2186,10 +2209,6 @@ code's own spacing — collapsed keeps one clipped line, expanded scrolls. */
2186
2209
  "data-action": "confirm"
2187
2210
  }, [el(doc, "span", { text: translate("addToChat") }), el(doc, "span", { class: "dshq-enter", text: "⏎" })]);
2188
2211
 
2189
- const grow = (node) => {
2190
- node.style.height = "auto";
2191
- node.style.height = Math.min(node.scrollHeight, COMMENT_MAX_HEIGHT) + "px";
2192
- };
2193
2212
  // The quote half is mandatory now (an empty quote is nothing to send);
2194
2213
  // the comment keeps Kimi's rule of being required in this box.
2195
2214
  const syncConfirm = () => {
@@ -2197,12 +2216,12 @@ code's own spacing — collapsed keeps one clipped line, expanded scrolls. */
2197
2216
  };
2198
2217
  quoteInput.addEventListener("input", () => {
2199
2218
  state.quote = quoteInput.value;
2200
- grow(quoteInput);
2219
+ growField(quoteInput);
2201
2220
  syncConfirm();
2202
2221
  });
2203
2222
  input.addEventListener("input", () => {
2204
2223
  state.comment = input.value;
2205
- grow(input);
2224
+ growField(input);
2206
2225
  syncConfirm();
2207
2226
  });
2208
2227
  const onKey = (event) => {
@@ -2234,7 +2253,8 @@ code's own spacing — collapsed keeps one clipped line, expanded scrolls. */
2234
2253
  el(doc, "span", { text: translate("replyToggle") })
2235
2254
  ]);
2236
2255
 
2237
- grow(quoteInput);
2256
+ // No grow here: the fields are measured in `position()`, once the bar
2257
+ // takes part in layout. Reading them while detached returned 0.
2238
2258
  syncConfirm();
2239
2259
 
2240
2260
  bar.replaceChildren(
@@ -2258,6 +2278,10 @@ code's own spacing — collapsed keeps one clipped line, expanded scrolls. */
2258
2278
 
2259
2279
  bar.style.visibility = "hidden";
2260
2280
  bar.dataset.visible = "true";
2281
+ // The bar is in layout now (merely invisible), which is the first
2282
+ // moment its fields can be measured — and it has to happen before the
2283
+ // height below is read, or the box would be placed at the wrong place.
2284
+ growInputs();
2261
2285
 
2262
2286
  const viewport = window.visualViewport;
2263
2287
  const width = viewport === undefined || viewport === null ? window.innerWidth : viewport.width;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-client-ui-quote",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Select text in the DSH transcript, a file preview or the terminal to quote it into the composer or into a side thread's composer, Kimi-Code style (comment / add to conversation / side chat), with Alt+Q/C/B, a jump back to the quoted source, a searchable quote list page that exports as Markdown, one-click back-flow of a side thread's conclusion, and revisions of a sent quote that mark the old capsule as superseded. The quoted text itself is editable in the comment box (trim a selection, correct a revision's wording), a selection from the file preview or the terminal travels to the model as a fenced code block carrying its file path and line range, and a per-quote reply toggle has the model answer quote by quote, each 「引用N」 label in the reply becoming a badge linked back to the quote.",
5
5
  "icon": "icon.svg",
6
6
  "license": "MIT",
package/test/capsule.mjs CHANGED
@@ -914,6 +914,21 @@ try {
914
914
  // this block is about what a mounted enhancer does with a revision.
915
915
  capsuleEffect.fn();
916
916
 
917
+ // A textarea measures itself from its own text — but only while it is in the
918
+ // document, the way the real one reads 0 before the bar is laid out. That is
919
+ // what made the auto-grow collapse the quote field.
920
+ const makeTextarea = dom.document.createElement.bind(dom.document);
921
+ dom.document.createElement = (tag) => {
922
+ const node = makeTextarea(tag);
923
+ if (String(tag).toLowerCase() === "textarea") {
924
+ Object.defineProperty(node, "scrollHeight", {
925
+ configurable: true,
926
+ get: () => (node.isConnected ? 12 + 12 * String(node.value ?? "").split("\n").length : 0)
927
+ });
928
+ }
929
+ return node;
930
+ };
931
+
917
932
  // A transcript of its own, with a capsule that has a comment.
918
933
  const host2 = dom.document.createElement("div");
919
934
  host2.setAttribute("data-conversation-content", "");
@@ -948,11 +963,16 @@ try {
948
963
  const input = bar?.querySelector(".dshq-comment-input");
949
964
  check("the box is prefilled with the old quote for editing", quoteBox?.value === original, JSON.stringify(quoteBox?.value));
950
965
  check("the box is prefilled with the old comment", input?.value === oldComment, JSON.stringify(input?.value));
966
+ // Regression: the quote half used to be measured before it was attached, so
967
+ // it was written `height: 0px` and its first line showed up clipped.
968
+ check("the quote field is grown to its text, not collapsed", quoteBox?.style?.height === "24px", JSON.stringify(quoteBox?.style?.height));
969
+ check("the comment field is grown too", input?.style?.height === "24px", JSON.stringify(input?.style?.height));
951
970
 
952
971
  // Correcting it writes a NEW quote (nothing is edited in place) and the old
953
972
  // capsule learns it was superseded.
954
973
  input.value = newComment;
955
974
  input.dispatch("input", {});
975
+ check("typing in a field re-measures it", input?.style?.height === "24px", JSON.stringify(input?.style?.height));
956
976
  // The bar handles clicks by delegation, so the click is dispatched on the bar
957
977
  // with the button as its target — the shim has no bubbling of its own.
958
978
  const confirm = bar.querySelector('[data-action="confirm"]');