dsh-select-quote 0.3.11 → 0.3.13

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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/lib/client.js +232 -55
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  - **右键菜单**:在选区矩形内点右键,同一套「复制 / 打开链接 / 添加到任务」出现在指针处;选区之外、其它插件已接管的右键一律不碰,桌面端未接管时保留宿主的原生 Copy 菜单
7
7
  - **快捷键**:选中文字后一键打开菜单或直接添加;桌面 macOS 是 `⌘L` / `⇧⌘L`,其它平台的默认键位见下表;可在设置里单独开关
8
8
  - **批注摘要**:添加后输入框上方出现一块「N 条批注」摘要,草稿里不会出现任何协议文本
9
- - **正文标记**:被划中的文字在原消息里留下淡色底、虚线下划线和序号角标,点摘要条目会滚回那段文字;点标记或角标会打开该条批注的预览(原文 + 评论),角标也能用 Tab 聚焦、Enter 打开。角标始终压在选区高亮之上,多条批注结尾重合时会自动向右/向上错开
9
+ - **正文标记**:被划中的文字在原消息里留下淡色底、虚线下划线和序号角标,点摘要条目会滚回那段文字;点角标会打开该条批注的预览(原文 + 评论),标记本身保持可选中、不抢点击,角标也能用 Tab 聚焦、Enter 打开。角标始终压在选区高亮之上,多条批注结尾重合时会自动向右/向上错开
10
10
  - **模型回指**:模型回答里指向某条批注的位置,渲染成带预览的 chip
11
11
  - **对话记录**:已发送消息仍由宿主气泡渲染(时间、复制、撤回都在),批注收成一块摘要,悬停(触屏点按)展开列表
12
12
  - **界面语言**:跟随 dsh 自己的语言设置,简体中文 / English / Русский
@@ -44,7 +44,7 @@ dsh web --patch "$PWD/dev.patch.yml"
44
44
  1. 在对话区选中一段文本,点「添加到任务」——到这一步批注就已经保存,输入框上方同步出现批注摘要。触屏下操作条固定在屏幕底部,不跟随选区挤在系统选择菜单旁边
45
45
  2. 想补充的话在弹出的评论框里写一句,点「添加评论」或 `⌘/Ctrl + Enter`;不写就直接点空白处或「暂不评论」,批注同样保留。触屏下评论在底部抽屉里完成,抽屉顶部会带上选中的原文
46
46
  3. 悬停(触屏点按)输入框上方的摘要展开列表,可逐条编辑、删除或一键清空,点条目跳回正文对应文字;对话记录里的摘要只提供列表和跳转。键盘聚焦摘要后按 Enter / ↓ 展开,Tab 在面板内移动,Esc 关闭并回到摘要
47
- 4. 点正文里的标记或角标(角标可用 Tab 聚焦后按 Enter)打开批注预览,查看原文和评论;点浮层外或按 Esc 关闭
47
+ 4. 点正文里的角标(可用 Tab 聚焦后按 Enter)打开批注预览,查看原文和评论;正文标记本身仍可正常鼠标划选,点浮层外或按 Esc 关闭
48
48
  5. 正常打字发送,批注随消息一起交给模型,摘要随之消失
49
49
  6. 模型回答里引用某条批注的位置显示为 chip:悬停、点击(触屏点按)或键盘聚焦都会打开预览,显示原文和评论;预览默认在 chip 上方,右上角「原文」按钮回到正文;键盘聚焦 chip 后按 ↓ 进入按钮,Esc 关闭
50
50
 
@@ -88,7 +88,7 @@ dsh web --patch "$PWD/dev.patch.yml"
88
88
 
89
89
  ## 已知限制
90
90
 
