dsh-select-quote 0.3.11 → 0.3.12
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 +2 -2
- package/lib/client.js +196 -49
- 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
|
|
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 {
|
|
@@ -1807,13 +1809,41 @@ const styles = {
|
|
|
1807
1809
|
segOption: "dsq_segOption",
|
|
1808
1810
|
srOnly: "dsq_srOnly"
|
|
1809
1811
|
};
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
+
const BADGE_MIN_WIDTH = 15;
|
|
1813
|
+
/** 10px/600 digits plus the 3px side padding. */
|
|
1814
|
+
const BADGE_DIGIT_WIDTH = 6;
|
|
1815
|
+
const BADGE_PADDING = 6;
|
|
1816
|
+
const HIT_SLOP = 3;
|
|
1812
1817
|
const STEP_X = 28;
|
|
1813
1818
|
const STEP_Y = 18;
|
|
1814
1819
|
/** Rest, then two steps right; after that the stack grows upward. */
|
|
1815
1820
|
const MAX_COLUMNS = 3;
|
|
1816
1821
|
const MAX_ROWS = 24;
|
|
1822
|
+
/** Rendered badge width, matching `min-width` + digit advance + padding. */
|
|
1823
|
+
function badgeWidth(index) {
|
|
1824
|
+
return Math.max(BADGE_MIN_WIDTH, String(index).length * BADGE_DIGIT_WIDTH + BADGE_PADDING);
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Hit box of a badge as painted: anchored to the final fragment's bottom-right
|
|
1828
|
+
* corner, then shifted by the collision offset. Used to tell a badge click from
|
|
1829
|
+
* a click on the marked prose.
|
|
1830
|
+
*/
|
|
1831
|
+
function badgeHitBox(anchor, index, offset = {
|
|
1832
|
+
dx: 0,
|
|
1833
|
+
dy: 0
|
|
1834
|
+
}) {
|
|
1835
|
+
const left = anchor.right + 2 + offset.dx;
|
|
1836
|
+
const top = anchor.bottom - 8 - 15 + offset.dy;
|
|
1837
|
+
return {
|
|
1838
|
+
left: left - HIT_SLOP,
|
|
1839
|
+
top: top - HIT_SLOP,
|
|
1840
|
+
right: left + badgeWidth(index) + HIT_SLOP,
|
|
1841
|
+
bottom: top + 15 + HIT_SLOP
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
function badgeContainsPoint(box, x, y) {
|
|
1845
|
+
return x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
|
|
1846
|
+
}
|
|
1817
1847
|
function overlaps(a, b) {
|
|
1818
1848
|
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
1849
|
}
|
|
@@ -1993,20 +2023,83 @@ function cleanTextOf(item) {
|
|
|
1993
2023
|
const MARK_ATTR = "data-dsq-ann-mark";
|
|
1994
2024
|
const INDEX_ATTR = "data-dsq-ann-index";
|
|
1995
2025
|
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
2026
|
const jobs = /* @__PURE__ */ new Map();
|
|
2000
2027
|
let pendingIds = /* @__PURE__ */ new Set();
|
|
2001
2028
|
function attrValue(value) {
|
|
2002
2029
|
return value.replace(/["\\]/g, "\\$&");
|
|
2003
2030
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2031
|
+
const FLOW_SELECTOR = "[data-chat-flow-key]";
|
|
2032
|
+
/**
|
|
2033
|
+
* One pass over the rendered messages instead of one query per annotation.
|
|
2034
|
+
* `querySelectorAll` preserves document order, so the first copy of a key wins,
|
|
2035
|
+
* matching the old per-job `querySelector`.
|
|
2036
|
+
*/
|
|
2037
|
+
function flowLookup(scope) {
|
|
2038
|
+
const all = [...scope.querySelectorAll(FLOW_SELECTOR)];
|
|
2039
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2040
|
+
for (const item of all) {
|
|
2041
|
+
const key = item.getAttribute("data-chat-flow-key");
|
|
2042
|
+
if (key && !byKey.has(key)) byKey.set(key, item);
|
|
2043
|
+
}
|
|
2044
|
+
return {
|
|
2045
|
+
all,
|
|
2046
|
+
byKey
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
function flowKeyOf(item) {
|
|
2050
|
+
return item.getAttribute("data-chat-flow-key") ?? "";
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* An annotation's source may sit in a block nested inside its message (for
|
|
2054
|
+
* example a reasoning panel), so a touched flow item answers to its own key
|
|
2055
|
+
* and every enclosing message key.
|
|
2056
|
+
*/
|
|
2057
|
+
function anchorKeysOf(item) {
|
|
2058
|
+
const keys = [];
|
|
2059
|
+
for (let el = item; el; el = el.parentElement?.closest(FLOW_SELECTOR) ?? null) {
|
|
2060
|
+
const key = el.getAttribute("data-chat-flow-key");
|
|
2061
|
+
if (key) keys.push(key);
|
|
2062
|
+
}
|
|
2063
|
+
return keys;
|
|
2064
|
+
}
|
|
2065
|
+
/** Items in a partial pass, grouped by flow key; other messages are not touched. */
|
|
2066
|
+
function groupByFlowKey(items) {
|
|
2067
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2068
|
+
for (const item of items) for (const key of anchorKeysOf(item)) {
|
|
2069
|
+
const list = grouped.get(key) ?? [];
|
|
2070
|
+
list.push(item);
|
|
2071
|
+
grouped.set(key, list);
|
|
2008
2072
|
}
|
|
2009
|
-
return
|
|
2073
|
+
return grouped;
|
|
2074
|
+
}
|
|
2075
|
+
/** The flow item that owns a mutated node, if any. */
|
|
2076
|
+
function ownerFlowItem(node) {
|
|
2077
|
+
if (!node) return null;
|
|
2078
|
+
if (node instanceof Element) return node.closest(FLOW_SELECTOR);
|
|
2079
|
+
return node.parentElement?.closest(FLOW_SELECTOR) ?? null;
|
|
2080
|
+
}
|
|
2081
|
+
function affectedFlowItems(records) {
|
|
2082
|
+
const items = /* @__PURE__ */ new Set();
|
|
2083
|
+
const addOwner = (node) => {
|
|
2084
|
+
const owner = ownerFlowItem(node);
|
|
2085
|
+
if (owner?.isConnected) items.add(owner);
|
|
2086
|
+
};
|
|
2087
|
+
/** An added wrapper can hold several newly rendered messages. */
|
|
2088
|
+
const addSubtree = (node) => {
|
|
2089
|
+
const owner = ownerFlowItem(node);
|
|
2090
|
+
if (owner) {
|
|
2091
|
+
if (owner.isConnected) items.add(owner);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
if (!(node instanceof Element)) return;
|
|
2095
|
+
for (const nested of node.querySelectorAll(FLOW_SELECTOR)) if (nested.isConnected) items.add(nested);
|
|
2096
|
+
};
|
|
2097
|
+
for (const record of records) {
|
|
2098
|
+
addOwner(record.target);
|
|
2099
|
+
if (record.type !== "childList") continue;
|
|
2100
|
+
for (const node of record.addedNodes) addSubtree(node);
|
|
2101
|
+
}
|
|
2102
|
+
return items;
|
|
2010
2103
|
}
|
|
2011
2104
|
/**
|
|
2012
2105
|
* Whitespace-insensitive comparison: `selection.toString()` inserts newlines
|
|
@@ -2133,7 +2226,8 @@ function refreshAnnotationMarkLabels() {
|
|
|
2133
2226
|
* offset (nested ranges), which puts their pseudo-element badges on the same
|
|
2134
2227
|
* pixel; nearby ends can also touch. Measure every last fragment, then write
|
|
2135
2228
|
* the offset back as variables inherited by `::after`. The badge's transform
|
|
2136
|
-
* never participates in text layout.
|
|
2229
|
+
* never participates in text layout. Collisions are resolved inside each
|
|
2230
|
+
* message block, and a stack may not climb into the message above.
|
|
2137
2231
|
*/
|
|
2138
2232
|
function layoutBadges(scope) {
|
|
2139
2233
|
const marks = Array.from(scope.querySelectorAll(`[${INDEX_ATTR}]`));
|
|
@@ -2145,37 +2239,57 @@ function layoutBadges(scope) {
|
|
|
2145
2239
|
const scroller = marks[0]?.closest("[data-conversation-scroll]");
|
|
2146
2240
|
const scrollerRect = scroller instanceof Element ? scroller.getBoundingClientRect() : null;
|
|
2147
2241
|
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
|
-
|
|
2242
|
+
const scrollerTop = scrollerRect && scrollerRect.height > 0 ? scrollerRect.top : 0;
|
|
2243
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2244
|
+
for (const mark of marks) {
|
|
2245
|
+
const owner = mark.closest(FLOW_SELECTOR);
|
|
2246
|
+
const group = groups.get(owner);
|
|
2247
|
+
if (group) group.push(mark);
|
|
2248
|
+
else groups.set(owner, [mark]);
|
|
2249
|
+
}
|
|
2250
|
+
for (const [owner, group] of groups) {
|
|
2251
|
+
if (group.length < 2) continue;
|
|
2252
|
+
const ownerRect = owner?.getBoundingClientRect();
|
|
2253
|
+
const topEdge = Math.max(scrollerTop, ownerRect && ownerRect.height > 0 ? ownerRect.top : scrollerTop) + 4;
|
|
2254
|
+
const anchors = group.map((mark) => {
|
|
2255
|
+
const rect = mark.getBoundingClientRect();
|
|
2256
|
+
return {
|
|
2257
|
+
mark,
|
|
2258
|
+
index: Number(mark.getAttribute(INDEX_ATTR) ?? 0),
|
|
2259
|
+
x: rect.right + 2,
|
|
2260
|
+
y: rect.bottom - 8 - 15,
|
|
2261
|
+
width: 24,
|
|
2262
|
+
height: 15,
|
|
2263
|
+
visible: rect.width > 0 && rect.height > 0
|
|
2264
|
+
};
|
|
2265
|
+
}).filter((anchor) => anchor.visible).sort((a, b) => a.index - b.index);
|
|
2266
|
+
if (anchors.length < 2) continue;
|
|
2267
|
+
const offsets = placeBadges(anchors, {
|
|
2268
|
+
right: rightEdge - 4,
|
|
2269
|
+
top: topEdge
|
|
2270
|
+
});
|
|
2271
|
+
anchors.forEach((anchor, i) => {
|
|
2272
|
+
const { dx, dy } = offsets[i];
|
|
2273
|
+
if (dx !== 0) anchor.mark.style.setProperty("--dsq-ann-badge-x", `${dx}px`);
|
|
2274
|
+
if (dy !== 0) anchor.mark.style.setProperty("--dsq-ann-badge-y", `${dy}px`);
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2171
2277
|
}
|
|
2172
|
-
/**
|
|
2173
|
-
|
|
2278
|
+
/**
|
|
2279
|
+
* Apply registered jobs. With `only`, touch just those rendered messages — the
|
|
2280
|
+
* mutation-driven path — instead of resolving and scanning every message for
|
|
2281
|
+
* every job. Badge collisions live inside one message's text flow, so both
|
|
2282
|
+
* paths resolve them per message.
|
|
2283
|
+
*/
|
|
2284
|
+
function applyAnnotationMarks(root, only) {
|
|
2174
2285
|
if (jobs.size === 0) return;
|
|
2175
2286
|
const scope = root ?? (typeof document === "undefined" ? void 0 : document.body);
|
|
2176
2287
|
if (!scope) return;
|
|
2288
|
+
const lookup = only ? null : flowLookup(scope);
|
|
2289
|
+
const onlyByKey = only ? groupByFlowKey(only) : null;
|
|
2290
|
+
const onlyItems = only ? [...only].filter((item) => flowKeyOf(item) !== "") : [];
|
|
2177
2291
|
for (const job of [...jobs.values()]) {
|
|
2178
|
-
const items =
|
|
2292
|
+
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
2293
|
let decorated = false;
|
|
2180
2294
|
for (const item of items) {
|
|
2181
2295
|
if (refreshMarkIndex(item, job)) {
|
|
@@ -2187,7 +2301,11 @@ function applyAnnotationMarks(root) {
|
|
|
2187
2301
|
if (decorateJob(item, job)) decorated = true;
|
|
2188
2302
|
}
|
|
2189
2303
|
}
|
|
2190
|
-
|
|
2304
|
+
if (!only) {
|
|
2305
|
+
layoutBadges(scope);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
for (const item of only) if (item.isConnected) layoutBadges(item);
|
|
2191
2309
|
}
|
|
2192
2310
|
/** Insert or refresh jobs; identical jobs are no-ops. */
|
|
2193
2311
|
function upsertAnnotationJobs(next) {
|
|
@@ -2234,26 +2352,46 @@ function syncPendingAnnotationJobs(annotations) {
|
|
|
2234
2352
|
endOffset: item.sources[0]?.endOffset
|
|
2235
2353
|
})));
|
|
2236
2354
|
}
|
|
2237
|
-
/**
|
|
2355
|
+
/**
|
|
2356
|
+
* Observe the transcript and re-apply marks after re-renders. Mutations are
|
|
2357
|
+
* narrowed to the flow items that actually changed; streaming a reply no
|
|
2358
|
+
* longer walks every annotation in the conversation on each frame. Resizes
|
|
2359
|
+
* still force a full pass because line wrapping moves every badge anchor.
|
|
2360
|
+
*/
|
|
2238
2361
|
function watchAnnotationMarks(root) {
|
|
2239
2362
|
const el = root ?? (typeof document === "undefined" ? void 0 : document.body);
|
|
2240
2363
|
if (!el || typeof MutationObserver === "undefined") return () => {};
|
|
2241
2364
|
applyAnnotationMarks(el);
|
|
2242
2365
|
let frame = 0;
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
frame =
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2366
|
+
let pending = /* @__PURE__ */ new Set();
|
|
2367
|
+
let needsFull = false;
|
|
2368
|
+
const run = () => {
|
|
2369
|
+
frame = 0;
|
|
2370
|
+
const only = pending;
|
|
2371
|
+
const full = needsFull;
|
|
2372
|
+
pending = /* @__PURE__ */ new Set();
|
|
2373
|
+
needsFull = false;
|
|
2374
|
+
if (full) applyAnnotationMarks(el);
|
|
2375
|
+
else if (only.size > 0) applyAnnotationMarks(el, only);
|
|
2250
2376
|
};
|
|
2251
|
-
const
|
|
2377
|
+
const schedule = (records) => {
|
|
2378
|
+
if (jobs.size === 0) {
|
|
2379
|
+
pending.clear();
|
|
2380
|
+
needsFull = false;
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
if (records) for (const item of affectedFlowItems(records)) pending.add(item);
|
|
2384
|
+
if (frame === 0) frame = window.requestAnimationFrame(run);
|
|
2385
|
+
};
|
|
2386
|
+
const observer = new MutationObserver((records) => schedule(records));
|
|
2252
2387
|
observer.observe(el, {
|
|
2253
2388
|
childList: true,
|
|
2254
2389
|
subtree: true
|
|
2255
2390
|
});
|
|
2256
|
-
const onResize = () =>
|
|
2391
|
+
const onResize = () => {
|
|
2392
|
+
needsFull = true;
|
|
2393
|
+
schedule();
|
|
2394
|
+
};
|
|
2257
2395
|
window.addEventListener("resize", onResize);
|
|
2258
2396
|
return () => {
|
|
2259
2397
|
observer.disconnect();
|
|
@@ -5676,6 +5814,15 @@ function bindAnnotationMarkPreview() {
|
|
|
5676
5814
|
const onClick = (event) => {
|
|
5677
5815
|
const mark = markOf(event.target);
|
|
5678
5816
|
if (!mark) return;
|
|
5817
|
+
const index = Number(mark.getAttribute("data-dsq-ann-index"));
|
|
5818
|
+
if (!Number.isFinite(index) || index <= 0) return;
|
|
5819
|
+
if (event.detail !== 0) {
|
|
5820
|
+
const offset = {
|
|
5821
|
+
dx: Number.parseFloat(mark.style.getPropertyValue("--dsq-ann-badge-x")) || 0,
|
|
5822
|
+
dy: Number.parseFloat(mark.style.getPropertyValue("--dsq-ann-badge-y")) || 0
|
|
5823
|
+
};
|
|
5824
|
+
if (!badgeContainsPoint(badgeHitBox(mark.getBoundingClientRect(), index, offset), event.clientX, event.clientY)) return;
|
|
5825
|
+
}
|
|
5679
5826
|
const selection = window.getSelection();
|
|
5680
5827
|
if (selection && !selection.isCollapsed) return;
|
|
5681
5828
|
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.12",
|
|
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",
|