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.
- package/README.md +3 -3
- package/lib/client.js +232 -55
- 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
|
-
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1811
|
-
|
|
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
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
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
|
|
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
|
|
2149
|
-
const
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
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
|
-
/**
|
|
2173
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
frame =
|
|
2247
|
-
|
|
2248
|
-
|
|
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
|
|
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 = () =>
|
|
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(() =>
|
|
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.
|
|
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",
|