91
- - **待发批注按会话持久化**:刷新页面、重启客户端后回到原会话,未发送的批注会原样回来(与草稿同在 localStorage,键各自独立);保留最近 20 个会话、7 天。发送即清除,切换会话跟着切,hero 里换工作区等于换会话,批注不跟着搬。同一会话开多个标签页时通过 `storage` 事件同步,按最后写入为准;一个标签发送清空后,另一个标签不会把旧批注写回。
91
+ - **待发批注按会话持久化**:刷新页面、重启客户端后回到原会话,未发送的批注会原样回来(与草稿同在 localStorage,键各自独立);保留最近 20 个会话、7 天。发送即清除,切换会话跟着切——正文的划词标记与输入框里的批注卡同进同退:分叉出的新会话不继承批注,也**不会**留下原会话的划词;切回原会话时两者一起恢复。hero 里换工作区等于换会话,批注不跟着搬。同一会话开多个标签页时通过 `storage` 事件同步,按最后写入为准;一个标签发送清空后,另一个标签不会把旧批注写回。
92
92
  - **占用了用户气泡的渲染**:本插件以 `priority: -10` 接管内置 `user` / `steering` 节点,再委托宿主 `UserMessageNodeView` 渲染(只改写 `node` 的文本块,剥掉协议)。其它插件若也想接管这两个键,优先级数值必须小于 `-10` 才会赢过本插件;被别的接管者赢走时,本插件不再折叠消息里的协议块。
93
93
  - **摘要宽度有限**:输入框上方只有一块摘要(约输入框的 1/5,148–220px),悬停列表里的原文和评论最多显示三行;编辑批注时原文完整显示,超过 132px 在框内滚动。
94
94
  - **划词只在对话区生效**:不介入编辑器、终端等面板。
package/lib/client.js CHANGED
@@ -1408,7 +1408,9 @@ ${atomRules}
1408
1408
  font-size: 10px;
1409
1409
  font-weight: 600;
1410
1410
  line-height: 1;
1411
- pointer-events: none;
1411
+ /* The only click target on a mark; the prose itself stays selectable. */
1412
+ pointer-events: auto;
1413
+ cursor: pointer;
1412
1414
  }
1413
1415
 
1414
1416
  .dsq_annMark:hover {
@@ -1492,22 +1494,32 @@ ${atomRules}
1492
1494
  padding: 2px 0;
1493
1495
  }
1494
1496
 
1497
+ /* Both settings rows follow the Host pattern: copy on the left, control on
1498
+ the right. The trigger control wraps below only when the row is too narrow. */
1495
1499
  .dsq_settingsSection {
1496
1500
  display: flex;
1497
- flex-direction: column;
1498
- gap: 10px;
1501
+ align-items: center;
1502
+ justify-content: space-between;
1503
+ flex-wrap: wrap;
1504
+ gap: 8px 16px;
1499
1505
  }
1500
1506
 
1501
1507
  .dsq_settingsCopy {
1502
1508
  display: flex;
1509
+ min-width: 0;
1503
1510
  flex-direction: column;
1504
1511
  gap: 3px;
1505
1512
  }
1506
1513
 
1514
+ .dsq_settingsSection .dsq_settingsCopy {
1515
+ flex: 1 1 240px;
1516
+ }
1517
+
1507
1518
  .dsq_settingsLabel {
1508
1519
  color: var(--dsq-label);
1509
1520
  font-size: 13px;
1510
1521
  font-weight: 500;
1522
+ line-height: 18px;
1511
1523
  }
1512
1524
 
