@shellraining/dsh-annotation 0.0.0-stage → 0.1.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/lib/client.js ADDED
@@ -0,0 +1,2296 @@
1
+ // Generated by `pnpm run build`; edit src/, not this file.
2
+ window.__ModuleLoader__.load({
3
+ id: "@shellraining/dsh-annotation",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
+ let react_dom_client = require("react-dom/client");
9
+ let react = require("react");
10
+ let react_jsx_runtime = require("react/jsx-runtime");
11
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
12
+ // src/appearance.ts
13
+ var STROKES = [
14
+ {
15
+ id: "marker",
16
+ label: "荧光笔"
17
+ },
18
+ {
19
+ id: "underline",
20
+ label: "实线"
21
+ },
22
+ {
23
+ id: "dashed",
24
+ label: "虚线"
25
+ },
26
+ {
27
+ id: "dotted",
28
+ label: "点线"
29
+ },
30
+ {
31
+ id: "wavy",
32
+ label: "波浪线"
33
+ }
34
+ ];
35
+ var ICONS = [
36
+ {
37
+ id: "message",
38
+ label: "气泡"
39
+ },
40
+ {
41
+ id: "bookmark",
42
+ label: "书签"
43
+ },
44
+ {
45
+ id: "note",
46
+ label: "便签"
47
+ },
48
+ {
49
+ id: "number",
50
+ label: "纯编号"
51
+ }
52
+ ];
53
+ var COLORS = [
54
+ {
55
+ value: "theme",
56
+ label: "主题色"
57
+ },
58
+ {
59
+ value: "#fbf719",
60
+ label: "柠檬黄"
61
+ },
62
+ {
63
+ value: "#77dba9",
64
+ label: "薄荷绿"
65
+ },
66
+ {
67
+ value: "#f5a6c8",
68
+ label: "樱花粉"
69
+ },
70
+ {
71
+ value: "#b9a3f5",
72
+ label: "薰衣紫"
73
+ },
74
+ {
75
+ value: "#80c8f5",
76
+ label: "晴空蓝"
77
+ }
78
+ ];
79
+ var DEFAULT_APPEARANCE = {
80
+ stroke: "marker",
81
+ color: "#fbf719",
82
+ icon: "message"
83
+ };
84
+ var PREFIX = "dsh-local-annotation.appearance.v1.";
85
+ var FIELDS = [
86
+ "stroke",
87
+ "color",
88
+ "icon"
89
+ ];
90
+ function valid(field, value) {
91
+ if (typeof value !== "string") return false;
92
+ if (field === "color") return value === "theme" || /^#[\da-f]{6}$/i.test(value);
93
+ return (field === "stroke" ? STROKES : ICONS).some((option) => option.id === value);
94
+ }
95
+ function ink(color) {
96
+ return color === "theme" ? "var(--dsw-alias-brand-primary)" : /^#[\da-f]{6}$/i.test(color) ? color : DEFAULT_APPEARANCE.color;
97
+ }
98
+ function highlightCSS(stroke, color) {
99
+ const tint = ink(color);
100
+ if (stroke === "marker") return `background-color:color-mix(in srgb, ${tint} 32%, transparent);text-decoration:none;`;
101
+ return `background-color:transparent;text-decoration:underline ${stroke === "underline" ? "solid" : stroke} ${tint};text-decoration-thickness:2px;text-underline-offset:.2em;`;
102
+ }
103
+ var AppearanceStore = class {
104
+ changed;
105
+ state = { ...DEFAULT_APPEARANCE };
106
+ style = document.createElement("style");
107
+ sync = (event) => {
108
+ if (event.key !== null && !FIELDS.some((field) => event.key === PREFIX + field)) return;
109
+ try {
110
+ if (event.storageArea !== window.localStorage) return;
111
+ } catch {
112
+ return;
113
+ }
114
+ this.read();
115
+ this.render();
116
+ this.changed(this.state);
117
+ };
118
+ constructor(changed) {
119
+ this.changed = changed;
120
+ this.read();
121
+ this.style.dataset.localAnnotationAppearance = "";
122
+ this.render();
123
+ document.head.append(this.style);
124
+ window.addEventListener("storage", this.sync);
125
+ }
126
+ getSnapshot() {
127
+ return this.state;
128
+ }
129
+ read() {
130
+ const next = { ...DEFAULT_APPEARANCE };
131
+ try {
132
+ for (const field of FIELDS) {
133
+ const value = window.localStorage.getItem(PREFIX + field);
134
+ if (valid(field, value)) Object.assign(next, { [field]: value });
135
+ }
136
+ } catch {}
137
+ this.state = next;
138
+ }
139
+ set(field, value) {
140
+ if (!valid(field, value)) throw new Error("无效的批注外观设置。");
141
+ try {
142
+ window.localStorage.setItem(PREFIX + field, value);
143
+ } catch {
144
+ throw new Error("无法保存外观设置,请检查浏览器存储权限或可用空间。");
145
+ }
146
+ this.read();
147
+ this.render();
148
+ this.changed(this.state);
149
+ }
150
+ render() {
151
+ const { stroke, color, icon } = this.state;
152
+ this.style.textContent = `
153
+ ::highlight(dsh-local-annotation) { ${highlightCSS(stroke, color)} }
154
+ [data-local-annotation-marker], [data-local-annotation-status] .dla-number {
155
+ --dla-ink:${ink(color)}; --dla-marker-mask:var(--dla-mask-${icon});
156
+ --dla-marker-outline:var(--dla-outline-${icon});
157
+ --dla-marker-radius:${icon === "number" ? "var(--dsw-radius-sm)" : "0"};
158
+ }
159
+ `;
160
+ }
161
+ dispose() {
162
+ window.removeEventListener("storage", this.sync);
163
+ this.style.remove();
164
+ }
165
+ };
166
+ // src/model.ts
167
+ var MAX_TEXT = 16e3;
168
+ var MAX_NOTE = 8e3;
169
+ var MAX_NUMBER = 1e6;
170
+ var MAX_ID = 1024;
171
+ var MAX_SOURCE = 8192;
172
+ function boundedString(value, name, max, nonempty = false) {
173
+ if (typeof value !== "string" || value.length > max || nonempty && !value.trim()) throw new TypeError(`${name} 必须是${nonempty ? "非空" : ""}字符串,且不超过 ${max} 个字符`);
174
+ return value;
175
+ }
176
+ function annotationKey(value, name) {
177
+ return boundedString(value, name, MAX_ID, true);
178
+ }
179
+ function object(value, name) {
180
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${name} 格式无效`);
181
+ return value;
182
+ }
183
+ function annotationSource(value, sessionId) {
184
+ const source = object(value, "批注来源");
185
+ if (source.kind === "message") return {
186
+ kind: "message",
187
+ key: boundedString(source.key, "消息来源", MAX_SOURCE, true)
188
+ };
189
+ if (source.kind !== "file") throw new TypeError("批注必须具有明确的消息或文件来源");
190
+ const url = boundedString(source.url, "文件来源 URL", MAX_SOURCE, true);
191
+ const path = boundedString(source.path, "文件来源路径", MAX_SOURCE, true);
192
+ if (/[?#\u0000-\u001f\u007f]/.test(url) || /[\u0000-\u001f\u007f]/.test(path)) throw new TypeError("文件来源包含无效字符");
193
+ const match = /^dsh-resource:\/\/file\/session\/([^/]+)\/(.+)$/.exec(url);
194
+ let owner;
195
+ let decodedPath;
196
+ try {
197
+ owner = match?.[1] === void 0 ? void 0 : decodeURIComponent(match[1]);
198
+ decodedPath = match?.[2]?.split("/").map(decodeURIComponent).join("/");
199
+ } catch {
200
+ throw new TypeError("文件来源 URL 编码无效");
201
+ }
202
+ if (owner !== sessionId || decodedPath !== path) throw new TypeError("文件来源必须属于当前会话且与路径一致");
203
+ return {
204
+ kind: "file",
205
+ url,
206
+ path
207
+ };
208
+ }
209
+ function timestamp(value, name) {
210
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} 必须是非负安全整数`);
211
+ return value;
212
+ }
213
+ /** Validate and detach caller-owned data before asynchronous storage work. */
214
+ function copyAnnotation(value) {
215
+ const item = object(value, "批注");
216
+ const sessionId = annotationKey(item.sessionId, "会话 ID");
217
+ const text = boundedString(item.text, "批注原文", MAX_TEXT, true);
218
+ const start = item.start;
219
+ const end = item.end;
220
+ if (typeof start !== "number" || typeof end !== "number" || !Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end <= start) throw new TypeError("批注位置必须是有效区间");
221
+ const createdAt = timestamp(item.createdAt, "创建时间");
222
+ const updatedAt = timestamp(item.updatedAt, "更新时间");
223
+ if (updatedAt < createdAt) throw new TypeError("更新时间不能早于创建时间");
224
+ const number = item.number;
225
+ if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0 || number > MAX_NUMBER) throw new TypeError(`批注编号必须是 0 到 ${MAX_NUMBER} 之间的整数`);
226
+ return {
227
+ id: annotationKey(item.id, "批注 ID"),
228
+ sessionId,
229
+ number,
230
+ text,
231
+ note: boundedString(item.note, "批注内容", MAX_NOTE),
232
+ source: annotationSource(item.source, sessionId),
233
+ start,
234
+ end,
235
+ before: boundedString(item.before, "前文", MAX_TEXT),
236
+ after: boundedString(item.after, "后文", MAX_TEXT),
237
+ createdAt,
238
+ updatedAt
239
+ };
240
+ }
241
+ // src/anchors.ts
242
+ var MESSAGE = "[data-chat-flow-kind=\"assistant-step\"][data-chat-anchor-key]";
243
+ var FILE_TEXT = "[data-document-markdown], [data-textpreview-plain], [data-code-preview] [data-code-block-content]";
244
+ var EXCLUDED = "input,textarea,select,[contenteditable=\"true\"],[hidden],[aria-hidden=\"true\"],[data-code-block-banner],[data-turn-process-hidden=\"true\"],[data-local-annotation-panel],[data-local-annotation-card],[data-local-annotation-toolbar],[data-local-annotation-markers],[data-local-annotation-status]";
245
+ var TEXT_BLOCK = "p,li,td,th,h1,h2,h3,h4,h5,h6,blockquote,code";
246
+ var BLOCK = `${TEXT_BLOCK},div,section,article,pre,ul,ol,table,figure`;
247
+ var MAX_SOURCE_TEXT = 2e6;
248
+ var MAX_NODES = 5e4;
249
+ function sourceKey(source) {
250
+ return source.kind === "message" ? `message:${source.key}` : `file:${source.url}`;
251
+ }
252
+ /**
253
+ * UI chrome never enters a quote. A button whose nearest block is running text,
254
+ * such as a Markdown file link or file mention, is content; other buttons are chrome.
255
+ */
256
+ function excluded(element) {
257
+ if (element.closest(EXCLUDED)) return true;
258
+ const button = element.closest("button");
259
+ return button !== null && button.parentElement?.closest(BLOCK)?.matches(TEXT_BLOCK) !== true;
260
+ }
261
+ function sourceOf(node, sessionId) {
262
+ const element = node instanceof Element ? node : node.parentElement;
263
+ if (!element || excluded(element)) return null;
264
+ const file = element.closest(FILE_TEXT);
265
+ if (file) {
266
+ const url = file.closest("[data-textpreview-state=\"text\"][data-textpreview-url]")?.getAttribute("data-textpreview-url") ?? "";
267
+ const match = /^dsh-resource:\/\/file\/session\/([^/]+)\/(.+)$/.exec(url);
268
+ if (!match?.[1] || !match[2] || /[?#\u0000-\u001f]/.test(url)) return null;
269
+ try {
270
+ if (decodeURIComponent(match[1]) !== sessionId) return null;
271
+ const path = match[2].split("/").map(decodeURIComponent).join("/");
272
+ if (/[\u0000-\u001f\u007f]/.test(path)) return null;
273
+ return {
274
+ root: file,
275
+ source: {
276
+ kind: "file",
277
+ url,
278
+ path
279
+ }
280
+ };
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
285
+ const root = element.closest(MESSAGE);
286
+ if (!root || root.dataset.chatGroupPart === "reasoning" || excluded(root)) return null;
287
+ if (root.closest("[data-conversation-session]")?.getAttribute("data-conversation-session") !== sessionId) return null;
288
+ return {
289
+ root,
290
+ source: {
291
+ kind: "message",
292
+ key: root.dataset.chatAnchorKey
293
+ }
294
+ };
295
+ }
296
+ function findRoot(source, sessionId) {
297
+ const selector = source.kind === "message" ? `${MESSAGE}[data-chat-anchor-key="${CSS.escape(source.key)}"]` : `[data-textpreview-url="${CSS.escape(source.url)}"] :is(${FILE_TEXT})`;
298
+ for (const element of document.querySelectorAll(selector)) {
299
+ const found = sourceOf(element, sessionId);
300
+ if (found && sourceKey(found.source) === sourceKey(source)) return found.root;
301
+ }
302
+ return null;
303
+ }
304
+ function indexText(root) {
305
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: (node) => node.parentElement && excluded(node.parentElement) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT });
306
+ const segments = [];
307
+ const parts = [];
308
+ let length = 0;
309
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
310
+ if (!(node instanceof Text)) continue;
311
+ if (segments.length >= MAX_NODES || length + node.length > MAX_SOURCE_TEXT) return null;
312
+ segments.push({
313
+ node,
314
+ start: length,
315
+ end: length + node.length
316
+ });
317
+ parts.push(node.data);
318
+ length += node.length;
319
+ }
320
+ return {
321
+ text: parts.join(""),
322
+ segments
323
+ };
324
+ }
325
+ function offsetAt(index, node, offset) {
326
+ const point = document.createRange();
327
+ point.setStart(node, offset);
328
+ point.collapse(true);
329
+ for (const segment of index.segments) {
330
+ if (segment.node === node) return segment.start + offset;
331
+ if (point.comparePoint(segment.node, 0) >= 0) return segment.start;
332
+ }
333
+ return index.text.length;
334
+ }
335
+ function rangeAt(index, start, end) {
336
+ const first = index.segments.find((s) => s.start <= start && s.end > start);
337
+ const last = index.segments.find((s) => s.start < end && s.end >= end);
338
+ if (!first || !last) return null;
339
+ const range = document.createRange();
340
+ range.setStart(first.node, start - first.start);
341
+ range.setEnd(last.node, end - last.start);
342
+ return range;
343
+ }
344
+ function capture(range, sessionId) {
345
+ const startRoot = sourceOf(range.startContainer, sessionId);
346
+ const endRoot = sourceOf(range.endContainer, sessionId);
347
+ if (!startRoot || !endRoot || startRoot.root !== endRoot.root) return null;
348
+ const index = indexText(startRoot.root);
349
+ if (!index) throw new Error("这段正文过大,请在较短的消息或文件预览中批注。");
350
+ const start = offsetAt(index, range.startContainer, range.startOffset);
351
+ const end = offsetAt(index, range.endContainer, range.endOffset);
352
+ if (end <= start || end - start > 16e3) throw new Error(`每条引文最多 ${MAX_TEXT} 个字符。`);
353
+ const text = index.text.slice(start, end);
354
+ if (!text.trim()) return null;
355
+ const now = Date.now();
356
+ return {
357
+ id: crypto.randomUUID(),
358
+ sessionId,
359
+ number: 0,
360
+ text,
361
+ note: "",
362
+ source: startRoot.source,
363
+ start,
364
+ end,
365
+ before: index.text.slice(Math.max(0, start - 32), start),
366
+ after: index.text.slice(end, end + 32),
367
+ createdAt: now,
368
+ updatedAt: now
369
+ };
370
+ }
371
+ var Anchors = class {
372
+ indices = /* @__PURE__ */ new WeakMap();
373
+ invalidate(root) {
374
+ this.indices.delete(root);
375
+ }
376
+ clear() {
377
+ this.indices = /* @__PURE__ */ new WeakMap();
378
+ }
379
+ resolve(item, root) {
380
+ let index = this.indices.get(root);
381
+ if (index === void 0) {
382
+ index = indexText(root);
383
+ this.indices.set(root, index);
384
+ }
385
+ if (!index) return null;
386
+ let start = item.start;
387
+ if (index.text.slice(start, item.end) !== item.text) {
388
+ let found = -1;
389
+ let from = 0;
390
+ while (from <= index.text.length) {
391
+ const pos = index.text.indexOf(item.text, from);
392
+ if (pos < 0) break;
393
+ const before = index.text.slice(Math.max(0, pos - item.before.length), pos);
394
+ const after = index.text.slice(pos + item.text.length, pos + item.text.length + item.after.length);
395
+ if ((!item.before || before === item.before) && (!item.after || after === item.after)) {
396
+ if (found >= 0) return null;
397
+ found = pos;
398
+ }
399
+ from = pos + 1;
400
+ }
401
+ if (found < 0) return null;
402
+ start = found;
403
+ }
404
+ return rangeAt(index, start, start + item.text.length);
405
+ }
406
+ };
407
+ // src/markers.ts
408
+ var WIDTH = 26;
409
+ var HEIGHT = 24;
410
+ var Markers = class {
411
+ open;
412
+ groups = /* @__PURE__ */ new Map();
413
+ frame = 0;
414
+ disposed = false;
415
+ resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => this.schedule());
416
+ intersection = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver((entries) => {
417
+ for (const entry of entries) {
418
+ const group = this.groups.get(entry.target);
419
+ if (!group) continue;
420
+ group.visible = entry.isIntersecting;
421
+ if (group.visible) this.schedule();
422
+ }
423
+ });
424
+ fonts = document.fonts;
425
+ schedule = () => {
426
+ if (!this.disposed && this.groups.size && !this.frame) this.frame = requestAnimationFrame(() => {
427
+ this.frame = 0;
428
+ this.position();
429
+ });
430
+ };
431
+ constructor(open) {
432
+ this.open = open;
433
+ this.fonts?.addEventListener("loadingdone", this.schedule);
434
+ }
435
+ update(entries) {
436
+ if (this.disposed) return;
437
+ const scrollable = /* @__PURE__ */ new Map();
438
+ const positionRoot = (node, source) => {
439
+ let element = node instanceof HTMLElement ? node : node.parentElement;
440
+ for (; element && element !== source && source.contains(element); element = element.parentElement) {
441
+ let scroll = scrollable.get(element);
442
+ if (scroll === void 0) {
443
+ const style = getComputedStyle(element);
444
+ scroll = /^(auto|scroll)$/.test(style.overflowX) || /^(auto|scroll)$/.test(style.overflowY);
445
+ scrollable.set(element, scroll);
446
+ }
447
+ if (scroll) return element;
448
+ }
449
+ return source;
450
+ };
451
+ const limited = [];
452
+ for (const entry of entries.slice(0, 200)) {
453
+ const root = positionRoot(entry.range.startContainer, entry.root);
454
+ if (root.contains(entry.range.endContainer) && root === positionRoot(entry.range.endContainer, entry.root)) limited.push({
455
+ ...entry,
456
+ root
457
+ });
458
+ }
459
+ const roots = new Set(limited.map((entry) => entry.root));
460
+ this.resize?.disconnect();
461
+ for (const [root, group] of this.groups) if (!roots.has(root)) this.remove(root, group);
462
+ else {
463
+ group.node.replaceChildren();
464
+ group.markers = [];
465
+ }
466
+ const layout = /* @__PURE__ */ new Set([document.documentElement]);
467
+ for (const entry of limited) {
468
+ const { root, item, number } = entry;
469
+ let group = this.groups.get(root);
470
+ if (!group) {
471
+ const position = getComputedStyle(root).position === "static" ? {
472
+ value: root.style.getPropertyValue("position"),
473
+ priority: root.style.getPropertyPriority("position")
474
+ } : null;
475
+ if (position) root.style.setProperty("position", "relative");
476
+ const node = document.createElement("div");
477
+ node.dataset.localAnnotationMarkers = "";
478
+ node.style.cssText = "all:initial;position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;z-index:1;";
479
+ root.append(node);
480
+ group = {
481
+ node,
482
+ position,
483
+ markers: [],
484
+ visible: true
485
+ };
486
+ this.groups.set(root, group);
487
+ this.intersection?.observe(root);
488
+ }
489
+ if (group.node.parentElement !== root) root.append(group.node);
490
+ const button = document.createElement("button");
491
+ button.type = "button";
492
+ button.dataset.localAnnotationMarker = "";
493
+ button.dataset.digits = String(number).length.toString();
494
+ const label = document.createElement("span");
495
+ label.textContent = String(number);
496
+ button.append(label);
497
+ button.setAttribute("aria-label", `编辑批注 ${number}`);
498
+ button.title = (item.note || item.text).slice(0, 240);
499
+ button.style.cssText = `position:absolute;box-sizing:border-box;width:${WIDTH}px;height:${HEIGHT}px;pointer-events:auto;visibility:hidden;user-select:none;text-align:center;`;
500
+ button.onpointerdown = (event) => {
501
+ event.preventDefault();
502
+ event.stopPropagation();
503
+ };
504
+ button.onclick = (event) => {
505
+ event.preventDefault();
506
+ event.stopPropagation();
507
+ this.open(item);
508
+ };
509
+ group.node.append(button);
510
+ group.markers.push({
511
+ entry,
512
+ button
513
+ });
514
+ for (let element = root; element; element = element.parentElement) layout.add(element);
515
+ for (let element = entry.range.startContainer.parentElement; element && root.contains(element); element = element.parentElement) {
516
+ layout.add(element);
517
+ if (element === root) break;
518
+ }
519
+ }
520
+ if (this.groups.size) for (const element of layout) this.resize?.observe(element);
521
+ if (!this.groups.size && this.frame) {
522
+ cancelAnimationFrame(this.frame);
523
+ this.frame = 0;
524
+ }
525
+ this.schedule();
526
+ }
527
+ position() {
528
+ const writes = [];
529
+ for (const [root, group] of this.groups) {
530
+ if (!group.visible || !root.isConnected) continue;
531
+ const box = root.getBoundingClientRect();
532
+ const sx = root.offsetWidth ? box.width / root.offsetWidth : 0;
533
+ const sy = root.offsetHeight ? box.height / root.offsetHeight : 0;
534
+ const left = root.scrollLeft - root.clientLeft;
535
+ const top = root.scrollTop - root.clientTop;
536
+ const maxX = Math.max(0, root.scrollWidth - WIDTH);
537
+ const maxY = Math.max(0, root.scrollHeight - HEIGHT);
538
+ const overlaps = /* @__PURE__ */ new Map();
539
+ for (const { entry, button } of group.markers) {
540
+ const write = { button };
541
+ writes.push(write);
542
+ if (!sx || !sy || !root.contains(entry.range.startContainer) || !root.contains(entry.range.endContainer)) continue;
543
+ const caret = entry.range.cloneRange();
544
+ caret.collapse(true);
545
+ const rect = caret.getClientRects()[0] ?? caret.getBoundingClientRect();
546
+ if (!rect.height) continue;
547
+ const x = (rect.left - box.left) / sx + left;
548
+ const y = Math.max(0, Math.min(maxY, (rect.top - box.top) / sy + top - HEIGHT + 2));
549
+ const key = `${Math.round(x / WIDTH)}:${Math.round(y / HEIGHT)}`;
550
+ const count = overlaps.get(key) ?? 0;
551
+ overlaps.set(key, count + 1);
552
+ write.x = Math.max(0, Math.min(maxX, x + count * WIDTH));
553
+ write.y = y;
554
+ }
555
+ }
556
+ for (const { button, x, y } of writes) {
557
+ button.style.visibility = x === void 0 ? "hidden" : "visible";
558
+ if (x !== void 0 && y !== void 0) {
559
+ button.style.left = `${x}px`;
560
+ button.style.top = `${y}px`;
561
+ }
562
+ }
563
+ }
564
+ remove(root, group) {
565
+ this.intersection?.unobserve(root);
566
+ group.node.remove();
567
+ if (group.position && root.style.getPropertyValue("position") === "relative" && root.style.getPropertyPriority("position") === "") {
568
+ if (group.position.value) root.style.setProperty("position", group.position.value, group.position.priority);
569
+ else root.style.removeProperty("position");
570
+ }
571
+ this.groups.delete(root);
572
+ }
573
+ dispose() {
574
+ this.disposed = true;
575
+ this.resize?.disconnect();
576
+ this.intersection?.disconnect();
577
+ this.fonts?.removeEventListener("loadingdone", this.schedule);
578
+ if (this.frame) cancelAnimationFrame(this.frame);
579
+ this.frame = 0;
580
+ for (const [root, group] of this.groups) this.remove(root, group);
581
+ }
582
+ };
583
+ // src/highlights.ts
584
+ var NAME = "dsh-local-annotation";
585
+ var api = CSS;
586
+ var HighlightClass = globalThis.Highlight;
587
+ var Highlights = class {
588
+ supported = !!api.highlights && !!HighlightClass;
589
+ highlight = HighlightClass ? new HighlightClass() : null;
590
+ groups = /* @__PURE__ */ new Map();
591
+ anchors = new Anchors();
592
+ dirty = /* @__PURE__ */ new Set();
593
+ timer;
594
+ sessionId = "";
595
+ markers;
596
+ numbers = /* @__PURE__ */ new Map();
597
+ observer = new MutationObserver((records) => {
598
+ let addedSource = false;
599
+ for (const record of records) {
600
+ if ((record.target instanceof Element ? record.target : record.target.parentElement)?.closest("[data-local-annotation-markers],[data-local-annotation-status]")) continue;
601
+ const changed = [...record.addedNodes, ...record.removedNodes];
602
+ if (record.type === "childList" && changed.length && changed.every((node) => node instanceof Element && node.matches("[data-local-annotation-markers]"))) continue;
603
+ for (const group of this.groups.values()) if (group.root && (!group.root.isConnected || group.root.contains(record.target) || record.type === "attributes" && record.target instanceof Element && record.target.contains(group.root))) this.dirty.add(group);
604
+ if (record.type === "attributes") addedSource = true;
605
+ if (!addedSource) {
606
+ for (const node of record.addedNodes) if (node instanceof Element && (node.matches(`[data-chat-flow-kind="assistant-step"][data-chat-anchor-key],[data-textpreview-url],[data-document-markdown], [data-textpreview-plain], [data-code-preview] [data-code-block-content]`) || node.querySelector(`[data-chat-flow-kind="assistant-step"][data-chat-anchor-key],[data-textpreview-url],[data-document-markdown], [data-textpreview-plain], [data-code-preview] [data-code-block-content]`))) {
607
+ addedSource = true;
608
+ break;
609
+ }
610
+ }
611
+ }
612
+ if (addedSource) {
613
+ for (const group of this.groups.values()) if (!group.root?.isConnected) this.dirty.add(group);
614
+ }
615
+ if (this.dirty.size && this.timer === void 0) this.timer = setTimeout(() => this.flush(), 100);
616
+ });
617
+ constructor(open) {
618
+ this.markers = new Markers(open);
619
+ if (this.highlight) api.highlights?.set(NAME, this.highlight);
620
+ }
621
+ update(sessionId, items) {
622
+ this.reset();
623
+ this.sessionId = sessionId;
624
+ const next = items.reduce((max, item) => Math.max(max, item.number), 0) + 1;
625
+ for (const item of items) this.numbers.set(item.id, item.number || next);
626
+ for (const item of items) {
627
+ const key = sourceKey(item.source);
628
+ let group = this.groups.get(key);
629
+ if (!group) {
630
+ group = {
631
+ source: item.source,
632
+ items: [],
633
+ root: null,
634
+ ranges: /* @__PURE__ */ new Map()
635
+ };
636
+ this.groups.set(key, group);
637
+ }
638
+ group.items.push(item);
639
+ this.dirty.add(group);
640
+ }
641
+ this.flush();
642
+ if (this.groups.size) this.observer.observe(document.getElementById("root") ?? document.body, {
643
+ subtree: true,
644
+ childList: true,
645
+ characterData: true,
646
+ attributes: true,
647
+ attributeFilter: [
648
+ "hidden",
649
+ "aria-hidden",
650
+ "data-turn-process-hidden",
651
+ "data-textpreview-state",
652
+ "data-textpreview-url"
653
+ ]
654
+ });
655
+ }
656
+ flush() {
657
+ clearTimeout(this.timer);
658
+ this.timer = void 0;
659
+ for (const group of this.dirty) {
660
+ for (const range of group.ranges.values()) this.highlight?.delete(range);
661
+ group.ranges.clear();
662
+ if (group.root) this.anchors.invalidate(group.root);
663
+ group.root = findRoot(group.source, this.sessionId);
664
+ if (!group.root) continue;
665
+ for (const item of group.items) {
666
+ const range = this.anchors.resolve(item, group.root);
667
+ if (range) {
668
+ group.ranges.set(item.id, range);
669
+ this.highlight?.add(range);
670
+ }
671
+ }
672
+ }
673
+ this.dirty.clear();
674
+ const entries = [];
675
+ for (const group of this.groups.values()) {
676
+ if (!group.root) continue;
677
+ for (const item of group.items) {
678
+ const range = group.ranges.get(item.id);
679
+ if (range) entries.push({
680
+ item,
681
+ range,
682
+ root: group.root,
683
+ number: this.numbers.get(item.id) ?? 1
684
+ });
685
+ }
686
+ }
687
+ this.markers.update(entries);
688
+ }
689
+ range(item) {
690
+ const root = findRoot(item.source, item.sessionId);
691
+ if (!root) return null;
692
+ this.anchors.invalidate(root);
693
+ return this.anchors.resolve(item, root);
694
+ }
695
+ hit(target, x, y) {
696
+ const source = sourceOf(target, this.sessionId);
697
+ if (!source) return null;
698
+ const group = this.groups.get(sourceKey(source.source));
699
+ if (!group) return null;
700
+ if (this.dirty.has(group)) this.flush();
701
+ for (const item of group.items) {
702
+ const range = group.ranges.get(item.id);
703
+ if (!range?.startContainer.isConnected) continue;
704
+ for (const rect of range.getClientRects()) if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) return item;
705
+ }
706
+ return null;
707
+ }
708
+ locate(item) {
709
+ const root = findRoot(item.source, item.sessionId);
710
+ if (!root) return false;
711
+ this.anchors.invalidate(root);
712
+ const range = this.anchors.resolve(item, root);
713
+ if (!range) return false;
714
+ range.startContainer.parentElement?.scrollIntoView({
715
+ block: "center",
716
+ behavior: "instant"
717
+ });
718
+ if (!this.supported) {
719
+ const selection = window.getSelection();
720
+ selection?.removeAllRanges();
721
+ selection?.addRange(range);
722
+ }
723
+ return true;
724
+ }
725
+ reset() {
726
+ this.observer.disconnect();
727
+ clearTimeout(this.timer);
728
+ this.timer = void 0;
729
+ this.markers.update([]);
730
+ this.numbers.clear();
731
+ this.groups.clear();
732
+ this.dirty.clear();
733
+ this.anchors.clear();
734
+ this.highlight?.clear();
735
+ }
736
+ dispose() {
737
+ this.reset();
738
+ this.markers.dispose();
739
+ if (api.highlights?.get(NAME) === this.highlight) api.highlights.delete(NAME);
740
+ }
741
+ };
742
+ // src/selection.ts
743
+ function installSelection(current, onSelect, onError) {
744
+ const events = new AbortController();
745
+ const button = document.createElement("button");
746
+ button.type = "button";
747
+ button.textContent = "批注";
748
+ button.title = "批注所选文字";
749
+ const toolbar = document.createElement("div");
750
+ toolbar.dataset.localAnnotationToolbar = "";
751
+ toolbar.hidden = true;
752
+ toolbar.append(button);
753
+ document.body.append(toolbar);
754
+ let pointerDown = false;
755
+ let composing = false;
756
+ let frame = 0;
757
+ let timer;
758
+ let range = null;
759
+ let sessionId;
760
+ let scrollEvents = null;
761
+ const hide = () => {
762
+ toolbar.hidden = true;
763
+ range = null;
764
+ sessionId = void 0;
765
+ scrollEvents?.abort();
766
+ scrollEvents = null;
767
+ clearTimeout(timer);
768
+ cancelAnimationFrame(frame);
769
+ frame = 0;
770
+ };
771
+ const show = () => {
772
+ frame = 0;
773
+ if (pointerDown || composing) return;
774
+ const selection = window.getSelection();
775
+ const id = current();
776
+ if (!id || !selection?.rangeCount || selection.isCollapsed) {
777
+ hide();
778
+ return;
779
+ }
780
+ const selected = selection.getRangeAt(0);
781
+ const start = sourceOf(selected.startContainer, id);
782
+ const end = sourceOf(selected.endContainer, id);
783
+ if (!start || !end || start.root !== end.root) {
784
+ hide();
785
+ return;
786
+ }
787
+ hide();
788
+ const caret = document.createRange();
789
+ if (!selection.focusNode) return;
790
+ caret.setStart(selection.focusNode, selection.focusOffset);
791
+ caret.collapse(true);
792
+ const rect = caret.getBoundingClientRect();
793
+ if (!rect.height || rect.bottom < 40 || rect.top > window.innerHeight) return;
794
+ range = selected.cloneRange();
795
+ sessionId = id;
796
+ toolbar.style.left = `${Math.max(8, Math.min(window.innerWidth - 80, rect.right + 8))}px`;
797
+ toolbar.style.top = `${Math.max(48, Math.min(window.innerHeight - 44, rect.bottom + 6))}px`;
798
+ toolbar.hidden = false;
799
+ scrollEvents = new AbortController();
800
+ document.addEventListener("scroll", hide, {
801
+ capture: true,
802
+ passive: true,
803
+ signal: scrollEvents.signal
804
+ });
805
+ window.addEventListener("resize", hide, {
806
+ passive: true,
807
+ signal: scrollEvents.signal
808
+ });
809
+ };
810
+ const schedule = () => {
811
+ cancelAnimationFrame(frame);
812
+ frame = requestAnimationFrame(show);
813
+ };
814
+ const signal = events.signal;
815
+ document.addEventListener("pointerdown", (event) => {
816
+ if (toolbar.contains(event.target)) return;
817
+ pointerDown = true;
818
+ hide();
819
+ }, {
820
+ capture: true,
821
+ signal
822
+ });
823
+ document.addEventListener("pointerup", () => {
824
+ pointerDown = false;
825
+ schedule();
826
+ }, { signal });
827
+ document.addEventListener("pointercancel", () => {
828
+ pointerDown = false;
829
+ hide();
830
+ }, { signal });
831
+ document.addEventListener("selectionchange", () => {
832
+ if (pointerDown || composing || toolbar.contains(document.activeElement)) return;
833
+ hide();
834
+ timer = setTimeout(schedule, 120);
835
+ }, { signal });
836
+ document.addEventListener("compositionstart", () => {
837
+ composing = true;
838
+ hide();
839
+ }, { signal });
840
+ document.addEventListener("compositionend", () => {
841
+ composing = false;
842
+ }, { signal });
843
+ document.addEventListener("keydown", (event) => {
844
+ if (event.key === "Escape") hide();
845
+ }, { signal });
846
+ window.addEventListener("blur", () => {
847
+ pointerDown = false;
848
+ hide();
849
+ }, { signal });
850
+ toolbar.addEventListener("pointerdown", (event) => event.preventDefault(), { signal });
851
+ button.addEventListener("click", () => {
852
+ if (!range || !sessionId || current() !== sessionId || !range.startContainer.isConnected) {
853
+ hide();
854
+ return;
855
+ }
856
+ try {
857
+ const selected = range.cloneRange();
858
+ const item = capture(selected, sessionId);
859
+ hide();
860
+ if (!item) throw new Error("这段选区无法批注,请只选择同一条回复或同一个文件里的正文。");
861
+ window.getSelection()?.removeAllRanges();
862
+ onSelect(item, selected);
863
+ } catch (error) {
864
+ hide();
865
+ onError(error);
866
+ }
867
+ }, { signal });
868
+ return {
869
+ hide,
870
+ dispose() {
871
+ hide();
872
+ events.abort();
873
+ toolbar.remove();
874
+ }
875
+ };
876
+ }
877
+ // src/storage.ts
878
+ var DATABASE = "dsh-local-annotation";
879
+ var ITEMS = "annotations";
880
+ var OPEN_TIMEOUT = 1e4;
881
+ function failure(error, fallback) {
882
+ if (error instanceof Error) return error;
883
+ return new Error(fallback, { cause: error });
884
+ }
885
+ function expectedVersion(value) {
886
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) throw new TypeError("预期版本必须是非负安全整数");
887
+ }
888
+ function conflict() {
889
+ return /* @__PURE__ */ new Error("批注已在其他窗口修改或删除,请刷新后重试");
890
+ }
891
+ function nextVersion(previous) {
892
+ const next = Math.max(Date.now(), previous === void 0 ? 0 : previous + 1);
893
+ if (!Number.isSafeInteger(next)) throw new Error("批注版本已超出安全范围");
894
+ return next;
895
+ }
896
+ function sortRows(rows) {
897
+ return rows.sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id));
898
+ }
899
+ function maxNumber(rows) {
900
+ return rows.reduce((max, row) => Math.max(max, row.number), 0);
901
+ }
902
+ var AnnotationStore = class {
903
+ listeners = /* @__PURE__ */ new Set();
904
+ connections = /* @__PURE__ */ new Set();
905
+ pending = /* @__PURE__ */ new Set();
906
+ environment;
907
+ channel;
908
+ disposed = false;
909
+ constructor(environment = globalThis) {
910
+ this.environment = environment;
911
+ try {
912
+ if (environment.BroadcastChannel) {
913
+ this.channel = new environment.BroadcastChannel(DATABASE);
914
+ this.channel.onmessage = (event) => {
915
+ const data = event.data;
916
+ if (!data || typeof data !== "object" || !("sessionId" in data)) return;
917
+ try {
918
+ this.notify(annotationKey(data.sessionId, "会话 ID"));
919
+ } catch {}
920
+ };
921
+ }
922
+ } catch {}
923
+ }
924
+ ensureActive() {
925
+ if (this.disposed) throw new Error("批注存储已关闭");
926
+ }
927
+ open() {
928
+ this.ensureActive();
929
+ return new Promise((resolve, reject) => {
930
+ let factory;
931
+ try {
932
+ factory = this.environment.indexedDB;
933
+ } catch (error) {
934
+ reject(failure(error, "无法访问 IndexedDB"));
935
+ return;
936
+ }
937
+ if (!factory) {
938
+ reject(/* @__PURE__ */ new Error("当前环境不支持 IndexedDB,批注未保存"));
939
+ return;
940
+ }
941
+ let request;
942
+ try {
943
+ request = factory.open(DATABASE, 1);
944
+ } catch (error) {
945
+ reject(failure(error, "无法打开批注存储"));
946
+ return;
947
+ }
948
+ let settled = false;
949
+ const stop = (error) => {
950
+ if (settled) return;
951
+ settled = true;
952
+ clearTimeout(timer);
953
+ this.pending.delete(cancel);
954
+ reject(error);
955
+ };
956
+ const cancel = () => stop(/* @__PURE__ */ new Error("批注存储已关闭"));
957
+ const timer = setTimeout(() => stop(/* @__PURE__ */ new Error("打开批注存储超时,请关闭占用数据库的旧窗口后重试")), OPEN_TIMEOUT);
958
+ this.pending.add(cancel);
959
+ request.onblocked = () => stop(/* @__PURE__ */ new Error("批注数据库升级被其他窗口阻塞,请关闭旧窗口后重试"));
960
+ request.onerror = () => stop(failure(request.error, "打开批注数据库失败"));
961
+ request.onupgradeneeded = () => {
962
+ if (settled || this.disposed) {
963
+ request.transaction?.abort();
964
+ return;
965
+ }
966
+ try {
967
+ request.result.createObjectStore(ITEMS, { keyPath: ["sessionId", "id"] }).createIndex("sessionId", "sessionId");
968
+ } catch (error) {
969
+ request.transaction?.abort();
970
+ stop(failure(error, "初始化批注数据库失败"));
971
+ }
972
+ };
973
+ request.onsuccess = () => {
974
+ const db = request.result;
975
+ if (settled || this.disposed) {
976
+ db.close();
977
+ return;
978
+ }
979
+ settled = true;
980
+ clearTimeout(timer);
981
+ this.pending.delete(cancel);
982
+ this.connections.add(db);
983
+ db.onversionchange = () => {
984
+ db.close();
985
+ this.connections.delete(db);
986
+ };
987
+ resolve(db);
988
+ };
989
+ });
990
+ }
991
+ async transaction(mode, action) {
992
+ const db = await this.open();
993
+ try {
994
+ this.ensureActive();
995
+ return await new Promise((resolve, reject) => {
996
+ const tx = db.transaction(ITEMS, mode);
997
+ let result;
998
+ let ready = false;
999
+ let error;
1000
+ const abort = (cause) => {
1001
+ error ??= failure(cause, "批注事务失败");
1002
+ try {
1003
+ tx.abort();
1004
+ } catch {
1005
+ reject(error);
1006
+ }
1007
+ };
1008
+ tx.oncomplete = () => {
1009
+ if (ready) resolve(result);
1010
+ else reject(/* @__PURE__ */ new Error("批注事务未产生结果"));
1011
+ };
1012
+ tx.onabort = () => reject(error ?? failure(tx.error, "批注事务已中止,数据未保存"));
1013
+ tx.onerror = () => {
1014
+ error ??= failure(tx.error, "批注读写失败,可能是存储空间不足");
1015
+ };
1016
+ try {
1017
+ action(tx.objectStore(ITEMS), (value) => {
1018
+ result = value;
1019
+ ready = true;
1020
+ }, abort);
1021
+ } catch (cause) {
1022
+ abort(cause);
1023
+ }
1024
+ });
1025
+ } finally {
1026
+ db.close();
1027
+ this.connections.delete(db);
1028
+ }
1029
+ }
1030
+ async list(sessionId) {
1031
+ annotationKey(sessionId, "会话 ID");
1032
+ return this.transaction("readonly", (items, finish, abort) => {
1033
+ const request = items.index("sessionId").getAll(sessionId, 201);
1034
+ request.onsuccess = () => {
1035
+ try {
1036
+ if (request.result.length > 200) throw new Error("批注数量超限,未截断数据");
1037
+ finish(sortRows(request.result.map(copyAnnotation)));
1038
+ } catch (error) {
1039
+ abort(error);
1040
+ }
1041
+ };
1042
+ });
1043
+ }
1044
+ async put(item, expectedUpdatedAt) {
1045
+ const value = copyAnnotation(item);
1046
+ expectedVersion(expectedUpdatedAt);
1047
+ await this.transaction("readwrite", (items, finish, abort) => {
1048
+ const request = items.get([value.sessionId, value.id]);
1049
+ request.onsuccess = () => {
1050
+ try {
1051
+ const old = request.result === void 0 ? void 0 : copyAnnotation(request.result);
1052
+ if (expectedUpdatedAt === void 0 ? old !== void 0 : old === void 0 || old.updatedAt !== expectedUpdatedAt) throw conflict();
1053
+ const save = () => {
1054
+ value.updatedAt = nextVersion(old?.updatedAt);
1055
+ value.createdAt = old?.createdAt ?? value.updatedAt;
1056
+ items.put(value);
1057
+ finish(void 0);
1058
+ };
1059
+ if (old) {
1060
+ value.number = old.number;
1061
+ save();
1062
+ return;
1063
+ }
1064
+ const rows = items.index("sessionId").getAll(value.sessionId, 201);
1065
+ rows.onsuccess = () => {
1066
+ try {
1067
+ if (rows.result.length >= 200) throw new Error(`每个会话最多保存 200 条批注`);
1068
+ value.number = maxNumber(rows.result.map(copyAnnotation)) + 1;
1069
+ save();
1070
+ } catch (error) {
1071
+ abort(error);
1072
+ }
1073
+ };
1074
+ } catch (error) {
1075
+ abort(error);
1076
+ }
1077
+ };
1078
+ });
1079
+ this.changed(value.sessionId);
1080
+ return value;
1081
+ }
1082
+ async remove(sessionId, id, expectedUpdatedAt) {
1083
+ annotationKey(sessionId, "会话 ID");
1084
+ annotationKey(id, "批注 ID");
1085
+ expectedVersion(expectedUpdatedAt);
1086
+ if (await this.transaction("readwrite", (items, finish, abort) => {
1087
+ const request = items.get([sessionId, id]);
1088
+ request.onsuccess = () => {
1089
+ try {
1090
+ const old = request.result === void 0 ? void 0 : copyAnnotation(request.result);
1091
+ if (expectedUpdatedAt !== void 0 && (!old || old.updatedAt !== expectedUpdatedAt)) throw conflict();
1092
+ if (old) items.delete([sessionId, id]);
1093
+ finish(old !== void 0);
1094
+ } catch (error) {
1095
+ abort(error);
1096
+ }
1097
+ };
1098
+ })) this.changed(sessionId);
1099
+ }
1100
+ subscribe(listener) {
1101
+ this.ensureActive();
1102
+ this.listeners.add(listener);
1103
+ return () => {
1104
+ this.listeners.delete(listener);
1105
+ };
1106
+ }
1107
+ notify(sessionId) {
1108
+ if (this.disposed) return;
1109
+ for (const listener of [...this.listeners]) try {
1110
+ listener(sessionId);
1111
+ } catch (error) {
1112
+ console.error("批注存储订阅回调失败", error);
1113
+ }
1114
+ }
1115
+ changed(sessionId) {
1116
+ if (this.disposed) return;
1117
+ this.notify(sessionId);
1118
+ try {
1119
+ this.channel?.postMessage({ sessionId });
1120
+ } catch {}
1121
+ }
1122
+ dispose() {
1123
+ if (this.disposed) return;
1124
+ this.disposed = true;
1125
+ for (const cancel of [...this.pending]) cancel();
1126
+ for (const db of this.connections) db.close();
1127
+ this.connections.clear();
1128
+ this.listeners.clear();
1129
+ this.channel?.close();
1130
+ this.channel = void 0;
1131
+ }
1132
+ };
1133
+ // src/composer.ts
1134
+ /** Input-trigger source name, chip serializer routing key, and user-text rule ID. */
1135
+ var CHIP_SOURCE = "local-annotation";
1136
+ var CLOSE = "\n</annotation>";
1137
+ var INDENT = " ";
1138
+ function chipRef(item) {
1139
+ return JSON.stringify([
1140
+ item.sessionId,
1141
+ item.id,
1142
+ item.number
1143
+ ]);
1144
+ }
1145
+ function parseChipRef(ref) {
1146
+ try {
1147
+ const value = JSON.parse(ref);
1148
+ if (!Array.isArray(value) || value.length !== 3) return void 0;
1149
+ const [sessionId, id, number] = value;
1150
+ if (typeof sessionId !== "string" || !sessionId || typeof id !== "string" || !id) return void 0;
1151
+ return typeof number === "number" && Number.isSafeInteger(number) && number > 0 ? {
1152
+ sessionId,
1153
+ id,
1154
+ number
1155
+ } : void 0;
1156
+ } catch {
1157
+ return;
1158
+ }
1159
+ }
1160
+ function chipText(number) {
1161
+ return `[批注 ${number}]`;
1162
+ }
1163
+ /** One composer chip per annotation; its label and clipboard text are fixed at insertion. */
1164
+ function annotationChip(item) {
1165
+ return {
1166
+ source: CHIP_SOURCE,
1167
+ ref: chipRef(item),
1168
+ label: `批注 ${item.number}`,
1169
+ clipboardText: chipText(item.number)
1170
+ };
1171
+ }
1172
+ /** Annotation ids the composer currently carries, in draft order. */
1173
+ function attachedIds(input, sessionId) {
1174
+ const ids = [];
1175
+ for (const occurrence of input?.occurrences ?? []) {
1176
+ if (occurrence.source !== "local-annotation") continue;
1177
+ const key = parseChipRef(occurrence.ref);
1178
+ if (key && key.sessionId === sessionId && !ids.includes(key.id)) ids.push(key.id);
1179
+ }
1180
+ return ids;
1181
+ }
1182
+ /**
1183
+ * Detect-coordinate span of one chip plus the separator inserted after it.
1184
+ * Every chip is one detect character; plain text maps 1:1. Valid only in the
1185
+ * plain phase, where no command chip leads the draft.
1186
+ */
1187
+ function chipSpan(input, target) {
1188
+ let shift = 0;
1189
+ for (const occurrence of input.occurrences) {
1190
+ if (occurrence === target) break;
1191
+ shift += occurrence.length - 1;
1192
+ }
1193
+ const start = target.offset - shift;
1194
+ return {
1195
+ start,
1196
+ end: start + (input.draft.charAt(target.offset + target.length) === " " ? 2 : 1)
1197
+ };
1198
+ }
1199
+ function instruction(number) {
1200
+ return `请在回复中以「Annotation ${number}:」开头回应这条批注。`;
1201
+ }
1202
+ /** Model form of one chip; the user-text rule below parses exactly this layout. */
1203
+ function annotationBlock(item) {
1204
+ const source = item.source.kind === "file" ? `原文(文件 ${item.source.path}):` : "原文:";
1205
+ const quote = item.text.split("\n").map((line) => `> ${line}`).join("\n");
1206
+ const note = item.note.trim() ? `\n批注:${item.note.trim().split("\n").join(`\n${INDENT}`)}` : "";
1207
+ return `\n<annotation n="${item.number}">\n${instruction(item.number)}\n${source}\n${quote}${note}${CLOSE}\n`;
1208
+ }
1209
+ function parseBlock(body, number) {
1210
+ const lines = body.split("\n");
1211
+ if (lines[0] !== instruction(number)) return void 0;
1212
+ const head = /^原文(?:(文件 (.+)))?:$/.exec(lines[1] ?? "");
1213
+ if (!head) return void 0;
1214
+ let index = 2;
1215
+ const quote = [];
1216
+ for (; index < lines.length && lines[index].startsWith(">"); index++) quote.push(lines[index].replace(/^> ?/, ""));
1217
+ if (!quote.length) return void 0;
1218
+ const notes = [];
1219
+ if (index < lines.length) {
1220
+ if (!lines[index].startsWith("批注:")) return void 0;
1221
+ notes.push(lines[index].slice(3));
1222
+ for (index++; index < lines.length; index++) {
1223
+ if (!lines[index].startsWith(INDENT)) return void 0;
1224
+ notes.push(lines[index].slice(2));
1225
+ }
1226
+ }
1227
+ const data = {
1228
+ number,
1229
+ quote: quote.join("\n"),
1230
+ note: notes.join("\n")
1231
+ };
1232
+ if (head[1] !== void 0) data.path = head[1];
1233
+ return data;
1234
+ }
1235
+ /** Recognizes sent annotation blocks in user messages so the bubble renders a chip. */
1236
+ var annotationRule = {
1237
+ id: CHIP_SOURCE,
1238
+ order: 20,
1239
+ match(text, from) {
1240
+ const heads = /<annotation n="([1-9]\d{0,6})">\n/g;
1241
+ heads.lastIndex = from;
1242
+ for (let head = heads.exec(text); head; head = heads.exec(text)) {
1243
+ const body = head.index + head[0].length;
1244
+ const close = text.indexOf(CLOSE, body);
1245
+ if (close < 0) return void 0;
1246
+ const data = parseBlock(text.slice(body, close), Number(head[1]));
1247
+ if (!data) continue;
1248
+ return {
1249
+ id: CHIP_SOURCE,
1250
+ start: head.index > from && text.charAt(head.index - 1) === "\n" ? head.index - 1 : head.index,
1251
+ end: close + 14 + (text.charAt(close + 14) === "\n" ? 1 : 0),
1252
+ data
1253
+ };
1254
+ }
1255
+ }
1256
+ };
1257
+ // src/floating.ts
1258
+ function floatingCard(host, close) {
1259
+ let anchor = null;
1260
+ let frame = 0;
1261
+ let events = null;
1262
+ const position = () => {
1263
+ frame = 0;
1264
+ if (!anchor || host.hidden) return;
1265
+ let { x, y } = anchor;
1266
+ if (anchor.range) {
1267
+ if (!anchor.range.endContainer.isConnected) {
1268
+ close();
1269
+ return;
1270
+ }
1271
+ const caret = anchor.range.cloneRange();
1272
+ caret.collapse(false);
1273
+ let rect = caret.getBoundingClientRect();
1274
+ if (!rect.height || rect.bottom < 48 || rect.top > window.innerHeight) {
1275
+ caret.setStart(anchor.range.startContainer, anchor.range.startOffset);
1276
+ caret.collapse(true);
1277
+ rect = caret.getBoundingClientRect();
1278
+ }
1279
+ if (!rect.height || rect.bottom < 48 || rect.top > window.innerHeight) {
1280
+ close();
1281
+ return;
1282
+ }
1283
+ x = rect.right;
1284
+ y = rect.bottom;
1285
+ }
1286
+ const width = host.offsetWidth;
1287
+ const height = host.offsetHeight;
1288
+ const left = Math.max(8, Math.min(window.innerWidth - width - 8, x + 8));
1289
+ const below = y + 8;
1290
+ const top = Math.max(48, Math.min(window.innerHeight - height - 8, below + height <= window.innerHeight - 8 ? below : y - height - 24));
1291
+ host.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
1292
+ };
1293
+ const schedule = () => {
1294
+ if (!frame) frame = requestAnimationFrame(position);
1295
+ };
1296
+ const observer = new ResizeObserver(schedule);
1297
+ const hide = () => {
1298
+ host.hidden = true;
1299
+ anchor = null;
1300
+ events?.abort();
1301
+ events = null;
1302
+ observer.disconnect();
1303
+ cancelAnimationFrame(frame);
1304
+ frame = 0;
1305
+ };
1306
+ return {
1307
+ show(next) {
1308
+ anchor = next;
1309
+ host.hidden = false;
1310
+ if (!events) {
1311
+ events = new AbortController();
1312
+ const signal = events.signal;
1313
+ document.addEventListener("scroll", (event) => {
1314
+ if (event.target instanceof Node && host.contains(event.target)) return;
1315
+ schedule();
1316
+ }, {
1317
+ capture: true,
1318
+ passive: true,
1319
+ signal
1320
+ });
1321
+ window.addEventListener("resize", schedule, {
1322
+ passive: true,
1323
+ signal
1324
+ });
1325
+ document.addEventListener("pointerdown", (event) => {
1326
+ const target = event.target;
1327
+ if (target instanceof Element && !host.contains(target) && !target.closest("[data-local-annotation-toolbar]")) close();
1328
+ }, {
1329
+ capture: true,
1330
+ signal
1331
+ });
1332
+ document.addEventListener("keydown", (event) => {
1333
+ if (event.key === "Escape" && !event.isComposing && event.keyCode !== 229) {
1334
+ event.preventDefault();
1335
+ close();
1336
+ }
1337
+ }, { signal });
1338
+ observer.observe(host);
1339
+ }
1340
+ schedule();
1341
+ },
1342
+ hide,
1343
+ dispose: hide
1344
+ };
1345
+ }
1346
+ // src/controller.ts
1347
+ var SENT_CACHE = 64;
1348
+ var Controller = class {
1349
+ ctx;
1350
+ store = new AnnotationStore();
1351
+ highlights = new Highlights((item) => this.showItem(item));
1352
+ listeners = /* @__PURE__ */ new Set();
1353
+ appearance = new AppearanceStore((appearance) => this.patch({ appearance }));
1354
+ state = {
1355
+ sessionId: void 0,
1356
+ items: [],
1357
+ attached: [],
1358
+ pending: null,
1359
+ error: "",
1360
+ loading: false,
1361
+ highlightSupported: this.highlights.supported,
1362
+ appearance: this.appearance.getSnapshot(),
1363
+ card: null
1364
+ };
1365
+ pendingBySession = /* @__PURE__ */ new Map();
1366
+ generation = 0;
1367
+ disposed = false;
1368
+ disposers = [];
1369
+ selection;
1370
+ input;
1371
+ disposeInput;
1372
+ /** Model text of chips already sent; a failed send restores chips whose rows are gone. */
1373
+ sent = /* @__PURE__ */ new Map();
1374
+ floating;
1375
+ anchor = {
1376
+ range: null,
1377
+ x: 24,
1378
+ y: 64
1379
+ };
1380
+ drafts = /* @__PURE__ */ new Map();
1381
+ constructor(ctx, host) {
1382
+ this.ctx = ctx;
1383
+ this.floating = floatingCard(host, () => this.close());
1384
+ this.selection = installSelection(() => this.state.sessionId, (item, range) => {
1385
+ this.anchor = {
1386
+ range,
1387
+ x: 24,
1388
+ y: 64
1389
+ };
1390
+ if (this.state.loading) return;
1391
+ if (this.state.pending) {
1392
+ this.showItem(this.state.pending);
1393
+ return;
1394
+ }
1395
+ if (this.state.items.length >= 200) {
1396
+ this.error(/* @__PURE__ */ new Error(`每个会话最多 200 条批注。`));
1397
+ this.showError();
1398
+ return;
1399
+ }
1400
+ if (this.pendingBySession.size >= 8 && !this.pendingBySession.has(item.sessionId)) {
1401
+ this.error(/* @__PURE__ */ new Error("已有 8 个会话保留未保存批注,请先保存或取消其中一条。"));
1402
+ this.showError();
1403
+ return;
1404
+ }
1405
+ this.pendingBySession.set(item.sessionId, item);
1406
+ this.patch({
1407
+ pending: item,
1408
+ error: "",
1409
+ card: {
1410
+ item,
1411
+ editing: true
1412
+ }
1413
+ });
1414
+ this.paint();
1415
+ this.floating.show(this.anchor);
1416
+ }, (error) => {
1417
+ this.error(error);
1418
+ this.showError();
1419
+ });
1420
+ const click = (event) => {
1421
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || !(event.target instanceof Element) || event.target.closest("a,button,input,textarea,[contenteditable=\"true\"],[data-local-annotation-card]")) return;
1422
+ if (!window.getSelection()?.isCollapsed) return;
1423
+ const item = this.highlights.hit(event.target, event.clientX, event.clientY);
1424
+ if (!item) return;
1425
+ this.anchor = {
1426
+ range: this.highlights.range(item),
1427
+ x: event.clientX,
1428
+ y: event.clientY
1429
+ };
1430
+ this.showItem(item);
1431
+ };
1432
+ document.addEventListener("click", click);
1433
+ this.disposers.push(() => document.removeEventListener("click", click));
1434
+ const sync = () => {
1435
+ const binding = this.ctx.uiSession.adapter.current.getSnapshot();
1436
+ const id = binding.key;
1437
+ if (id !== this.state.sessionId) {
1438
+ ++this.generation;
1439
+ this.selection.hide();
1440
+ this.close();
1441
+ this.patch({
1442
+ sessionId: id,
1443
+ items: [],
1444
+ pending: id ? this.pendingBySession.get(id) ?? null : null,
1445
+ error: "",
1446
+ loading: !!id
1447
+ });
1448
+ this.paint();
1449
+ if (id) this.refresh(id);
1450
+ }
1451
+ this.watchInput(binding);
1452
+ };
1453
+ this.disposers.push(ctx.uiSession.adapter.current.subscribe(sync));
1454
+ this.disposers.push(this.store.subscribe((id) => {
1455
+ if (id === this.state.sessionId) this.refresh(id);
1456
+ }));
1457
+ const onFocus = () => {
1458
+ if (this.state.sessionId) this.refresh(this.state.sessionId);
1459
+ };
1460
+ window.addEventListener("focus", onFocus);
1461
+ this.disposers.push(() => window.removeEventListener("focus", onFocus));
1462
+ sync();
1463
+ }
1464
+ subscribe = (listener) => {
1465
+ this.listeners.add(listener);
1466
+ return () => this.listeners.delete(listener);
1467
+ };
1468
+ getSnapshot = () => this.state;
1469
+ setAppearance(field, value) {
1470
+ this.appearance.set(field, value);
1471
+ }
1472
+ patch(patch) {
1473
+ if (this.disposed) return;
1474
+ this.state = {
1475
+ ...this.state,
1476
+ ...patch
1477
+ };
1478
+ for (const listener of this.listeners) listener();
1479
+ }
1480
+ error(error) {
1481
+ this.patch({ error: error instanceof Error ? error.message : String(error) });
1482
+ }
1483
+ /** Follow the current Session's composer; the status chip reads only this state. */
1484
+ watchInput(binding) {
1485
+ const source = binding.key ? binding.hooks.input : void 0;
1486
+ if (source !== this.input) {
1487
+ this.disposeInput?.();
1488
+ this.input = source;
1489
+ this.disposeInput = source?.subscribe(() => this.readInput());
1490
+ }
1491
+ this.readInput();
1492
+ }
1493
+ readInput() {
1494
+ const attached = attachedIds(this.input?.getSnapshot(), this.state.sessionId);
1495
+ const previous = this.state.attached;
1496
+ if (attached.length !== previous.length || attached.some((id, index) => id !== previous[index])) this.patch({ attached });
1497
+ }
1498
+ paint() {
1499
+ this.highlights.update(this.state.sessionId ?? "", this.state.pending ? [...this.state.items, this.state.pending] : this.state.items);
1500
+ }
1501
+ async refresh(id) {
1502
+ const generation = ++this.generation;
1503
+ try {
1504
+ const items = await this.store.list(id);
1505
+ if (this.disposed || generation !== this.generation || id !== this.state.sessionId) return;
1506
+ this.patch({
1507
+ items,
1508
+ loading: false
1509
+ });
1510
+ this.paint();
1511
+ } catch (error) {
1512
+ if (!this.disposed && generation === this.generation && id === this.state.sessionId) {
1513
+ this.patch({ loading: false });
1514
+ this.error(error);
1515
+ }
1516
+ }
1517
+ }
1518
+ close() {
1519
+ const pending = this.state.pending;
1520
+ if (pending && !this.draft(pending).trim()) {
1521
+ this.pendingBySession.delete(pending.sessionId);
1522
+ this.drafts.delete(this.draftKey(pending));
1523
+ this.patch({ pending: null });
1524
+ this.paint();
1525
+ }
1526
+ this.floating.hide();
1527
+ this.patch({ card: null });
1528
+ }
1529
+ showError() {
1530
+ if (!this.state.sessionId) return;
1531
+ this.patch({ card: {
1532
+ item: null,
1533
+ editing: false
1534
+ } });
1535
+ if (!this.anchor.range?.endContainer.isConnected) this.anchor = {
1536
+ range: null,
1537
+ x: Math.min(window.innerWidth / 2, 480),
1538
+ y: 80
1539
+ };
1540
+ this.floating.show(this.anchor);
1541
+ }
1542
+ showItem(item) {
1543
+ if (item.sessionId !== this.state.sessionId) return;
1544
+ const base = this.drafts.get(this.draftKey(item))?.item ?? item;
1545
+ this.patch({
1546
+ card: {
1547
+ item: base,
1548
+ editing: true
1549
+ },
1550
+ error: ""
1551
+ });
1552
+ this.anchor = {
1553
+ ...this.anchor,
1554
+ range: this.highlights.range(item)
1555
+ };
1556
+ this.floating.show(this.anchor);
1557
+ }
1558
+ draftKey(item) {
1559
+ return JSON.stringify([item.sessionId, item.id]);
1560
+ }
1561
+ draft(item) {
1562
+ return this.drafts.get(this.draftKey(item))?.note ?? item.note;
1563
+ }
1564
+ setDraft(item, note) {
1565
+ const key = this.draftKey(item);
1566
+ if (!this.drafts.has(key) && this.drafts.size >= 32) this.drafts.delete(this.drafts.keys().next().value);
1567
+ this.drafts.set(key, {
1568
+ item,
1569
+ note: note.slice(0, MAX_NOTE)
1570
+ });
1571
+ }
1572
+ async save(item, note) {
1573
+ if (item.sessionId !== this.state.sessionId || this.disposed) return;
1574
+ const id = item.sessionId;
1575
+ try {
1576
+ if (note.length > 8e3) throw new Error(`批注最多 ${MAX_NOTE} 个字符。`);
1577
+ const pending = this.state.pending?.id === item.id;
1578
+ const saved = await this.store.put({
1579
+ ...item,
1580
+ note
1581
+ }, pending ? void 0 : item.updatedAt);
1582
+ this.drafts.delete(this.draftKey(item));
1583
+ if (this.pendingBySession.get(id)?.id === item.id) this.pendingBySession.delete(id);
1584
+ if (this.disposed || id !== this.state.sessionId) return;
1585
+ if (this.state.pending?.id === item.id) this.patch({ pending: null });
1586
+ const items = this.state.items.filter((value) => value.id !== saved.id);
1587
+ items.push(saved);
1588
+ items.sort((a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id));
1589
+ this.patch({
1590
+ items,
1591
+ error: ""
1592
+ });
1593
+ if (this.state.card?.item?.id === item.id) this.close();
1594
+ this.paint();
1595
+ if (pending && !this.attach(saved) && this.state.error) this.showError();
1596
+ await this.refresh(id);
1597
+ } catch (error) {
1598
+ if (id === this.state.sessionId) this.error(error);
1599
+ throw error;
1600
+ }
1601
+ }
1602
+ cancelPending() {
1603
+ if (this.state.sessionId) this.pendingBySession.delete(this.state.sessionId);
1604
+ if (this.state.pending) this.drafts.delete(this.draftKey(this.state.pending));
1605
+ this.close();
1606
+ this.patch({
1607
+ pending: null,
1608
+ error: ""
1609
+ });
1610
+ this.paint();
1611
+ }
1612
+ async remove(item) {
1613
+ if (item.sessionId !== this.state.sessionId || this.disposed) return;
1614
+ try {
1615
+ await this.store.remove(item.sessionId, item.id, item.updatedAt);
1616
+ this.drafts.delete(this.draftKey(item));
1617
+ if (this.state.card?.item?.id === item.id) this.close();
1618
+ if (item.sessionId === this.state.sessionId) {
1619
+ this.patch({ error: "" });
1620
+ if (!this.detach(item)) this.error(/* @__PURE__ */ new Error(`批注 ${item.number} 已删除,但输入框里的引用未能移除,请手动删除。`));
1621
+ await this.refresh(item.sessionId);
1622
+ }
1623
+ } catch (error) {
1624
+ if (item.sessionId === this.state.sessionId) this.error(error);
1625
+ throw error;
1626
+ }
1627
+ }
1628
+ /** Chips are serialized by this source when the composer submits. */
1629
+ source = {
1630
+ trigger: "@",
1631
+ name: CHIP_SOURCE,
1632
+ showGroupTitle: false,
1633
+ candidates: () => Promise.resolve([]),
1634
+ onPick: () => void 0,
1635
+ openReference: (session, { ref }) => {
1636
+ const key = parseChipRef(ref);
1637
+ const item = key?.sessionId === session.sessionId ? this.state.items.find((value) => value.id === key.id) : void 0;
1638
+ if (!item) return false;
1639
+ this.showItem(item);
1640
+ return true;
1641
+ },
1642
+ codec: {
1643
+ clipboardText: (ref) => {
1644
+ const key = parseChipRef(ref);
1645
+ return key ? chipText(key.number) : ref;
1646
+ },
1647
+ serialize: (ref) => this.serialize(ref)
1648
+ }
1649
+ };
1650
+ /** Resolve one chip at submit time, then clear the sent annotation. */
1651
+ async serialize(ref) {
1652
+ const key = parseChipRef(ref);
1653
+ if (!key) throw new Error("批注引用无效,请从输入框删除后重试。");
1654
+ const item = (await this.store.list(key.sessionId)).find((value) => value.id === key.id);
1655
+ if (!item) {
1656
+ const sent = this.sent.get(ref);
1657
+ if (sent !== void 0) return sent;
1658
+ throw new Error("有一条批注已被删除,请从输入框删除它的引用后再发送。");
1659
+ }
1660
+ const block = annotationBlock(item);
1661
+ if (!this.sent.has(ref) && this.sent.size >= SENT_CACHE) this.sent.delete(this.sent.keys().next().value);
1662
+ this.sent.set(ref, block);
1663
+ this.store.remove(item.sessionId, item.id).then(() => {
1664
+ this.drafts.delete(this.draftKey(item));
1665
+ if (this.state.card?.item?.id === item.id) this.close();
1666
+ }, (error) => console.error("清理已发送批注失败", error));
1667
+ return block;
1668
+ }
1669
+ composer(sessionId) {
1670
+ const binding = this.ctx.uiSession.adapter.current.getSnapshot();
1671
+ if (!binding.key || binding.key !== sessionId || binding.key !== this.state.sessionId) return void 0;
1672
+ const input = binding.hooks.input?.getSnapshot();
1673
+ if (!binding.ctx || !input) throw new Error("输入框尚未就绪,请稍后重试。");
1674
+ return {
1675
+ binding,
1676
+ scope: binding.ctx,
1677
+ input
1678
+ };
1679
+ }
1680
+ /** Put this annotation's chip after the composer caret; returns whether the draft carries it. */
1681
+ attach(item) {
1682
+ try {
1683
+ const composer = this.composer(item.sessionId);
1684
+ if (!composer) return false;
1685
+ const { binding, scope, input } = composer;
1686
+ if (attachedIds(input, binding.key).includes(item.id)) return true;
1687
+ const actions = binding.props.inputActions;
1688
+ if (!actions || item.number < 1) throw new Error("输入框尚未就绪,请稍后重试。");
1689
+ if (input.phase !== "plain" || input.claim !== void 0) throw new Error("输入框正在提交或处于命令模式,批注已保存但未附加。");
1690
+ const { end, draftRev } = actions.captureInsertion();
1691
+ if (scope.bail(scope, "slash/input-insert-reference", {
1692
+ reference: annotationChip(item),
1693
+ span: {
1694
+ start: end,
1695
+ end,
1696
+ draftRev
1697
+ }
1698
+ }) !== true) throw new Error("输入框正在更新,批注已保存但未附加;请稍后重试。");
1699
+ return true;
1700
+ } catch (error) {
1701
+ this.error(error);
1702
+ return false;
1703
+ }
1704
+ }
1705
+ /** Remove this annotation's chips from the composer; returns whether none remain. */
1706
+ detach(item) {
1707
+ try {
1708
+ const composer = this.composer(item.sessionId);
1709
+ if (!composer) return true;
1710
+ const { binding, scope } = composer;
1711
+ let input = composer.input;
1712
+ for (let guard = 0; input && guard < 200; guard++) {
1713
+ const chip = input.occurrences.find((occurrence) => occurrence.source === "local-annotation" && parseChipRef(occurrence.ref)?.id === item.id);
1714
+ if (!chip) return true;
1715
+ if (input.phase !== "plain" || input.claim !== void 0) throw new Error("输入框正在提交或处于命令模式,暂时无法移除批注引用。");
1716
+ const span = chipSpan(input, chip);
1717
+ if (scope.bail(scope, "slash/input-insert-text", {
1718
+ text: "",
1719
+ span: {
1720
+ ...span,
1721
+ draftRev: input.draftRev
1722
+ }
1723
+ }) !== true) throw new Error("输入框正在更新,未能移除批注引用;请稍后重试。");
1724
+ const next = binding.hooks.input?.getSnapshot();
1725
+ if (next?.draftRev === input.draftRev) return true;
1726
+ input = next;
1727
+ }
1728
+ return true;
1729
+ } catch (error) {
1730
+ this.error(error);
1731
+ return false;
1732
+ }
1733
+ }
1734
+ dispose() {
1735
+ this.disposed = true;
1736
+ ++this.generation;
1737
+ this.selection.dispose();
1738
+ this.floating.dispose();
1739
+ this.drafts.clear();
1740
+ for (const dispose of this.disposers) dispose();
1741
+ this.disposeInput?.();
1742
+ this.input = void 0;
1743
+ this.sent.clear();
1744
+ this.highlights.dispose();
1745
+ this.appearance.dispose();
1746
+ this.store.dispose();
1747
+ this.pendingBySession.clear();
1748
+ this.listeners.clear();
1749
+ }
1750
+ };
1751
+ // src/view.tsx
1752
+ function Trash() {
1753
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1754
+ width: "14",
1755
+ height: "14",
1756
+ viewBox: "0 0 24 24",
1757
+ fill: "none",
1758
+ stroke: "currentColor",
1759
+ strokeWidth: "1.7",
1760
+ "aria-hidden": "true",
1761
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7" })
1762
+ });
1763
+ }
1764
+ function Close() {
1765
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1766
+ width: "14",
1767
+ height: "14",
1768
+ viewBox: "0 0 24 24",
1769
+ fill: "none",
1770
+ stroke: "currentColor",
1771
+ strokeWidth: "1.7",
1772
+ "aria-hidden": "true",
1773
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 6 12 12M18 6 6 18" })
1774
+ });
1775
+ }
1776
+ function brief(text, length) {
1777
+ return text.length > length ? text.slice(0, length) + "…" : text;
1778
+ }
1779
+ function Card({ controller }) {
1780
+ const state = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
1781
+ if (!state.card) return null;
1782
+ if (!state.card.item) return state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1783
+ "data-local-annotation-panel": "",
1784
+ role: "alert",
1785
+ children: [state.error, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1786
+ type: "button",
1787
+ className: "dla-icon",
1788
+ "aria-label": "关闭",
1789
+ onClick: () => controller.close(),
1790
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Close, {})
1791
+ })]
1792
+ }) : null;
1793
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Editor, {
1794
+ controller,
1795
+ item: state.card.item
1796
+ }, JSON.stringify([state.sessionId, state.card.item.id]));
1797
+ }
1798
+ function Editor({ controller, item }) {
1799
+ const state = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
1800
+ const [base] = (0, react.useState)(item);
1801
+ const [note, setNote] = (0, react.useState)(() => controller.draft(item));
1802
+ const [busy, setBusy] = (0, react.useState)(false);
1803
+ const [error, setError] = (0, react.useState)("");
1804
+ const textarea = (0, react.useRef)(null);
1805
+ const composing = (0, react.useRef)(false);
1806
+ const inFlight = (0, react.useRef)(false);
1807
+ const newItem = state.pending?.id === item.id;
1808
+ const quote = (item.source.kind === "file" ? `${item.source.path}\n` : "") + item.text;
1809
+ (0, react.useEffect)(() => {
1810
+ textarea.current?.focus({ preventScroll: true });
1811
+ textarea.current?.setSelectionRange(note.length, note.length);
1812
+ }, []);
1813
+ async function run(action) {
1814
+ if (inFlight.current) return;
1815
+ inFlight.current = true;
1816
+ setBusy(true);
1817
+ setError("");
1818
+ try {
1819
+ await action();
1820
+ } catch (cause) {
1821
+ setError(cause instanceof Error ? cause.message : "操作失败,请重试。");
1822
+ } finally {
1823
+ inFlight.current = false;
1824
+ setBusy(false);
1825
+ }
1826
+ }
1827
+ function keyDown(event) {
1828
+ if (composing.current || event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
1829
+ if (event.key === "Enter" && !event.shiftKey) {
1830
+ event.preventDefault();
1831
+ event.stopPropagation();
1832
+ run(() => controller.save(base, note));
1833
+ } else if (event.key === "Escape") {
1834
+ event.preventDefault();
1835
+ event.stopPropagation();
1836
+ controller.close();
1837
+ }
1838
+ }
1839
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1840
+ "data-local-annotation-panel": "",
1841
+ role: "dialog",
1842
+ "aria-label": newItem ? "添加批注" : "编辑批注",
1843
+ "aria-modal": false,
1844
+ children: [
1845
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1846
+ className: "dla-head",
1847
+ children: [
1848
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: newItem ? "添加批注" : "编辑批注" }),
1849
+ !newItem ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1850
+ type: "button",
1851
+ className: "dla-icon dla-danger",
1852
+ title: "删除批注",
1853
+ "aria-label": "删除批注",
1854
+ disabled: busy,
1855
+ onClick: () => {
1856
+ run(() => controller.remove(base));
1857
+ },
1858
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash, {})
1859
+ }) : null,
1860
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1861
+ type: "button",
1862
+ className: "dla-icon dla-close",
1863
+ title: "关闭",
1864
+ "aria-label": "关闭批注",
1865
+ onClick: () => controller.close(),
1866
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Close, {})
1867
+ })
1868
+ ]
1869
+ }),
1870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("blockquote", {
1871
+ className: "dla-quote",
1872
+ title: quote,
1873
+ children: brief(quote, 200)
1874
+ }),
1875
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1876
+ ref: textarea,
1877
+ "aria-label": "批注内容",
1878
+ placeholder: "写下批注(可留空,仅标记原文)",
1879
+ value: note,
1880
+ maxLength: MAX_NOTE,
1881
+ rows: 3,
1882
+ disabled: busy,
1883
+ spellCheck: false,
1884
+ onChange: (event) => {
1885
+ setNote(event.target.value);
1886
+ controller.setDraft(base, event.target.value);
1887
+ },
1888
+ onKeyDown: keyDown,
1889
+ onCompositionStart: () => {
1890
+ composing.current = true;
1891
+ },
1892
+ onCompositionEnd: () => {
1893
+ composing.current = false;
1894
+ }
1895
+ }),
1896
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
1897
+ className: "dla-footer",
1898
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppearancePicker, {
1899
+ controller,
1900
+ disabled: busy
1901
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1902
+ className: "dla-save",
1903
+ children: [!newItem && !state.attached.includes(item.id) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1904
+ type: "button",
1905
+ className: "dla-attach",
1906
+ disabled: busy,
1907
+ onClick: () => {
1908
+ if (controller.attach(base)) controller.close();
1909
+ },
1910
+ children: "附加到输入框"
1911
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1912
+ type: "button",
1913
+ disabled: busy || state.loading,
1914
+ onClick: () => {
1915
+ run(() => controller.save(base, note));
1916
+ },
1917
+ children: ["✓ ", busy ? "保存中…" : "保存"]
1918
+ })]
1919
+ })]
1920
+ }),
1921
+ error || state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1922
+ className: "dla-error",
1923
+ role: "alert",
1924
+ children: error || state.error
1925
+ }) : null
1926
+ ]
1927
+ });
1928
+ }
1929
+ function AppearancePicker({ controller, disabled }) {
1930
+ const { appearance } = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
1931
+ const [error, setError] = (0, react.useState)("");
1932
+ function change(field, value) {
1933
+ try {
1934
+ controller.setAppearance(field, value);
1935
+ setError("");
1936
+ } catch (cause) {
1937
+ setError(cause instanceof Error ? cause.message : "外观设置未保存。");
1938
+ }
1939
+ }
1940
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
1941
+ className: "dla-appearance",
1942
+ style: { "--dla-ink": ink(appearance.color) },
1943
+ children: [
1944
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: "批注外观" }),
1945
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1946
+ disabled,
1947
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: "划线" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1948
+ className: "dla-options",
1949
+ children: STROKES.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1950
+ type: "button",
1951
+ "aria-pressed": appearance.stroke === option.id,
1952
+ onClick: () => change("stroke", option.id),
1953
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1954
+ className: "dla-stroke-sample",
1955
+ "data-stroke": option.id,
1956
+ children: option.label
1957
+ })
1958
+ }, option.id))
1959
+ })]
1960
+ }),
1961
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1962
+ disabled,
1963
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: "颜色" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1964
+ className: "dla-options",
1965
+ children: [COLORS.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1966
+ type: "button",
1967
+ className: "dla-swatch",
1968
+ title: option.label,
1969
+ "aria-label": option.label,
1970
+ "aria-pressed": appearance.color.toLowerCase() === option.value,
1971
+ style: { "--dla-swatch": ink(option.value) },
1972
+ onClick: () => change("color", option.value)
1973
+ }, option.value)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1974
+ className: "dla-custom-color",
1975
+ title: "自定义荧光色",
1976
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1977
+ type: "color",
1978
+ "aria-label": "自定义荧光色",
1979
+ value: appearance.color === "theme" ? COLORS[1].value : appearance.color,
1980
+ onChange: (event) => change("color", event.target.value)
1981
+ }), "自选"]
1982
+ })]
1983
+ })]
1984
+ }),
1985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1986
+ disabled,
1987
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: "旁注图标" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1988
+ className: "dla-options",
1989
+ children: ICONS.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1990
+ type: "button",
1991
+ title: option.label,
1992
+ "aria-label": option.label,
1993
+ "aria-pressed": appearance.icon === option.id,
1994
+ onClick: () => change("icon", option.id),
1995
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1996
+ "aria-hidden": "true",
1997
+ className: "dla-marker-sample",
1998
+ style: {
1999
+ "--dla-marker-mask": `var(--dla-mask-${option.id})`,
2000
+ "--dla-marker-outline": `var(--dla-outline-${option.id})`,
2001
+ "--dla-marker-radius": option.id === "number" ? "var(--dsw-radius-sm)" : "0"
2002
+ },
2003
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "1" })
2004
+ })
2005
+ }, option.id))
2006
+ })]
2007
+ }),
2008
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2009
+ className: "dla-appearance-hint",
2010
+ children: "立即生效,应用于所有批注"
2011
+ }),
2012
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2013
+ className: "dla-error",
2014
+ role: "alert",
2015
+ children: error
2016
+ }) : null
2017
+ ]
2018
+ });
2019
+ }
2020
+ function ComposerStatus({ controller, sessionId }) {
2021
+ const state = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
2022
+ const [open, setOpen] = (0, react.useState)(false);
2023
+ (0, react.useEffect)(() => {
2024
+ setOpen(false);
2025
+ }, [sessionId, state.sessionId]);
2026
+ const items = state.attached.flatMap((id) => state.items.filter((item) => item.id === id));
2027
+ if (sessionId !== state.sessionId || !items.length) return null;
2028
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2029
+ "data-local-annotation-status": "",
2030
+ onMouseEnter: () => setOpen(true),
2031
+ onMouseLeave: () => setOpen(false),
2032
+ onFocus: () => setOpen(true),
2033
+ onBlur: (event) => {
2034
+ if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
2035
+ },
2036
+ onKeyDown: (event) => {
2037
+ if (event.key === "Escape") {
2038
+ event.stopPropagation();
2039
+ setOpen(false);
2040
+ }
2041
+ },
2042
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2043
+ type: "button",
2044
+ className: "dla-chip",
2045
+ "aria-expanded": open,
2046
+ onClick: () => setOpen((value) => !value),
2047
+ children: ["批注 ×", items.length]
2048
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2049
+ className: "dla-preview",
2050
+ role: "region",
2051
+ "aria-label": "本条消息附带的批注",
2052
+ children: [items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2053
+ className: "dla-preview-item",
2054
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2055
+ type: "button",
2056
+ className: "dla-summary",
2057
+ onClick: () => {
2058
+ setOpen(false);
2059
+ controller.showItem(item);
2060
+ },
2061
+ children: [
2062
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2063
+ className: "dla-number",
2064
+ "data-digits": String(item.number).length,
2065
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.number })
2066
+ }),
2067
+ brief(item.text, 100),
2068
+ item.note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2069
+ className: "dla-preview-note",
2070
+ children: brief(item.note, 60)
2071
+ }) : null
2072
+ ]
2073
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2074
+ type: "button",
2075
+ className: "dla-icon dla-danger",
2076
+ title: "从本条消息移除",
2077
+ "aria-label": `从输入框移除批注 ${item.number}`,
2078
+ onClick: () => {
2079
+ controller.detach(item);
2080
+ },
2081
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Close, {})
2082
+ })]
2083
+ }, item.id)), state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2084
+ className: "dla-error",
2085
+ role: "alert",
2086
+ children: state.error
2087
+ }) : null]
2088
+ }) : null]
2089
+ });
2090
+ }
2091
+ // src/bubble.tsx
2092
+ /** A sent annotation block inside a user message: one chip, its quote and note on hover. */
2093
+ function SentAnnotationChip({ node }) {
2094
+ const { number, quote, note, path } = node.data;
2095
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.HoverCard, {
2096
+ inline: true,
2097
+ openDelayMs: 200,
2098
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2099
+ className: "dla-sent-chip",
2100
+ tabIndex: 0,
2101
+ "aria-label": `批注 ${number}`,
2102
+ children: ["批注 ", number]
2103
+ }),
2104
+ content: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2105
+ className: "dla-sent-card",
2106
+ children: [
2107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2108
+ className: "dla-sent-head",
2109
+ children: [
2110
+ "批注 ",
2111
+ number,
2112
+ path ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2113
+ className: "dla-sent-path",
2114
+ children: path
2115
+ }) : null
2116
+ ]
2117
+ }),
2118
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("blockquote", { children: quote }),
2119
+ note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: note }) : null
2120
+ ]
2121
+ })
2122
+ });
2123
+ }
2124
+ // src/icons.ts
2125
+ /*! Lucide icon paths: https://lucide.dev — ISC License
2126
+ * Copyright (c) 2026 Lucide Icons and Contributors
2127
+ *
2128
+ * Permission to use, copy, modify, and/or distribute this software for any
2129
+ * purpose with or without fee is hereby granted, provided that the above
2130
+ * copyright notice and this permission notice appear in all copies.
2131
+ *
2132
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
2133
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
2134
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
2135
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
2136
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
2137
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
2138
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
2139
+ */
2140
+ var paths = {
2141
+ message: ["M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"],
2142
+ bookmark: ["M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z"],
2143
+ note: ["M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z", "M15 3v5a1 1 0 0 0 1 1h5"]
2144
+ };
2145
+ function mask(parts, outline, viewBox) {
2146
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" preserveAspectRatio="none" fill="${outline ? "none" : "white"}" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${parts.map((d) => `<path d="${d}"/>`).join("")}</svg>`;
2147
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
2148
+ }
2149
+ // src/styles.ts
2150
+ var STYLE = `
2151
+ :root { ${Object.entries(paths).map(([id, parts]) => {
2152
+ const viewBox = id === "bookmark" ? "4 0 16 24" : "1 0 22 24";
2153
+ return `--dla-mask-${id}:${mask(parts, false, viewBox)};--dla-outline-${id}:${mask(parts, true, viewBox)};`;
2154
+ }).join("\n") + "--dla-mask-number:none;--dla-outline-number:linear-gradient(transparent,transparent);"} }
2155
+ ::highlight(dsh-local-annotation) { background-color: var(--dsw-alias-bg-document-selection); }
2156
+ [data-local-annotation-card] { position: fixed; z-index: 1000; top: 0; left: 0; width: fit-content; }
2157
+ [data-local-annotation-card][hidden], [data-local-annotation-toolbar][hidden] { display: none; }
2158
+ [data-local-annotation-toolbar], [data-local-annotation-panel], .dla-preview {
2159
+ box-sizing: border-box; color: var(--dsw-alias-label-primary); background: var(--dsw-menu-surface-fill);
2160
+ backdrop-filter: var(--dsw-menu-backdrop-filter); border: 1px solid var(--dsw-alias-border-l2);
2161
+ border-radius: var(--dsw-radius-panel); corner-shape: var(--dsw-corner-shape); box-shadow: var(--dsw-elevation-panel);
2162
+ font: var(--dsw-font-xs-13); font-family: var(--dsw-font-family); -webkit-app-region: no-drag;
2163
+ }
2164
+ [data-local-annotation-toolbar] { position: fixed; z-index: 1000; padding: 3px; max-width: calc(100vw - 16px); }
2165
+ [data-local-annotation-panel] { width: min(320px, calc(100vw - 16px)); max-height: calc(100dvh - 64px); overflow: auto; padding: 12px; }
2166
+ [data-local-annotation-panel] .dla-head { display: flex; align-items: center; gap: 6px; font: var(--dsw-font-xs-strong-13); }
2167
+ [data-local-annotation-panel] .dla-close { margin-left: auto; }
2168
+ [data-local-annotation-panel] .dla-quote { margin: 10px 0; padding: 4px 9px; border-left: 2px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-tertiary); font: var(--dsw-font-xxs-12); max-height: 76px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
2169
+ [data-local-annotation-panel] textarea { box-sizing: border-box; width: 100%; min-height: 74px; resize: vertical; padding: 8px; font: inherit; color: var(--dsw-alias-label-primary); background: var(--dsw-specific-input-major); border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-md); corner-shape: var(--dsw-corner-shape); }
2170
+ [data-local-annotation-panel] button, [data-local-annotation-toolbar] button, [data-local-annotation-status] button { font: inherit; color: inherit; border: 0; background: transparent; border-radius: var(--dsw-radius-sm); corner-shape: var(--dsw-corner-shape); cursor: pointer; padding: 4px 8px; -webkit-app-region: no-drag; }
2171
+ :is([data-local-annotation-panel], [data-local-annotation-toolbar], [data-local-annotation-status]) button:hover { background: var(--dsw-alias-interactive-bg-hover); }
2172
+ :is([data-local-annotation-panel], [data-local-annotation-status]) .dla-icon { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 24px; height: 24px; padding: 4px; }
2173
+ :is([data-local-annotation-panel], [data-local-annotation-status]) .dla-danger { color: var(--dsw-alias-label-tertiary); }
2174
+ :is([data-local-annotation-panel], [data-local-annotation-status]) .dla-danger:hover { color: var(--dsw-alias-state-error-primary); background: var(--dsw-alias-interactive-bg-hover-danger); }
2175
+ :is([data-local-annotation-panel], [data-local-annotation-toolbar], [data-local-annotation-status]) :is(button,textarea):focus-visible, [data-local-annotation-marker]:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color); outline-offset: 2px; }
2176
+ [data-local-annotation-panel] button:disabled { opacity: .5; cursor: default; }
2177
+ .dla-error { color: var(--dsw-alias-state-error-primary); font: var(--dsw-font-xxs-12); overflow-wrap: anywhere; }
2178
+ [data-local-annotation-status] { position: relative; display: inline-flex; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-secondary); }
2179
+ [data-local-annotation-status] .dla-chip { border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-xl); padding: 2px 8px; white-space: nowrap; }
2180
+ [data-local-annotation-status] .dla-preview { position: absolute; bottom: 100%; left: 0; z-index: 1000; width: min(300px,calc(100vw - 32px)); max-height: min(360px,50dvh); overflow: auto; padding: 8px 10px; }
2181
+ [data-local-annotation-status] .dla-preview-item { display: flex; align-items: flex-start; gap: 4px; padding: 6px 0; border-bottom: 1px solid var(--dsw-alias-border-l2); }
2182
+ [data-local-annotation-status] .dla-summary { flex: 1; min-width: 0; padding: 0; text-align: left; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-tertiary); overflow-wrap: anywhere; }
2183
+ [data-local-annotation-status] .dla-preview-note { display: block; color: var(--dsw-alias-label-secondary); margin: 3px 0 0 22px; }
2184
+ [data-local-annotation-status] .dla-number { display: inline-grid; vertical-align: middle; width: 26px; height: 24px; margin-right: 6px; }
2185
+ [data-local-annotation-panel] .dla-attach { color: var(--dsw-alias-brand-text); }
2186
+ [data-local-annotation-marker] { display: grid; width: 26px; height: 24px; padding: 0; border: 0; background: transparent; cursor: pointer; pointer-events: auto; -webkit-app-region: no-drag; }
2187
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample) {
2188
+ position: relative; isolation: isolate; place-items: center; box-sizing: border-box;
2189
+ color: var(--dsw-alias-label-primary); font: var(--dsw-font-xxxs-strong-11); font-variant-numeric: tabular-nums;
2190
+ }
2191
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample)::before,
2192
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample)::after {
2193
+ content: ''; position: absolute; inset: 0; pointer-events: none; z-index: -1;
2194
+ border-radius: var(--dla-marker-radius, 0);
2195
+ mask: var(--dla-marker-mask, var(--dla-mask-message)) center / 100% 100% no-repeat;
2196
+ }
2197
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample)::before { background: color-mix(in srgb, var(--dla-ink) 26%, var(--dsw-specific-input-major)); }
2198
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample)::after {
2199
+ background: color-mix(in srgb, var(--dla-ink) 55%, var(--dsw-alias-label-primary));
2200
+ mask-image: var(--dla-marker-outline, var(--dla-outline-message));
2201
+ }
2202
+ [data-local-annotation-marker]:hover::before { background: color-mix(in srgb, var(--dla-ink) 45%, var(--dsw-specific-input-major)); }
2203
+ [data-local-annotation-marker]:hover { filter: drop-shadow(0 1px 2px var(--dsw-alias-border-l2)); }
2204
+ :is([data-local-annotation-marker], .dla-number, .dla-marker-sample) > span { position: relative; padding-bottom: 2px; }
2205
+ :is([data-local-annotation-marker], .dla-number)[data-digits='3'] > span { font-size: 9px; }
2206
+ [data-local-annotation-panel] .dla-footer { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-top: 8px; }
2207
+ [data-local-annotation-panel] .dla-save { display: flex; gap: 4px; margin: 0; margin-left: auto; flex: none; }
2208
+ .dla-appearance { min-width: 0; flex: 1; }
2209
+ .dla-appearance summary { width: fit-content; padding: 4px 0; cursor: pointer; color: var(--dsw-alias-label-secondary); font: var(--dsw-font-xxs-12); }
2210
+ .dla-appearance summary:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color); outline-offset: 2px; }
2211
+ .dla-appearance fieldset { border: 0; padding: 0; margin: 8px 0; min-width: 0; }
2212
+ .dla-appearance legend { padding: 0; margin-bottom: 5px; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-tertiary); }
2213
+ .dla-appearance .dla-options { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
2214
+ [data-local-annotation-panel] .dla-appearance button { border: 1px solid transparent; padding: 4px 6px; }
2215
+ [data-local-annotation-panel] .dla-appearance button[aria-pressed='true'] { border-color: var(--dsw-alias-brand-primary); background: var(--dsw-alias-interactive-bg-hover); }
2216
+ .dla-stroke-sample { display: inline-block; padding: 0 1px 3px; font: var(--dsw-font-xxs-12); }
2217
+ ${STROKES.map((option) => `.dla-stroke-sample[data-stroke='${option.id}'] { ${highlightCSS(option.id, "theme").replaceAll("var(--dsw-alias-brand-primary)", "var(--dla-ink)")} }`).join("\n")}
2218
+ [data-local-annotation-panel] .dla-appearance .dla-swatch { padding: 4px; width: 26px; height: 26px; border-radius: var(--dsw-radius-xl); }
2219
+ .dla-swatch::before { content: ''; display: block; width: 16px; height: 16px; border-radius: 50%; background: var(--dla-swatch); box-shadow: inset 0 0 0 1px var(--dsw-alias-border-l2); }
2220
+ .dla-custom-color { display: inline-flex; align-items: center; gap: 4px; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-secondary); cursor: pointer; }
2221
+ .dla-custom-color input { width: 24px; height: 24px; padding: 0; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: transparent; cursor: pointer; }
2222
+ .dla-custom-color input:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color); outline-offset: 2px; }
2223
+ .dla-marker-sample { display: inline-grid; width: 26px; height: 24px; }
2224
+ .dla-appearance-hint { font: var(--dsw-font-xxxs-strong-11); color: var(--dsw-alias-label-tertiary); margin: 4px 0; }
2225
+ .dla-sent-chip { margin: 0 2px; color: var(--dsw-alias-state-business-primary); font-weight: 500; cursor: default; border-radius: var(--dsw-radius-sm); }
2226
+ .dla-sent-chip:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color); outline-offset: 2px; }
2227
+ .dla-sent-card { min-width: 0; font: var(--dsw-font-xs-13); font-family: var(--dsw-font-family); color: var(--dsw-alias-label-primary); text-align: left; white-space: normal; overflow-wrap: anywhere; }
2228
+ .dla-sent-card .dla-sent-head { display: flex; gap: 6px; align-items: baseline; min-width: 0; margin-bottom: 6px; font: var(--dsw-font-xs-strong-13); }
2229
+ .dla-sent-card .dla-sent-path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-tertiary); font: var(--dsw-font-xxs-12); }
2230
+ .dla-sent-card blockquote { margin: 0; padding: 4px 9px; border-left: 2px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-tertiary); font: var(--dsw-font-xxs-12); white-space: pre-wrap; }
2231
+ .dla-sent-card p { margin: 6px 0 0; white-space: pre-wrap; }
2232
+ `;
2233
+ // src/client.tsx
2234
+ var name = "dsh-local-annotation";
2235
+ var inject = [
2236
+ "uiSession",
2237
+ "uiConversation",
2238
+ "slots",
2239
+ "inputTriggers"
2240
+ ];
2241
+ function apply(ctx) {
2242
+ ctx.effect(() => {
2243
+ const style = document.createElement("style");
2244
+ style.dataset.plugin = name;
2245
+ style.textContent = STYLE;
2246
+ document.head.append(style);
2247
+ const host = document.createElement("div");
2248
+ host.dataset.localAnnotationCard = "";
2249
+ host.hidden = true;
2250
+ document.body.append(host);
2251
+ let controller;
2252
+ let root;
2253
+ let disposeSource;
2254
+ let disposeStatus;
2255
+ const dispose = () => {
2256
+ disposeStatus?.();
2257
+ disposeSource?.();
2258
+ root?.unmount();
2259
+ controller?.dispose();
2260
+ host.remove();
2261
+ style.remove();
2262
+ };
2263
+ try {
2264
+ controller = new Controller(ctx, host);
2265
+ disposeSource = ctx.inputTriggers.registerSource(controller.source);
2266
+ root = (0, react_dom_client.createRoot)(host);
2267
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, { controller }));
2268
+ const active = controller;
2269
+ disposeStatus = ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
2270
+ name: "conversation.input.left",
2271
+ id: name,
2272
+ inject: (sessionId) => ({
2273
+ controller: active,
2274
+ sessionId
2275
+ })
2276
+ }, ComposerStatus));
2277
+ return dispose;
2278
+ } catch (error) {
2279
+ dispose();
2280
+ throw error;
2281
+ }
2282
+ }, `${name}: inline annotation card`);
2283
+ ctx.effect(() => ctx.uiConversation.userText.register(annotationRule), `${name}: sent annotation rule`);
2284
+ ctx.effect(() => ctx.slots.inject("conversation.user.text.node", () => ctx.slots.register({
2285
+ name: "conversation.user.text.node",
2286
+ key: CHIP_SOURCE,
2287
+ priority: 100
2288
+ }, SentAnnotationChip)), `${name}: sent annotation chip`);
2289
+ }
2290
+ exports.apply = apply;
2291
+ exports.inject = inject;
2292
+ exports.name = name;
2293
+
2294
+ return module.exports;
2295
+ },
2296
+ });