1513
1525
  .dsq_settingsHint {
@@ -1564,8 +1576,9 @@ ${atomRules}
1564
1576
 
1565
1577
  .dsq_seg {
1566
1578
  display: inline-flex;
1567
- align-self: flex-start;
1579
+ flex: none;
1568
1580
  gap: 2px;
1581
+ margin-left: auto;
1569
1582
  padding: 2px;
1570
1583
  border-radius: 8px;
1571
1584
  background: var(--dsq-fill-hover);
@@ -1807,13 +1820,41 @@ const styles = {
1807
1820
  segOption: "dsq_segOption",
1808
1821
  srOnly: "dsq_srOnly"
1809
1822
  };
1810
- //#endregion
1811
- //#region src/client/dom/badge-layout.ts
1823
+ const BADGE_MIN_WIDTH = 15;
1824
+ /** 10px/600 digits plus the 3px side padding. */
1825
+ const BADGE_DIGIT_WIDTH = 6;
1826
+ const BADGE_PADDING = 6;
1827
+ const HIT_SLOP = 3;
1812
1828
  const STEP_X = 28;
1813
1829
  const STEP_Y = 18;
1814
1830
  /** Rest, then two steps right; after that the stack grows upward. */
1815
1831
  const MAX_COLUMNS = 3;
1816
1832
  const MAX_ROWS = 24;
1833
+ /** Rendered badge width, matching `min-width` + digit advance + padding. */
1834
+ function badgeWidth(index) {
1835
+ return Math.max(BADGE_MIN_WIDTH, String(index).length * BADGE_DIGIT_WIDTH + BADGE_PADDING);
1836
+ }
1837
+ /**
1838
+ * Hit box of a badge as painted: anchored to the final fragment's bottom-right
1839
+ * corner, then shifted by the collision offset. Used to tell a badge click from
1840
+ * a click on the marked prose.
1841
+ */
1842
+ function badgeHitBox(anchor, index, offset = {
1843
+ dx: 0,
1844
+ dy: 0
1845
+ }) {
1846
+ const left = anchor.right + 2 + offset.dx;
1847
+ const top = anchor.bottom - 8 - 15 + offset.dy;
1848
+ return {
1849
+ left: left - HIT_SLOP,
1850
+ top: top - HIT_SLOP,
1851
+ right: left + badgeWidth(index) + HIT_SLOP,
1852
+ bottom: top + 15 + HIT_SLOP
1853
+ };
1854
+ }
1855
+ function badgeContainsPoint(box, x, y) {
1856
+ return x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
1857
+ }
1817
1858
  function overlaps(a, b) {
1818
1859
  return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
1819
1860
  }
@@ -1993,20 +2034,83 @@ function cleanTextOf(item) {
1993
2034
  const MARK_ATTR = "data-dsq-ann-mark";
1994
2035
  const INDEX_ATTR = "data-dsq-ann-index";
1995
2036
  const FLASH_MS = 1500;
1996
- /** Distance from the mark's bottom edge to the badge's bottom edge (CSS). */
1997
- const BADGE_BOTTOM = 8;
1998
- const BADGE_MARGIN = 2;
1999
2037
  const jobs = /* @__PURE__ */ new Map();
2000
2038
  let pendingIds = /* @__PURE__ */ new Set();
2001
2039
  function attrValue(value) {
2002
2040
  return value.replace(/["\\]/g, "\\$&");
2003
2041
  }
2004
- function flowItems(messageId, scope) {
2005
- if (messageId !== "") {
2006
- const item = scope.querySelector(`[data-chat-flow-key="${attrValue(messageId)}"]`);
2007
- return item instanceof HTMLElement ? [item] : [];
2042
+ const FLOW_SELECTOR = "[data-chat-flow-key]";
2043
+ /**
2044
+ * One pass over the rendered messages instead of one query per annotation.
2045
+ * `querySelectorAll` preserves document order, so the first copy of a key wins,
2046
+ * matching the old per-job `querySelector`.
2047
+ */
2048
+ function flowLookup(scope) {
2049
+ const all = [...scope.querySelectorAll(FLOW_SELECTOR)];
2050
+ const byKey = /* @__PURE__ */ new Map();
2051
+ for (const item of all) {
2052
+ const key = item.getAttribute("data-chat-flow-key");
2053
+ if (key && !byKey.has(key)) byKey.set(key, item);
2054
+ }
2055
+ return {
2056
+ all,
2057
+ byKey
2058
+ };
2059
+ }
2060
+ function flowKeyOf(item) {
2061
+ return item.getAttribute("data-chat-flow-key") ?? "";
2062
+ }
2063
+ /**
2064
+ * An annotation's source may sit in a block nested inside its message (for
2065
+ * example a reasoning panel), so a touched flow item answers to its own key
2066
+ * and every enclosing message key.
2067
+ */
2068
+ function anchorKeysOf(item) {
2069
+ const keys = [];
2070
+ for (let el = item; el; el = el.parentElement?.closest(FLOW_SELECTOR) ?? null) {
2071
+ const key = el.getAttribute("data-chat-flow-key");
2072
+ if (key) keys.push(key);
2073
+ }
2074
+ return keys;
2075
+ }
2076
+ /** Items in a partial pass, grouped by flow key; other messages are not touched. */
2077
+ function groupByFlowKey(items) {
2078
+ const grouped = /* @__PURE__ */ new Map();
2079
+ for (const item of items) for (const key of anchorKeysOf(item)) {
2080
+ const list = grouped.get(key) ?? [];
2081
+ list.push(item);
2082
+ grouped.set(key, list);
2083
+ }
2084
+ return grouped;
2085
+ }
2086
+ /** The flow item that owns a mutated node, if any. */
2087
+ function ownerFlowItem(node) {
2088
+ if (!node) return null;
2089
+ if (node instanceof Element) return node.closest(FLOW_SELECTOR);
2090
+ return node.parentElement?.closest(FLOW_SELECTOR) ?? null;
2091
+ }
2092
+ function affectedFlowItems(records) {
2093
+ const items = /* @__PURE__ */ new Set();
2094
+ const addOwner = (node) => {
2095
+ const owner = ownerFlowItem(node);
2096
+ if (owner?.isConnected) items.add(owner);
2097
+ };
2098
+ /** An added wrapper can hold several newly rendered messages. */
2099
+ const addSubtree = (node) => {
2100
+ const owner = ownerFlowItem(node);
2101
+ if (owner) {
2102
+ if (owner.isConnected) items.add(owner);
2103
+ return;
2104
+ }
2105
+ if (!(node instanceof Element)) return;
2106
+ for (const nested of node.querySelectorAll(FLOW_SELECTOR)) if (nested.isConnected) items.add(nested);
2107
+ };
2108
+ for (const record of records) {
2109
+ addOwner(record.target);
2110
+ if (record.type !== "childList") continue;
2111
+ for (const node of record.addedNodes) addSubtree(node);
2008
2112
  }
2009
- return [...scope.querySelectorAll("[data-chat-flow-key]")].filter((node) => node instanceof HTMLElement);
2113
+ return items;
2010
2114
  }
2011
2115
  /**
2012
2116
  * Whitespace-insensitive comparison: `selection.toString()` inserts newlines
@@ -2133,7 +2237,8 @@ function refreshAnnotationMarkLabels() {
2133
2237
  * offset (nested ranges), which puts their pseudo-element badges on the same
2134
2238
  * pixel; nearby ends can also touch. Measure every last fragment, then write
2135
2239
  * the offset back as variables inherited by `::after`. The badge's transform
2136
- * never participates in text layout.
2240
+ * never participates in text layout. Collisions are resolved inside each
2241
+ * message block, and a stack may not climb into the message above.
2137
2242
  */
2138
2243
  function layoutBadges(scope) {
2139
2244
  const marks = Array.from(scope.querySelectorAll(`[${INDEX_ATTR}]`));
@@ -2145,37 +2250,57 @@ function layoutBadges(scope) {
2145
2250
  const scroller = marks[0]?.closest("[data-conversation-scroll]");
2146
2251
  const scrollerRect = scroller instanceof Element ? scroller.getBoundingClientRect() : null;
2147
2252
  const rightEdge = scrollerRect && scrollerRect.width > 0 ? scrollerRect.right : document.documentElement.clientWidth;
2148
- const topEdge = scrollerRect && scrollerRect.height > 0 ? scrollerRect.top : 0;
2149
- const anchors = marks.map((mark) => {
2150
- const rect = mark.getBoundingClientRect();
2151
- return {
2152
- mark,
2153
- index: Number(mark.getAttribute(INDEX_ATTR) ?? 0),
2154
- x: rect.right + BADGE_MARGIN,
2155
- y: rect.bottom - BADGE_BOTTOM - 15,
2156
- width: 24,
2157
- height: 15,
2158
- visible: rect.width > 0 && rect.height > 0
2159
- };
2160
- }).filter((anchor) => anchor.visible).sort((a, b) => a.index - b.index);
2161
- if (anchors.length < 2) return;
2162
- const offsets = placeBadges(anchors, {
2163
- right: rightEdge - 4,
2164
- top: topEdge + 4
2165
- });
2166
- anchors.forEach((anchor, i) => {
2167
- const { dx, dy } = offsets[i];
2168
- if (dx !== 0) anchor.mark.style.setProperty("--dsq-ann-badge-x", `${dx}px`);
2169
- if (dy !== 0) anchor.mark.style.setProperty("--dsq-ann-badge-y", `${dy}px`);
2170
- });
2253
+ const scrollerTop = scrollerRect && scrollerRect.height > 0 ? scrollerRect.top : 0;
2254
+ const groups = /* @__PURE__ */ new Map();
2255
+ for (const mark of marks) {
2256
+ const owner = mark.closest(FLOW_SELECTOR);
2257
+ const group = groups.get(owner);
2258
+ if (group) group.push(mark);
2259
+ else groups.set(owner, [mark]);
2260
+ }
2261
+ for (const [owner, group] of groups) {
2262
+ if (group.length < 2) continue;
2263
+ const ownerRect = owner?.getBoundingClientRect();
2264
+ const topEdge = Math.max(scrollerTop, ownerRect && ownerRect.height > 0 ? ownerRect.top : scrollerTop) + 4;
2265
+ const anchors = group.map((mark) => {
2266
+ const rect = mark.getBoundingClientRect();
2267
+ return {
2268
+ mark,
2269
+ index: Number(mark.getAttribute(INDEX_ATTR) ?? 0),
2270
+ x: rect.right + 2,
2271
+ y: rect.bottom - 8 - 15,
2272
+ width: 24,
2273
+ height: 15,
2274
+ visible: rect.width > 0 && rect.height > 0
2275
+ };
2276
+ }).filter((anchor) => anchor.visible).sort((a, b) => a.index - b.index);
2277
+ if (anchors.length < 2) continue;
2278
+ const offsets = placeBadges(anchors, {
2279
+ right: rightEdge - 4,
2280
+ top: topEdge
2281
+ });
2282
+ anchors.forEach((anchor, i) => {
2283
+ const { dx, dy } = offsets[i];
2284
+ if (dx !== 0) anchor.mark.style.setProperty("--dsq-ann-badge-x", `${dx}px`);
2285
+ if (dy !== 0) anchor.mark.style.setProperty("--dsq-ann-badge-y", `${dy}px`);
2286
+ });
2287
+ }
2171
2288
  }
2172
- /** Apply every registered job whose message is currently rendered. */
2173
- function applyAnnotationMarks(root) {
2289
+ /**
2290
+ * Apply registered jobs. With `only`, touch just those rendered messages — the
2291
+ * mutation-driven path — instead of resolving and scanning every message for
2292
+ * every job. Badge collisions live inside one message's text flow, so both
2293
+ * paths resolve them per message.
2294
+ */
2295
+ function applyAnnotationMarks(root, only) {
2174
2296
  if (jobs.size === 0) return;
2175
2297
  const scope = root ?? (typeof document === "undefined" ? void 0 : document.body);
2176
2298
  if (!scope) return;
2299
+ const lookup = only ? null : flowLookup(scope);
2300
+ const onlyByKey = only ? groupByFlowKey(only) : null;
2301
+ const onlyItems = only ? [...only].filter((item) => flowKeyOf(item) !== "") : [];
2177
2302
  for (const job of [...jobs.values()]) {
2178
- const items = flowItems(job.messageId, scope);
2303
+ const items = only ? job.messageId === "" ? onlyItems : onlyByKey?.get(job.messageId) ?? [] : job.messageId === "" ? lookup?.all ?? [] : [lookup?.byKey.get(job.messageId)].filter((item) => item !== void 0);
2179
2304
  let decorated = false;
2180
2305
  for (const item of items) {
2181
2306
  if (refreshMarkIndex(item, job)) {
@@ -2187,7 +2312,11 @@ function applyAnnotationMarks(root) {
2187
2312
  if (decorateJob(item, job)) decorated = true;
2188
2313
  }
2189
2314
  }
2190
- layoutBadges(scope);
2315
+ if (!only) {
2316
+ layoutBadges(scope);
2317
+ return;
2318
+ }
2319
+ for (const item of only) if (item.isConnected) layoutBadges(item);
2191
2320
  }
2192
2321
  /** Insert or refresh jobs; identical jobs are no-ops. */
2193
2322
  function upsertAnnotationJobs(next) {
@@ -2234,26 +2363,46 @@ function syncPendingAnnotationJobs(annotations) {
2234
2363
  endOffset: item.sources[0]?.endOffset
2235
2364
  })));
2236
2365
  }
2237
- /** Observe the transcript and re-apply marks after re-renders. Returns disposer. */
2366
+ /**
2367
+ * Observe the transcript and re-apply marks after re-renders. Mutations are
2368
+ * narrowed to the flow items that actually changed; streaming a reply no
2369
+ * longer walks every annotation in the conversation on each frame. Resizes
2370
+ * still force a full pass because line wrapping moves every badge anchor.
2371
+ */
2238
2372
  function watchAnnotationMarks(root) {
2239
2373
  const el = root ?? (typeof document === "undefined" ? void 0 : document.body);
2240
2374
  if (!el || typeof MutationObserver === "undefined") return () => {};
2241
2375
  applyAnnotationMarks(el);
2242
2376
  let frame = 0;
2243
- const schedule = () => {
2244
- if (jobs.size === 0) return;
2245
- if (frame !== 0) return;
2246
- frame = window.requestAnimationFrame(() => {
2247
- frame = 0;
2248
- applyAnnotationMarks(el);
2249
- });
2377
+ let pending = /* @__PURE__ */ new Set();
2378
+ let needsFull = false;
2379
+ const run = () => {
2380
+ frame = 0;
2381
+ const only = pending;
2382
+ const full = needsFull;
2383
+ pending = /* @__PURE__ */ new Set();
2384
+ needsFull = false;
2385
+ if (full) applyAnnotationMarks(el);
2386
+ else if (only.size > 0) applyAnnotationMarks(el, only);
2250
2387
  };
2251
- const observer = new MutationObserver(schedule);
2388
+ const schedule = (records) => {
2389
+ if (jobs.size === 0) {
2390
+ pending.clear();
2391
+ needsFull = false;
2392
+ return;
2393
+ }
2394
+ if (records) for (const item of affectedFlowItems(records)) pending.add(item);
2395
+ if (frame === 0) frame = window.requestAnimationFrame(run);
2396
+ };
2397
+ const observer = new MutationObserver((records) => schedule(records));
2252
2398
  observer.observe(el, {
2253
2399
  childList: true,
2254
2400
  subtree: true
2255
2401
  });
2256
- const onResize = () => schedule();
2402
+ const onResize = () => {
2403
+ needsFull = true;
2404
+ schedule();
2405
+ };
2257
2406
  window.addEventListener("resize", onResize);
2258
2407
  return () => {
2259
2408
  observer.disconnect();
@@ -2737,11 +2886,25 @@ function clearAnnotations(sessionId) {
2737
2886
  * Called from render-phase bridges, so adoption is deferred to a microtask:
2738
2887
  * the resulting notify() must not land inside another component's render.
2739
2888
  * Re-binding the same session is a no-op.
2889
+ *
2890
+ * A rebind is itself a publication even when adoption changes nothing: the
2891
+ * host mints a brand-new session id on a fork (annotations are not
2892
+ * inherited), and subscribers like the pending-mark job sync must re-read
2893
+ * the new partition so the previous session's marks are dropped instead of
2894
+ * bleeding into the forked transcript while the composer card is already
2895
+ * gone.
2896
+ *
2897
+ * A bind superseded in the same tick adopts nothing: running its adoption
2898
+ * would publish a partition that is no longer active. Its stored data is
2899
+ * untouched and is picked up when that session is bound again.
2740
2900
  */
2741
2901
  function bindAnnotationSession(sessionId) {
2742
2902
  if (!sessionId || sessionId === runtime.sessionId) return;
2743
2903
  setRuntimeSession(sessionId);
2744
- queueMicrotask(() => adoptPersistedAnnotations(sessionId));
2904
+ queueMicrotask(() => {
2905
+ if (runtime.sessionId !== sessionId) return;
2906
+ if (!adoptPersistedAnnotations(sessionId)) notify();
2907
+ });
2745
2908
  }
2746
2909
  /**
2747
2910
  * Adopt persisted annotations for a session.
@@ -2749,9 +2912,13 @@ function bindAnnotationSession(sessionId) {
2749
2912
  * In-memory entries win on conflict — they can only be newer, because anything
2750
2913
  * added after the bind is persisted immediately. Annotations parked in the
2751
2914
  * memory-only bucket (added before the bind) move over as well.
2915
+ *
2916
+ * Returns whether the partition changed (and published): `false` means the
2917
+ * stored state already matched memory, so callers that also represent "the
2918
+ * active partition moved" (see `bindAnnotationSession`) still need to publish.
2752
2919
  */
2753
2920
  function adoptPersistedAnnotations(sessionId) {
2754
- if (!persistence) return;
2921
+ if (!persistence) return false;
2755
2922
  const stored = persistence.load(sessionId);
2756
2923
  const current = bySession.get(sessionId) ?? EMPTY$1;
2757
2924
  const orphaned = bySession.get(MEMORY_KEY) ?? EMPTY$1;
@@ -2759,10 +2926,11 @@ function adoptPersistedAnnotations(sessionId) {
2759
2926
  for (const item of stored) merged.set(item.id, item);
2760
2927
  for (const item of [...current, ...orphaned]) merged.set(item.id, item);
2761
2928
  const next = [...merged.values()];
2762
- if (next.length === current.length && next.every((item, i) => item.id === current[i]?.id) && orphaned.length === 0) return;
2929
+ if (next.length === current.length && next.every((item, i) => item.id === current[i]?.id) && orphaned.length === 0) return false;
2763
2930
  bySession.set(sessionId, next);
2764
2931
  if (orphaned.length > 0) bySession.delete(MEMORY_KEY);
2765
2932
  notify();
2933
+ return true;
2766
2934
  }
2767
2935
  /** Same ids in the same order, with the only mutable field (comment) equal. */
2768
2936
  function sameAnnotations(a, b) {
@@ -5676,6 +5844,15 @@ function bindAnnotationMarkPreview() {
5676
5844
  const onClick = (event) => {
5677
5845
  const mark = markOf(event.target);
5678
5846
  if (!mark) return;
5847
+ const index = Number(mark.getAttribute("data-dsq-ann-index"));
5848
+ if (!Number.isFinite(index) || index <= 0) return;
5849
+ if (event.detail !== 0) {
5850
+ const offset = {
5851
+ dx: Number.parseFloat(mark.style.getPropertyValue("--dsq-ann-badge-x")) || 0,
5852
+ dy: Number.parseFloat(mark.style.getPropertyValue("--dsq-ann-badge-y")) || 0
5853
+ };
5854
+ if (!badgeContainsPoint(badgeHitBox(mark.getBoundingClientRect(), index, offset), event.clientX, event.clientY)) return;
5855
+ }
5679
5856
  const selection = window.getSelection();
5680
5857
  if (selection && !selection.isCollapsed) return;
5681
5858
  showMarkPreview(mark, false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-select-quote",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "DeepSeek Harness web plugin: annotate text selected in the conversation — the selection rides along with your next message, is marked in the source text, and the model points back at it.",
5
5
  "keywords": [
6
6
  "deepseek",