dsh-sidenote 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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/cordis.patch.yml +16 -0
  4. package/lib/client.js +2143 -0
  5. package/lib/index.js +6 -0
  6. package/lib/types/client/annotate/anchor.d.ts +57 -0
  7. package/lib/types/client/annotate/chip.d.ts +11 -0
  8. package/lib/types/client/annotate/draft.d.ts +29 -0
  9. package/lib/types/client/annotate/format.d.ts +52 -0
  10. package/lib/types/client/annotate/index.d.ts +2 -0
  11. package/lib/types/client/annotate/model.d.ts +59 -0
  12. package/lib/types/client/annotate/overlay.d.ts +27 -0
  13. package/lib/types/client/annotate/selection.d.ts +49 -0
  14. package/lib/types/client/bridge.d.ts +19 -0
  15. package/lib/types/client/index.d.ts +12 -0
  16. package/lib/types/client/locales.d.ts +77 -0
  17. package/lib/types/client/sidechat/SideChatPanel.d.ts +2 -0
  18. package/lib/types/client/sidechat/composer.d.ts +26 -0
  19. package/lib/types/client/sidechat/index.d.ts +2 -0
  20. package/lib/types/client/sidechat/model.d.ts +101 -0
  21. package/lib/types/client/sidechat/open.d.ts +16 -0
  22. package/lib/types/client/sidechat/slash.d.ts +12 -0
  23. package/lib/types/context-types.d.ts +335 -0
  24. package/lib/types/index.d.ts +9 -0
  25. package/package.json +96 -0
  26. package/src/client/annotate/anchor.ts +158 -0
  27. package/src/client/annotate/annotate.module.css +301 -0
  28. package/src/client/annotate/chip.tsx +115 -0
  29. package/src/client/annotate/css.d.ts +9 -0
  30. package/src/client/annotate/draft.ts +67 -0
  31. package/src/client/annotate/format.ts +83 -0
  32. package/src/client/annotate/index.tsx +66 -0
  33. package/src/client/annotate/model.ts +138 -0
  34. package/src/client/annotate/overlay.tsx +531 -0
  35. package/src/client/annotate/selection.ts +203 -0
  36. package/src/client/bridge.ts +19 -0
  37. package/src/client/index.tsx +22 -0
  38. package/src/client/locales.ts +175 -0
  39. package/src/client/sidechat/SideChatPanel.tsx +366 -0
  40. package/src/client/sidechat/composer.ts +129 -0
  41. package/src/client/sidechat/css-modules.d.ts +5 -0
  42. package/src/client/sidechat/index.tsx +56 -0
  43. package/src/client/sidechat/model.ts +331 -0
  44. package/src/client/sidechat/open.ts +66 -0
  45. package/src/client/sidechat/sidechat.module.css +230 -0
  46. package/src/client/sidechat/slash.ts +84 -0
  47. package/src/context-types.ts +369 -0
  48. package/src/index.ts +13 -0
package/lib/client.js ADDED
@@ -0,0 +1,2143 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-sidenote",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ let react_dom_client = require("react-dom/client");
11
+ //#region src/client/locales.ts
12
+ /** 英文词典(key 的权威清单)。 */
13
+ const en = {
14
+ menuTitle: "Side chat",
15
+ tabBaseTitle: "Side",
16
+ codeCopy: "Copy",
17
+ codeCopied: "Copied",
18
+ forkErrorTitle: "Couldn't create the side chat",
19
+ forkErrorHint: "The main session needs at least one completed turn to fork from. Close this tab with the × on the tab.",
20
+ missingTitle: "Session no longer exists",
21
+ missingDetail: "This side chat's session was removed and can't be restored.",
22
+ missingHint: "Close this tab with the × on the tab.",
23
+ preparing: "Preparing the side chat…",
24
+ historyFailed: "Failed to load the session history. Close and reopen this tab.",
25
+ emptyTitle: "Side chat",
26
+ emptyText: "Forked from the current session and evolves independently. Closing the tab removes it.",
27
+ thinking: "Thinking",
28
+ writing: "Writing…",
29
+ stopped: "Stopped",
30
+ toolLabel: "Tool",
31
+ failed: "failed",
32
+ running: "running…",
33
+ inputPlaceholder: "Message… (Enter to send, Shift+Enter for a newline)",
34
+ modelLabel: "Model: {name}",
35
+ modelFollowsMain: "follows main session",
36
+ stopReply: "Stop",
37
+ stopReplyTitle: "Stop the current reply",
38
+ send: "Send",
39
+ cmdDesc: "Open a side chat (forked from the current session)",
40
+ cmdNew: "New side chat",
41
+ cmdNewDetail: "Forked from the current session; evolves independently",
42
+ cmdFocus: "Focus \"{title}\"",
43
+ cmdFocusDetail: "Existing side chat",
44
+ addToConversation: "Add to conversation",
45
+ askInSideChat: "Ask in side chat",
46
+ toolbarAria: "Selection annotations",
47
+ notePlaceholder: "Add a note (optional)…",
48
+ confirmTitle: "Confirm",
49
+ saveNoteAria: "Save note",
50
+ sideNotePlaceholder: "Add a note for the side chat (optional)…",
51
+ sideNoteAria: "Side chat note",
52
+ confirmAskAria: "Confirm and ask",
53
+ openSideFailed: "Couldn't open the side chat. Try again.",
54
+ deleteNote: "Delete annotation",
55
+ cancel: "Cancel",
56
+ save: "Save",
57
+ chipOne: "1 annotation",
58
+ chipMany: "{n} annotations",
59
+ removeTitle: "Remove annotation",
60
+ removeAria: "Remove annotation {n}",
61
+ noteLine: "Note: {note}",
62
+ noNote: "(no note)"
63
+ };
64
+ /** 中文词典(与 en 同 key)。 */
65
+ const zh = {
66
+ menuTitle: "侧边聊天",
67
+ tabBaseTitle: "侧边",
68
+ codeCopy: "复制",
69
+ codeCopied: "已复制",
70
+ forkErrorTitle: "无法创建侧边聊天",
71
+ forkErrorHint: "主会话需要至少一轮已完成的对话才能 fork。点击标签上的 × 可关闭此标签页。",
72
+ missingTitle: "会话已不存在",
73
+ missingDetail: "此侧边聊天的会话已被移除,无法恢复。",
74
+ missingHint: "点击标签上的 × 关闭此标签页。",
75
+ preparing: "正在准备侧边聊天…",
76
+ historyFailed: "会话历史加载失败,可关闭后重新打开此标签页。",
77
+ emptyTitle: "侧边聊天",
78
+ emptyText: "侧边聊天从当前会话 fork,独立演进;关闭标签页后消失。",
79
+ thinking: "思考过程",
80
+ writing: "正在输出…",
81
+ stopped: "已停止",
82
+ toolLabel: "工具",
83
+ failed: "失败",
84
+ running: "执行中…",
85
+ inputPlaceholder: "输入消息…(Enter 发送,Shift+Enter 换行)",
86
+ modelLabel: "模型:{name}",
87
+ modelFollowsMain: "跟随主会话",
88
+ stopReply: "停止",
89
+ stopReplyTitle: "停止当前回复",
90
+ send: "发送",
91
+ cmdDesc: "打开侧边聊天(从当前会话 fork)",
92
+ cmdNew: "新建侧边聊天",
93
+ cmdNewDetail: "从当前会话 fork,独立演进",
94
+ cmdFocus: "聚焦「{title}」",
95
+ cmdFocusDetail: "已存在的侧边聊天",
96
+ addToConversation: "添加到对话",
97
+ askInSideChat: "在侧边聊天中提问",
98
+ toolbarAria: "划选注释",
99
+ notePlaceholder: "这里可以写自己的注解…",
100
+ confirmTitle: "确认",
101
+ saveNoteAria: "确认注解",
102
+ sideNotePlaceholder: "给侧边聊天写个注解(可空)…",
103
+ sideNoteAria: "侧边聊天注解",
104
+ confirmAskAria: "确认并提问",
105
+ openSideFailed: "打开侧边聊天失败,请重试",
106
+ deleteNote: "删除注释",
107
+ cancel: "取消",
108
+ save: "保存",
109
+ chipOne: "1 条注释",
110
+ chipMany: "{n} 条注释",
111
+ removeTitle: "移除注释",
112
+ removeAria: "移除注释 {n}",
113
+ noteLine: "注解:{note}",
114
+ noNote: "(无注解)"
115
+ };
116
+ let localeService;
117
+ /** Attach the DSH locale service (call once from the client apply). */
118
+ function attachLocale(service) {
119
+ localeService = service;
120
+ }
121
+ /** Subscribe to locale switches (component re-render driver). */
122
+ function subscribeLocale(fn) {
123
+ return localeService?.subscribe(fn) ?? (() => {});
124
+ }
125
+ /** The active locale id:DSH 语言设置优先,缺失时回退浏览器语言。 */
126
+ function activeLocale() {
127
+ return localeService?.getSnapshot().active ?? (typeof navigator !== "undefined" ? navigator.language : "") ?? "en";
128
+ }
129
+ /** Translate a copy key; `{name}` placeholders interpolate from `params`. */
130
+ function t(key, params) {
131
+ let text = (activeLocale().toLowerCase().startsWith("zh") ? zh : en)[key];
132
+ if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, String(value));
133
+ return text;
134
+ }
135
+ /** 语言切换时驱动组件重渲(useSyncExternalStore 标准接法)。 */
136
+ function useLocaleTick() {
137
+ (0, react.useSyncExternalStore)(subscribeLocale, () => localeService?.getSnapshot().active ?? "en");
138
+ }
139
+ //#endregion
140
+ //#region src/client/bridge.ts
141
+ const sideChatBridge = { current: null };
142
+ //#endregion
143
+ //#region src/client/sidechat/model.ts
144
+ /** Tab 类型 id(better-sidebar 注册表键;带包前缀防冲突)。 */
145
+ const SIDE_TAB_TYPE = "dsh-sidenote:side";
146
+ /** 多实例 Tab id:`side:<uuid>`(terminal/browser 的 createTab 铸 id 先例)。 */
147
+ function mintSideTabId() {
148
+ return `side:${crypto.randomUUID()}`;
149
+ }
150
+ /**
151
+ * 标题编号:首个「侧边」;并存时新 Tab「侧边 N」(N = 既有最大编号 + 1,
152
+ * 从标题解析)——关掉「侧边」再新建不会重名复用「侧边 2」。
153
+ * @param existingTitles - 创建时刻同会话侧边聊天 Tab 的标题集。
154
+ */
155
+ function sideTabTitle(existingTitles) {
156
+ const base = t("tabBaseTitle");
157
+ let max = 0;
158
+ for (const title of existingTitles) {
159
+ const match = /^(?:Side|侧边)(?: (\d+))?$/.exec(title);
160
+ if (match !== null) max = Math.max(max, match[1] === void 0 ? 1 : Number(match[1]));
161
+ }
162
+ return max <= 0 ? base : `${base} ${max + 1}`;
163
+ }
164
+ /**
165
+ * 容错解析 tab.meta:布局持久化里的 meta 来自上一版本的自己,字段缺失或
166
+ * 类型漂移一律降级为缺省,绝不抛错(刷新恢复路径不许崩)。
167
+ */
168
+ function parseSideChatMeta(meta) {
169
+ if (typeof meta !== "object" || meta === null) return {};
170
+ const raw = meta;
171
+ const out = {};
172
+ if (typeof raw.childId === "string" && raw.childId !== "") out.childId = raw.childId;
173
+ if (typeof raw.parentSessionId === "string" && raw.parentSessionId !== "") out.parentSessionId = raw.parentSessionId;
174
+ if (typeof raw.pendingDraft === "string" && raw.pendingDraft !== "") out.pendingDraft = raw.pendingDraft;
175
+ return out;
176
+ }
177
+ /** 清除 meta.pendingDraft(其余字段原样保留)。 */
178
+ function clearPendingDraft(meta) {
179
+ const { pendingDraft: _dropped, ...rest } = meta;
180
+ return rest;
181
+ }
182
+ function isTab(value) {
183
+ if (typeof value !== "object" || value === null) return false;
184
+ const tab = value;
185
+ return typeof tab.id === "string" && typeof tab.type === "string";
186
+ }
187
+ function walkTree(node, into) {
188
+ if (typeof node !== "object" || node === null) return;
189
+ const n = node;
190
+ if (n.kind === "leaf") {
191
+ if (Array.isArray(n.tabs)) {
192
+ for (const tab of n.tabs) if (isTab(tab)) into.push(tab);
193
+ }
194
+ return;
195
+ }
196
+ if (Array.isArray(n.children)) for (const child of n.children) walkTree(child, into);
197
+ }
198
+ /** 枚举侧栏状态(splits + bottomSplits 两棵树)里的全部 Tab。 */
199
+ function collectTabs(state) {
200
+ if (typeof state !== "object" || state === null) return [];
201
+ const s = state;
202
+ const into = [];
203
+ walkTree(s.splits, into);
204
+ walkTree(s.bottomSplits, into);
205
+ return into;
206
+ }
207
+ /** 枚举当前并存的侧边聊天 Tab(树顺序 = 打开顺序的近似)。 */
208
+ function collectSideTabs(state) {
209
+ return collectTabs(state).filter((tab) => tab.type === SIDE_TAB_TYPE);
210
+ }
211
+ /**
212
+ * blank 着陆页会话没有已完成 turn,fork 必败(host 返回 fork-unavailable)
213
+ * ——+ 菜单与 /side 命令的 available 用它禁用入口;面板内错误态兜底。
214
+ * 摘要求知(列表未就绪)时放行,交给 fork 错误态。
215
+ */
216
+ function canForkFrom(ctx, sessionId) {
217
+ try {
218
+ return (ctx.sessions.list.getSnapshot().byId?.[sessionId])?.blank !== true;
219
+ } catch {
220
+ return true;
221
+ }
222
+ }
223
+ function phaseOf(input) {
224
+ if (input.childId === void 0) return input.forkError === null ? "forking" : "fork-error";
225
+ if (input.bound) return "chat";
226
+ if (input.listPhase === "ready" && !input.listed) return "missing";
227
+ return "loading";
228
+ }
229
+ /** 注入草稿的拼接规则:空草稿直接落文本,否则换行追加(引文+问题的自然形态)。 */
230
+ function appendDraftText(draft, text) {
231
+ return draft.trim() === "" ? text : `${draft}\n${text}`;
232
+ }
233
+ /** 工具结果正文截断上限(面板是窄栏,超长输出不撑爆 DOM)。 */
234
+ const TOOL_TEXT_LIMIT = 4e3;
235
+ function truncateText(text, max) {
236
+ return text.length <= max ? text : `${text.slice(0, max)}…`;
237
+ }
238
+ /** ContentBlock[] → 纯文本:text 块拼接;image 块降级占位;其余忽略。 */
239
+ function contentTextOf(content) {
240
+ if (!Array.isArray(content)) return "";
241
+ const parts = [];
242
+ for (const block of content) {
243
+ if (typeof block !== "object" || block === null) continue;
244
+ const b = block;
245
+ if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
246
+ else if (b.type === "image") parts.push("[图片]");
247
+ }
248
+ return parts.join("\n");
249
+ }
250
+ function seqKey(prefix, node) {
251
+ return `${prefix}:${typeof node.seq === "number" ? node.seq : "?"}`;
252
+ }
253
+ function assistantParts(blocks) {
254
+ const texts = [];
255
+ const reasonings = [];
256
+ let hasToolCall = false;
257
+ if (Array.isArray(blocks)) for (const block of blocks) {
258
+ if (typeof block !== "object" || block === null) continue;
259
+ const b = block;
260
+ if (b.kind === "text" && typeof b.text === "string") texts.push(b.text);
261
+ else if (b.kind === "reasoning" && typeof b.text === "string") reasonings.push(b.text);
262
+ else if (b.kind === "tool-call") hasToolCall = true;
263
+ }
264
+ return {
265
+ text: texts.join("\n\n"),
266
+ reasoning: reasonings.join("\n\n"),
267
+ hasToolCall
268
+ };
269
+ }
270
+ /**
271
+ * 单个 ConversationNode → ChatMessage;返回 null = 面板不渲染
272
+ * (context 注入、未知面事件、已取消的重试等对读者无信息的节点)。
273
+ */
274
+ function nodeToMessage(node) {
275
+ if (typeof node !== "object" || node === null) return null;
276
+ const n = node;
277
+ switch (n.kind) {
278
+ case "user": return {
279
+ key: seqKey("u", n),
280
+ role: "user",
281
+ text: contentTextOf(n.content)
282
+ };
283
+ case "steering": return {
284
+ key: seqKey("s", n),
285
+ role: "user",
286
+ text: contentTextOf(n.content)
287
+ };
288
+ case "assistant": {
289
+ const { text, reasoning, hasToolCall } = assistantParts(n.blocks);
290
+ if (text === "" && reasoning === "" && hasToolCall) return null;
291
+ return {
292
+ key: seqKey("a", n),
293
+ role: "assistant",
294
+ text,
295
+ ...reasoning !== "" ? { reasoning } : {},
296
+ ...n.interrupted === true ? { interrupted: true } : {}
297
+ };
298
+ }
299
+ case "tool-result": {
300
+ const call = n.call;
301
+ const toolName = typeof call?.name === "string" ? call.name : typeof n.callId === "string" ? n.callId : "工具";
302
+ return {
303
+ key: seqKey("t", n),
304
+ role: "tool",
305
+ toolName,
306
+ text: truncateText(contentTextOf(n.content), TOOL_TEXT_LIMIT),
307
+ ...n.isError === true ? { isError: true } : {}
308
+ };
309
+ }
310
+ case "turn-error": return {
311
+ key: seqKey("e", n),
312
+ role: "error",
313
+ text: typeof n.message === "string" ? n.message : "未知错误"
314
+ };
315
+ case "model-retry":
316
+ if (n.retryState === "cancelled") return null;
317
+ return {
318
+ key: seqKey("r", n),
319
+ role: "notice",
320
+ text: n.retryState === "started" ? "模型请求失败,正在自动重试…" : "模型请求失败,等待自动重试…"
321
+ };
322
+ case "turn-max-tokens": return {
323
+ key: seqKey("m", n),
324
+ role: "notice",
325
+ text: "输出达到长度上限,本轮回复已截断。"
326
+ };
327
+ case "command": {
328
+ const name = typeof n.name === "string" && n.name !== "" ? `/${n.name}` : "/命令";
329
+ const args = typeof n.args === "string" ? n.args : "";
330
+ return {
331
+ key: seqKey("c", n),
332
+ role: "notice",
333
+ text: `执行命令 ${name}${args}`
334
+ };
335
+ }
336
+ case "compaction": return {
337
+ key: seqKey("k", n),
338
+ role: "notice",
339
+ text: "已压缩更早的对话上下文。"
340
+ };
341
+ default: return null;
342
+ }
343
+ }
344
+ /**
345
+ * ConversationSnapshot → 渲染消息列表:终态节点 + 在途工具调用 + 流式部分。
346
+ * 快照缺省(未绑定)时为空列表。
347
+ */
348
+ function transcriptOf(snapshot) {
349
+ if (snapshot === void 0 || snapshot === null) return [];
350
+ const out = [];
351
+ for (const node of snapshot.nodes ?? []) {
352
+ const message = nodeToMessage(node);
353
+ if (message !== null) out.push(message);
354
+ }
355
+ if (Array.isArray(snapshot.runningCalls)) for (const call of snapshot.runningCalls) {
356
+ if (typeof call !== "object" || call === null) continue;
357
+ const c = call;
358
+ out.push({
359
+ key: `rc:${typeof c.callId === "string" ? c.callId : "?"}`,
360
+ role: "tool",
361
+ toolName: typeof c.name === "string" ? c.name : "工具",
362
+ text: "",
363
+ streaming: true
364
+ });
365
+ }
366
+ const partial = snapshot.partial;
367
+ if (partial !== void 0 && partial !== null) {
368
+ const { text, reasoning, hasToolCall } = assistantParts(partial.blocks);
369
+ if (text !== "" || reasoning !== "" || !hasToolCall) out.push({
370
+ key: "partial",
371
+ role: "assistant",
372
+ text,
373
+ ...reasoning !== "" ? { reasoning } : {},
374
+ streaming: true
375
+ });
376
+ }
377
+ return out;
378
+ }
379
+ //#endregion
380
+ //#region src/client/sidechat/composer.ts
381
+ /**
382
+ * 侧边聊天 composer 的状态层:优先复用会话 input 机器(草稿机),
383
+ * 拿不到时整体降级为本地草稿 + session.prompt。
384
+ *
385
+ * 机器路径(dsh-better-sidebar src/client/conversation-draft.ts 同款惰性模式):
386
+ * ctx.sessions.scope(childId) 取会话 scope ctx
387
+ * → ctx.get('conversation') 惰性取会话服务
388
+ * → conversation.input.for(actx) 取该会话的 SessionInput 草稿机
389
+ * (draft 读写 / submit / notify 全在这张脸上)。
390
+ *
391
+ * 已知风险(design.md 已标注的 spike 点):InputHub 的 shell 由 sessions
392
+ * provide materialization 触发创建,「从未 staged 的会话 scope」可能没有
393
+ * resident shell —— 任何一步失败都降级,绝不崩面板。
394
+ */
395
+ const NOOP_UNSUBSCRIBE$1 = () => {};
396
+ /**
397
+ * 解析 childId 会话的 input 草稿机;任何一步缺失/抛错返回 null
398
+ * (调用方走降级路径)。
399
+ */
400
+ function resolveSessionInput(ctx, childId) {
401
+ try {
402
+ const actx = ctx.sessions.scope(childId);
403
+ if (actx === void 0) return null;
404
+ const conversation = ctx.get("conversation");
405
+ if (conversation === void 0 || conversation === null) return null;
406
+ const input = conversation.input.for(actx);
407
+ if (input === null || input === void 0) return null;
408
+ if (typeof input.setDraft !== "function" || typeof input.submit !== "function") return null;
409
+ if (input.state === void 0 || typeof input.state.getSnapshot !== "function") return null;
410
+ return input;
411
+ } catch {
412
+ return null;
413
+ }
414
+ }
415
+ /** 读草稿机的当前草稿(容错)。 */
416
+ function readInputDraft(input) {
417
+ try {
418
+ const draft = input.state.getSnapshot().draft;
419
+ return typeof draft === "string" ? draft : "";
420
+ } catch {
421
+ return "";
422
+ }
423
+ }
424
+ /**
425
+ * 面板 composer hook。机器路径:草稿住在 input 机器的 store 里(随会话持久、
426
+ * 与主输入框同语义),submit 走机器事务(序列化/裁决/默认 sink 全在机器内)。
427
+ * 降级路径:草稿住本地 state,发送直连 session.prompt([...], 'queue'),
428
+ * 失败时回填草稿并报错。
429
+ */
430
+ function useComposer(ctx, session, childId) {
431
+ const input = (0, react.useMemo)(() => childId === void 0 ? null : resolveSessionInput(ctx, childId), [ctx, childId]);
432
+ const machineDraft = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => input === null ? NOOP_UNSUBSCRIBE$1 : input.state.subscribe(notify), [input]), () => input === null ? "" : readInputDraft(input));
433
+ const [localDraft, setLocalDraft] = (0, react.useState)("");
434
+ const [sendError, setSendError] = (0, react.useState)(null);
435
+ const draft = input === null ? localDraft : machineDraft;
436
+ const setDraft = (0, react.useCallback)((text) => {
437
+ if (input === null) setLocalDraft(text);
438
+ else input.setDraft(text);
439
+ }, [input]);
440
+ const appendDraft = (0, react.useCallback)((text) => {
441
+ if (input === null) setLocalDraft((d) => appendDraftText(d, text));
442
+ else input.setDraft(appendDraftText(readInputDraft(input), text));
443
+ }, [input]);
444
+ const submit = (0, react.useCallback)(() => {
445
+ const text = draft.trim();
446
+ if (text === "") return;
447
+ if (input !== null) {
448
+ input.submit();
449
+ return;
450
+ }
451
+ if (session === void 0) return;
452
+ setLocalDraft("");
453
+ setSendError(null);
454
+ session.prompt([{
455
+ type: "text",
456
+ text
457
+ }], "queue").then((result) => {
458
+ const r = result;
459
+ if (r !== void 0 && r.ok === false) {
460
+ setLocalDraft((d) => d === "" ? text : d);
461
+ setSendError(r.error?.message ?? "发送失败");
462
+ }
463
+ }).catch((error) => {
464
+ setLocalDraft((d) => d === "" ? text : d);
465
+ setSendError(error instanceof Error ? error.message : "发送失败");
466
+ });
467
+ }, [
468
+ draft,
469
+ input,
470
+ session
471
+ ]);
472
+ return {
473
+ machine: input !== null,
474
+ draft,
475
+ sendError,
476
+ setDraft,
477
+ appendDraft,
478
+ submit
479
+ };
480
+ }
481
+ //#endregion
482
+ //#region src/client/sidechat/open.ts
483
+ /** 从最新快照读一个 Tab(meta 合并写入前的读取面;布局即注册表)。 */
484
+ function readTab(ctx, tabId) {
485
+ return collectTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => tab.id === tabId);
486
+ }
487
+ /**
488
+ * 为 sessionId 新建或聚焦一个侧边聊天 Tab;draftText 给出时写入其草稿。
489
+ * @returns false = 未能打开(调用方保持自己的流程)。
490
+ */
491
+ function openOrFocusSideChat(ctx, sessionId, draftText) {
492
+ try {
493
+ const snapshot = ctx.betterSidebar.getSnapshot();
494
+ if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
495
+ const existing = collectSideTabs(snapshot.state);
496
+ if (existing.length > 0) {
497
+ const target = existing[existing.length - 1];
498
+ if (draftText !== void 0 && draftText !== "") {
499
+ const meta = parseSideChatMeta(target.meta);
500
+ const input = meta.childId === void 0 ? null : resolveSessionInput(ctx, meta.childId);
501
+ if (input !== null) input.setDraft(appendDraftText(readInputDraft(input), draftText));
502
+ else ctx.betterSidebar.updateTab(target.id, { meta: {
503
+ ...meta,
504
+ pendingDraft: appendDraftText(meta.pendingDraft ?? "", draftText)
505
+ } });
506
+ }
507
+ ctx.betterSidebar.activateTab(target.id, { sessionId });
508
+ return true;
509
+ }
510
+ const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
511
+ ctx.betterSidebar.openTab({ type: SIDE_TAB_TYPE }, { sessionId });
512
+ const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
513
+ if (created === void 0) return false;
514
+ if (draftText !== void 0 && draftText !== "") ctx.betterSidebar.updateTab(created.id, { meta: { pendingDraft: draftText } });
515
+ return true;
516
+ } catch (error) {
517
+ console.warn("[dsh-sidenote] 打开侧边聊天失败:", error);
518
+ return false;
519
+ }
520
+ }
521
+ //#endregion
522
+ //#region \0dsh-css:/Users/gaoyixuan3/gyx_personal_files/dsh_project/dsh-sidenote/src/client/sidechat/sidechat.module.css.mjs
523
+ const css$1 = ".F1HU0W_root{height:100%;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:13px;display:flex}.F1HU0W_body{flex-direction:column;flex:1;padding:12px 10px;display:flex;overflow-y:auto}.F1HU0W_transcript{flex-direction:column;gap:12px;display:flex}.F1HU0W_empty,.F1HU0W_stateScreen{text-align:center;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px 16px;display:flex}.F1HU0W_emptyIcon{color:var(--dsw-alias-label-tertiary)}.F1HU0W_emptyTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600}.F1HU0W_emptyText{color:var(--dsw-alias-label-tertiary);max-width:240px;font-size:12px;line-height:1.6}.F1HU0W_stateDetail{color:var(--dsw-alias-state-error-primary);word-break:break-word;max-width:260px;font-size:12px;line-height:1.6}.F1HU0W_userRow{justify-content:flex-end;display:flex}.F1HU0W_userBubble{background:var(--dsw-alias-button-info-fill);color:#fff;white-space:pre-wrap;word-break:break-word;border-radius:12px 12px 2px;max-width:88%;padding:8px 12px}.F1HU0W_assistantRow{justify-content:flex-start;display:flex}.F1HU0W_assistantBody{word-break:break-word;max-width:100%;font-size:13px;line-height:1.6}.F1HU0W_reasoning{border-left:3px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);margin-bottom:6px;padding-left:8px;font-size:12px}.F1HU0W_reasoning>summary{cursor:pointer;user-select:none}.F1HU0W_reasoningBody{white-space:pre-wrap;word-break:break-word;margin-top:4px}.F1HU0W_streamingHint{color:var(--dsw-alias-label-dimmed)}.F1HU0W_toolCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;overflow:hidden}.F1HU0W_toolHead{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;padding:5px 10px;font-size:12px;display:flex}.F1HU0W_toolError{color:var(--dsw-alias-state-error-primary)}.F1HU0W_toolRunning{color:var(--dsw-alias-label-tertiary)}.F1HU0W_toolBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;max-height:160px;padding:8px 10px;font-size:12px;overflow-y:auto}.F1HU0W_errorRow{color:var(--dsw-alias-state-error-primary);white-space:pre-wrap;padding:4px 2px;font-size:12px}.F1HU0W_noticeRow{color:var(--dsw-alias-label-dimmed);text-align:center;padding:2px 0;font-size:12px}.F1HU0W_composer{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:6px;padding:8px 10px;display:flex}.F1HU0W_input{resize:vertical;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);width:100%;min-height:40px;max-height:160px;color:var(--dsw-alias-label-primary);font:inherit;box-sizing:border-box;border-radius:8px;padding:8px 10px}.F1HU0W_input:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}.F1HU0W_composerFoot{justify-content:space-between;align-items:center;gap:8px;display:flex}.F1HU0W_modelLabel{color:var(--dsw-alias-label-dimmed);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.F1HU0W_sendButton,.F1HU0W_stopButton{appearance:none;border:1px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;border-radius:8px;flex:none;align-items:center;gap:4px;padding:6px 12px;font-size:13px;display:inline-flex}.F1HU0W_sendButton:disabled{opacity:.55;cursor:default}";
524
+ const tagId$1 = "dsh-sidenote/sidechat.module.css";
525
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
526
+ const tag = document.createElement("style");
527
+ tag.dataset.plugin = "dsh-sidenote";
528
+ tag.dataset.pluginCss = tagId$1;
529
+ tag.textContent = css$1;
530
+ document.head.appendChild(tag);
531
+ }
532
+ var sidechat_module_css_default = {
533
+ "assistantRow": "F1HU0W_assistantRow",
534
+ "composer": "F1HU0W_composer",
535
+ "stateScreen": "F1HU0W_stateScreen",
536
+ "empty": "F1HU0W_empty",
537
+ "toolError": "F1HU0W_toolError",
538
+ "errorRow": "F1HU0W_errorRow",
539
+ "modelLabel": "F1HU0W_modelLabel",
540
+ "toolBody": "F1HU0W_toolBody",
541
+ "noticeRow": "F1HU0W_noticeRow",
542
+ "stateDetail": "F1HU0W_stateDetail",
543
+ "transcript": "F1HU0W_transcript",
544
+ "assistantBody": "F1HU0W_assistantBody",
545
+ "stopButton": "F1HU0W_stopButton",
546
+ "sendButton": "F1HU0W_sendButton",
547
+ "emptyText": "F1HU0W_emptyText",
548
+ "emptyTitle": "F1HU0W_emptyTitle",
549
+ "toolHead": "F1HU0W_toolHead",
550
+ "composerFoot": "F1HU0W_composerFoot",
551
+ "reasoning": "F1HU0W_reasoning",
552
+ "root": "F1HU0W_root",
553
+ "body": "F1HU0W_body",
554
+ "toolRunning": "F1HU0W_toolRunning",
555
+ "userBubble": "F1HU0W_userBubble",
556
+ "reasoningBody": "F1HU0W_reasoningBody",
557
+ "emptyIcon": "F1HU0W_emptyIcon",
558
+ "userRow": "F1HU0W_userRow",
559
+ "toolCard": "F1HU0W_toolCard",
560
+ "input": "F1HU0W_input",
561
+ "streamingHint": "F1HU0W_streamingHint"
562
+ };
563
+ //#endregion
564
+ //#region src/client/sidechat/SideChatPanel.tsx
565
+ /**
566
+ * 侧边聊天 Tab 面板:fork 编排(首开)/ 绑定恢复(刷新后)/ 消息流 / composer。
567
+ *
568
+ * 打开流程(design.md 详细方案 2):组件挂载时 tab.meta 无 childId →
569
+ * ctx.sessions.fork({ sessionId: scope.sessionId })(fork 时刻全量历史快照)
570
+ * → ctx.workspaces.archiveSession(childId)(durable 隐藏出会话列表)
571
+ * → ctx.betterSidebar.updateTab(tab.id, { meta: { childId, parentSessionId } })
572
+ * (Tab meta 即注册表,随布局持久化,刷新/重启后恢复)。
573
+ * fork 失败(blank 会话无已完成 turn 等)→ 中文错误态 + 关闭指引,不崩页面。
574
+ *
575
+ * 恢复流程:有 meta.childId → ctx.sessions.binding(childId) 直接绑定;
576
+ * 列表就绪后仍不在列 → 「会话已不存在」态。
577
+ *
578
+ * 消息流:binding.session 快照订阅(useSyncExternalStore),visible=false 时
579
+ * 暂停订阅。已知偏差(记录于此):client-runtime 只为 staged(当前选中)会话
580
+ * 打开事件窗口,非 staged 会话 openState 停留 'cold' 且 acceptLiveEvent 丢弃
581
+ * 事件 —— 因此绑定后对该会话调一次 concrete Session 的 open()(off-face、
582
+ * feature-check、幂等),否则消息流永远为空。
583
+ */
584
+ /** MarkdownText 代码块复制按钮文案(契约要求中文;引用稳定,变了会清流式缓存)。 */
585
+ /** MarkdownText 代码块复制按钮文案(跟随 DSH 语言;函数内读取保持引用稳定)。 */
586
+ function codeLabels() {
587
+ return {
588
+ copyLabel: t("codeCopy"),
589
+ copiedLabel: t("codeCopied")
590
+ };
591
+ }
592
+ const NOOP_UNSUBSCRIBE = () => {};
593
+ /**
594
+ * 打开非 staged 会话的事件窗口(off-face:open() 在 concrete Session 上是
595
+ * public 且幂等,但不在 SessionFace 契约上 —— 契约面只有 staged 会话会被
596
+ * 运行时自动 open)。feature-check + 吞错,运行时若移除则降级为只发不收。
597
+ */
598
+ function openSessionWindow(session) {
599
+ const openable = session;
600
+ if (typeof openable?.open !== "function") return;
601
+ openable.open().catch((error) => {
602
+ console.warn("[dsh-sidenote] 会话窗口打开失败:", error);
603
+ });
604
+ }
605
+ function SideChatPanel(props) {
606
+ useLocaleTick();
607
+ const { ctx, scope, tab, visible } = props;
608
+ const meta = parseSideChatMeta(tab.meta);
609
+ const childId = meta.childId;
610
+ const [forkError, setForkError] = (0, react.useState)(null);
611
+ const forkStarted = (0, react.useRef)(false);
612
+ const { store } = props;
613
+ (0, react.useEffect)(() => {
614
+ const mutable = store;
615
+ try {
616
+ mutable?.update?.((state) => {
617
+ if (state.panelOpen === false) state.panelOpen = true;
618
+ });
619
+ } catch {}
620
+ }, [store]);
621
+ (0, react.useEffect)(() => {
622
+ if (childId !== void 0 || forkStarted.current) return;
623
+ forkStarted.current = true;
624
+ let cancelled = false;
625
+ (async () => {
626
+ try {
627
+ const forked = await ctx.sessions.fork({ sessionId: scope.sessionId });
628
+ const current = parseSideChatMeta(readTab(ctx, tab.id)?.meta);
629
+ ctx.betterSidebar.updateTab(tab.id, { meta: {
630
+ ...current,
631
+ childId: forked,
632
+ parentSessionId: scope.sessionId
633
+ } });
634
+ try {
635
+ await ctx.workspaces.archiveSession(forked);
636
+ } catch (error) {
637
+ console.warn("[dsh-sidenote] 归档侧边会话失败(会话列表可能短暂可见):", error);
638
+ }
639
+ try {
640
+ const parentModels = await ctx.connection.api.sessions.models({ sessionId: scope.sessionId });
641
+ if (parentModels.result.ok) {
642
+ const current = parentModels.result.value.current;
643
+ await ctx.connection.api.sessions.selectModel({
644
+ sessionId: forked,
645
+ provider: current.provider,
646
+ model: current.model,
647
+ ...current.reasoningEffort !== void 0 ? { reasoningEffort: current.reasoningEffort } : {}
648
+ });
649
+ }
650
+ } catch (error) {
651
+ console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型):", error);
652
+ }
653
+ } catch (error) {
654
+ if (!cancelled) setForkError(error instanceof Error ? error.message : String(error));
655
+ }
656
+ })();
657
+ return () => {
658
+ cancelled = true;
659
+ };
660
+ }, [
661
+ ctx,
662
+ scope.sessionId,
663
+ tab.id,
664
+ childId
665
+ ]);
666
+ const listSnap = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => ctx.sessions.list.subscribe(notify), [ctx]), () => ctx.sessions.list.getSnapshot());
667
+ const listed = childId !== void 0 && listSnap.byId?.[childId] !== void 0;
668
+ const session = (childId === void 0 ? void 0 : ctx.sessions.binding(childId))?.session;
669
+ (0, react.useEffect)(() => {
670
+ openSessionWindow(session);
671
+ }, [session]);
672
+ const phase = phaseOf({
673
+ childId,
674
+ forkError,
675
+ bound: session !== void 0,
676
+ listPhase: listSnap.phase,
677
+ listed
678
+ });
679
+ const snapshot = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => visible && session !== void 0 ? session.subscribe(notify) : NOOP_UNSUBSCRIBE, [visible, session]), () => session === void 0 ? null : session.getSnapshot());
680
+ const messages = (0, react.useMemo)(() => transcriptOf(snapshot), [snapshot]);
681
+ const composer = useComposer(ctx, session, childId);
682
+ const [modelName, setModelName] = (0, react.useState)(null);
683
+ (0, react.useEffect)(() => {
684
+ if (childId === void 0 || session === void 0) return;
685
+ let cancelled = false;
686
+ ctx.connection.api.sessions.models({ sessionId: childId }).then((res) => {
687
+ if (!cancelled && res.result.ok) setModelName(res.result.value.current.model);
688
+ }).catch(() => {});
689
+ return () => {
690
+ cancelled = true;
691
+ };
692
+ }, [
693
+ ctx,
694
+ childId,
695
+ session
696
+ ]);
697
+ const pendingDraft = meta.pendingDraft;
698
+ (0, react.useEffect)(() => {
699
+ if (pendingDraft === void 0 || pendingDraft === "" || phase !== "chat") return;
700
+ composer.appendDraft(pendingDraft);
701
+ const current = parseSideChatMeta(readTab(ctx, tab.id)?.meta);
702
+ ctx.betterSidebar.updateTab(tab.id, { meta: clearPendingDraft(current) });
703
+ }, [
704
+ pendingDraft,
705
+ phase,
706
+ ctx,
707
+ tab.id
708
+ ]);
709
+ const bodyRef = (0, react.useRef)(null);
710
+ const tailKey = messages.length === 0 ? "" : `${messages[messages.length - 1].key}:${messages[messages.length - 1].text.length}:${messages[messages.length - 1].reasoning?.length ?? 0}`;
711
+ (0, react.useEffect)(() => {
712
+ const el = bodyRef.current;
713
+ if (el !== null && visible) el.scrollTop = el.scrollHeight;
714
+ }, [tailKey, visible]);
715
+ if (phase === "fork-error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateScreen, {
716
+ title: t("forkErrorTitle"),
717
+ detail: forkError ?? void 0,
718
+ hint: t("forkErrorHint")
719
+ });
720
+ if (phase === "missing") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateScreen, {
721
+ title: t("missingTitle"),
722
+ detail: t("missingDetail"),
723
+ hint: t("missingHint")
724
+ });
725
+ if (phase === "forking" || phase === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateScreen, { title: t("preparing") });
726
+ const running = snapshot?.running === true;
727
+ const openFailed = snapshot?.openState === "error";
728
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
729
+ className: sidechat_module_css_default.root,
730
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
731
+ ref: bodyRef,
732
+ className: sidechat_module_css_default.body,
733
+ children: [messages.length === 0 && !running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, { messages }), openFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
734
+ className: sidechat_module_css_default.errorRow,
735
+ children: t("historyFailed")
736
+ })]
737
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerBar, {
738
+ ctx,
739
+ session,
740
+ composer,
741
+ running,
742
+ visible,
743
+ modelName
744
+ })]
745
+ });
746
+ }
747
+ /** 空状态:💬 类图标 + 标题 + fork 语义文案(形态规格)。 */
748
+ function EmptyState() {
749
+ useLocaleTick();
750
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
751
+ className: sidechat_module_css_default.empty,
752
+ children: [
753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
754
+ className: sidechat_module_css_default.emptyIcon,
755
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutline16, { size: 32 })
756
+ }),
757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
758
+ className: sidechat_module_css_default.emptyTitle,
759
+ children: t("emptyTitle")
760
+ }),
761
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
762
+ className: sidechat_module_css_default.emptyText,
763
+ children: t("emptyText")
764
+ })
765
+ ]
766
+ });
767
+ }
768
+ /** 加载 / 错误整屏态(标题 + 可选详情 + 可选指引)。 */
769
+ function StateScreen(props) {
770
+ useLocaleTick();
771
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
772
+ className: sidechat_module_css_default.stateScreen,
773
+ children: [
774
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
775
+ className: sidechat_module_css_default.emptyIcon,
776
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutline16, { size: 32 })
777
+ }),
778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
779
+ className: sidechat_module_css_default.emptyTitle,
780
+ children: props.title
781
+ }),
782
+ props.detail !== void 0 && props.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
783
+ className: sidechat_module_css_default.stateDetail,
784
+ children: props.detail
785
+ }),
786
+ props.hint !== void 0 && props.hint !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
787
+ className: sidechat_module_css_default.emptyText,
788
+ children: props.hint
789
+ })
790
+ ]
791
+ });
792
+ }
793
+ function MessageList({ messages }) {
794
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
795
+ className: sidechat_module_css_default.transcript,
796
+ children: messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageRow, { message }, message.key))
797
+ });
798
+ }
799
+ function MessageRow({ message }) {
800
+ useLocaleTick();
801
+ switch (message.role) {
802
+ case "user": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
803
+ className: sidechat_module_css_default.userRow,
804
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
805
+ className: sidechat_module_css_default.userBubble,
806
+ children: message.text
807
+ })
808
+ });
809
+ case "assistant": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
810
+ className: sidechat_module_css_default.assistantRow,
811
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
812
+ className: sidechat_module_css_default.assistantBody,
813
+ children: [
814
+ message.reasoning !== void 0 && message.reasoning !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
815
+ className: sidechat_module_css_default.reasoning,
816
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("thinking") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
817
+ className: sidechat_module_css_default.reasoningBody,
818
+ children: message.reasoning
819
+ })]
820
+ }),
821
+ message.text !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
822
+ text: message.text,
823
+ streaming: message.streaming,
824
+ codeLabels: codeLabels()
825
+ }) : message.streaming === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
826
+ className: sidechat_module_css_default.streamingHint,
827
+ children: t("writing")
828
+ }),
829
+ message.interrupted === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
830
+ className: sidechat_module_css_default.noticeRow,
831
+ children: t("stopped")
832
+ })
833
+ ]
834
+ })
835
+ });
836
+ case "tool": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
837
+ className: sidechat_module_css_default.toolCard,
838
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
839
+ className: sidechat_module_css_default.toolHead,
840
+ children: [
841
+ t("toolLabel"),
842
+ " · ",
843
+ message.toolName,
844
+ message.isError === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
845
+ className: sidechat_module_css_default.toolError,
846
+ children: t("failed")
847
+ }),
848
+ message.streaming === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
849
+ className: sidechat_module_css_default.toolRunning,
850
+ children: t("running")
851
+ })
852
+ ]
853
+ }), message.text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
854
+ className: sidechat_module_css_default.toolBody,
855
+ children: message.text
856
+ })]
857
+ });
858
+ case "error": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
859
+ className: sidechat_module_css_default.errorRow,
860
+ children: message.text
861
+ });
862
+ case "notice": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
863
+ className: sidechat_module_css_default.noticeRow,
864
+ children: message.text
865
+ });
866
+ }
867
+ }
868
+ /** 底部 composer:自绘输入框;模型标签显示子会话真实当前模型(fork 时同步主会话选择)。 */
869
+ function ComposerBar(props) {
870
+ useLocaleTick();
871
+ const { session, composer, running, visible } = props;
872
+ const inputRef = (0, react.useRef)(null);
873
+ (0, react.useEffect)(() => {
874
+ if (visible) inputRef.current?.focus();
875
+ }, [visible]);
876
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
877
+ className: sidechat_module_css_default.composer,
878
+ children: [
879
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
880
+ ref: inputRef,
881
+ className: sidechat_module_css_default.input,
882
+ placeholder: t("inputPlaceholder"),
883
+ value: composer.draft,
884
+ onChange: (event) => {
885
+ composer.setDraft(event.target.value);
886
+ },
887
+ onKeyDown: (event) => {
888
+ if (event.key !== "Enter" || event.shiftKey) return;
889
+ if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
890
+ event.preventDefault();
891
+ composer.submit();
892
+ }
893
+ }),
894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
895
+ className: sidechat_module_css_default.composerFoot,
896
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
897
+ className: sidechat_module_css_default.modelLabel,
898
+ children: t("modelLabel", { name: props.modelName ?? t("modelFollowsMain") })
899
+ }), running ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
900
+ type: "button",
901
+ className: sidechat_module_css_default.stopButton,
902
+ title: t("stopReplyTitle"),
903
+ onClick: () => {
904
+ session?.cancel().catch(() => {});
905
+ },
906
+ children: [
907
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFill16, { size: 14 }),
908
+ " ",
909
+ t("stopReply")
910
+ ]
911
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
912
+ type: "button",
913
+ className: sidechat_module_css_default.sendButton,
914
+ disabled: composer.draft.trim() === "",
915
+ onClick: () => {
916
+ composer.submit();
917
+ },
918
+ children: [
919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSendOutline16, { size: 14 }),
920
+ " ",
921
+ t("send")
922
+ ]
923
+ })]
924
+ }),
925
+ composer.sendError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
926
+ className: sidechat_module_css_default.errorRow,
927
+ children: composer.sendError
928
+ })
929
+ ]
930
+ });
931
+ }
932
+ //#endregion
933
+ //#region src/client/sidechat/slash.ts
934
+ /** 注册 /side 命令;不可行时静默降级(返回 undefined)。 */
935
+ function registerSideCommand(ctx) {
936
+ let commandUi;
937
+ try {
938
+ commandUi = ctx.get("commandUi");
939
+ } catch {
940
+ return;
941
+ }
942
+ if (commandUi === void 0 || typeof commandUi.register !== "function") return;
943
+ try {
944
+ ctx.effect(() => commandUi.register({
945
+ name: "side",
946
+ description: t("cmdDesc"),
947
+ available: (session) => canForkFrom(ctx, session.sessionId),
948
+ ui: {
949
+ kind: "popupSelect",
950
+ options: (session) => {
951
+ const options = [{
952
+ id: "new",
953
+ label: t("cmdNew"),
954
+ detail: t("cmdNewDetail")
955
+ }];
956
+ const snapshot = ctx.betterSidebar.getSnapshot();
957
+ if (snapshot.sessionId === session.sessionId && snapshot.state !== void 0) for (const tab of collectSideTabs(snapshot.state)) options.push({
958
+ id: `focus:${tab.id}`,
959
+ label: t("cmdFocus", { title: tab.title }),
960
+ detail: t("cmdFocusDetail")
961
+ });
962
+ return Promise.resolve(options);
963
+ },
964
+ onSelect: (option, session) => {
965
+ if (option.id === "new") {
966
+ openOrFocusSideChat(ctx, session.sessionId);
967
+ return;
968
+ }
969
+ if (option.id.startsWith("focus:")) ctx.betterSidebar.activateTab(option.id.slice(6), { sessionId: session.sessionId });
970
+ }
971
+ }
972
+ }), "dsh-sidenote: /side command");
973
+ } catch (error) {
974
+ console.warn("[dsh-sidenote] /side 命令注册失败(Tab 入口不受影响):", error);
975
+ }
976
+ }
977
+ //#endregion
978
+ //#region src/client/sidechat/index.tsx
979
+ /**
980
+ * Workitem 01 — 侧边聊天面板。
981
+ *
982
+ * 注册「侧边聊天」Tab 类型(dsh-sidenote:side,多实例一等公民):
983
+ * - + 菜单可见(order 60,terminal=40 / browser=50 之后);菜单行标题
984
+ * 「侧边聊天」,铸造出的 Tab 标题为「侧边」/「侧边 N」(createTab 按
985
+ * 当前并存数编号 —— 带 createTab 的 descriptor 会被 openTab 忽略
986
+ * seed.title,编号必须在铸造处完成);
987
+ * - createTab 铸 `side:<uuid>`(crypto.randomUUID())多实例 id;
988
+ * - blank 着陆页会话没有已完成 turn、fork 必败 → available 禁用 +
989
+ * 面板内错误态双保险;
990
+ * - Tab × 关闭即从界面消失(better-sidebar 关 tab 即出布局,meta 随之
991
+ * 消失;jsonl 留盘是设计意图,不做删除)。
992
+ *
993
+ * 另安装:annotate 桥(sideChatBridge.current,WI-03 联动缝)与 /side
994
+ * 斜杠命令(spike,popupSelect 形态,不可行时降级为只有 Tab 入口)。
995
+ */
996
+ function registerSideChat(ctx) {
997
+ ctx.effect(() => ctx.betterSidebar.registerTab({
998
+ id: SIDE_TAB_TYPE,
999
+ title: () => t("menuTitle"),
1000
+ icon: (size) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutline16, { size }),
1001
+ order: 60,
1002
+ available: (availableCtx, scope) => canForkFrom(availableCtx, scope.sessionId),
1003
+ createTab: (state) => ({ tab: {
1004
+ id: mintSideTabId(),
1005
+ type: SIDE_TAB_TYPE,
1006
+ title: sideTabTitle(collectSideTabs(state).map((tab) => tab.title))
1007
+ } }),
1008
+ component: (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SideChatPanel, { ...props })
1009
+ }), "dsh-sidenote: side chat tab");
1010
+ ctx.effect(() => {
1011
+ const impl = { askInSideChat: (sessionId, draftText) => openOrFocusSideChat(ctx, sessionId, draftText) };
1012
+ sideChatBridge.current = impl;
1013
+ return () => {
1014
+ if (sideChatBridge.current === impl) sideChatBridge.current = null;
1015
+ };
1016
+ }, "dsh-sidenote: annotate bridge");
1017
+ registerSideCommand(ctx);
1018
+ }
1019
+ //#endregion
1020
+ //#region src/client/annotate/model.ts
1021
+ function createAnnotationStore(now = () => Date.now()) {
1022
+ let annotations = [];
1023
+ let nextId = 1;
1024
+ let version = 0;
1025
+ /** Per-session next display number. */
1026
+ const counters = /* @__PURE__ */ new Map();
1027
+ const listeners = /* @__PURE__ */ new Set();
1028
+ const notify = () => {
1029
+ version += 1;
1030
+ for (const fn of [...listeners]) fn();
1031
+ };
1032
+ const replace = (id, patch) => {
1033
+ annotations = annotations.map((a) => a.id === id ? {
1034
+ ...a,
1035
+ ...patch
1036
+ } : a);
1037
+ notify();
1038
+ };
1039
+ return {
1040
+ getSnapshot: () => version,
1041
+ subscribe(fn) {
1042
+ listeners.add(fn);
1043
+ return () => {
1044
+ listeners.delete(fn);
1045
+ };
1046
+ },
1047
+ add(draft) {
1048
+ const number = (counters.get(draft.sessionId) ?? 0) + 1;
1049
+ counters.set(draft.sessionId, number);
1050
+ const annotation = {
1051
+ id: nextId,
1052
+ number,
1053
+ sessionId: draft.sessionId,
1054
+ anchorKey: draft.anchorKey,
1055
+ text: draft.text,
1056
+ anchorText: draft.anchorText,
1057
+ occurrence: draft.occurrence,
1058
+ note: draft.note,
1059
+ state: "active",
1060
+ createdAt: now()
1061
+ };
1062
+ nextId += 1;
1063
+ annotations = [...annotations, annotation];
1064
+ notify();
1065
+ return annotation;
1066
+ },
1067
+ setNote(id, note) {
1068
+ if (!annotations.some((a) => a.id === id)) return;
1069
+ replace(id, { note });
1070
+ },
1071
+ remove(id) {
1072
+ if (!annotations.some((a) => a.id === id)) return;
1073
+ annotations = annotations.filter((a) => a.id !== id);
1074
+ notify();
1075
+ },
1076
+ markSessionSent(sessionId) {
1077
+ if (!annotations.some((a) => a.sessionId === sessionId && a.state === "active")) return;
1078
+ annotations = annotations.map((a) => a.sessionId === sessionId && a.state === "active" ? {
1079
+ ...a,
1080
+ state: "sent"
1081
+ } : a);
1082
+ notify();
1083
+ },
1084
+ get(id) {
1085
+ return annotations.find((a) => a.id === id);
1086
+ },
1087
+ list(sessionId) {
1088
+ return annotations.filter((a) => a.sessionId === sessionId);
1089
+ },
1090
+ listActive(sessionId) {
1091
+ return annotations.filter((a) => a.sessionId === sessionId && a.state === "active");
1092
+ },
1093
+ countActive(sessionId) {
1094
+ return annotations.reduce((n, a) => n + (a.sessionId === sessionId && a.state === "active" ? 1 : 0), 0);
1095
+ },
1096
+ sessions() {
1097
+ return [...new Set(annotations.map((a) => a.sessionId))];
1098
+ }
1099
+ };
1100
+ }
1101
+ /** Bound a quote to {@link SELECTION_LIMIT} characters, marking truncation. */
1102
+ function truncateQuote(text, limit = 500) {
1103
+ if (text.length <= limit) return text;
1104
+ return `${text.slice(0, limit)}…`;
1105
+ }
1106
+ /** Quote one (possibly multi-line) text as `> `-prefixed lines. */
1107
+ function quoteLines(text) {
1108
+ return text.split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n");
1109
+ }
1110
+ /** One annotation's block: quoted original plus the note line. */
1111
+ function formatQuoteItem(item) {
1112
+ const noteLine = item.note.trim() === "" ? t("noNote") : t("noteLine", { note: item.note });
1113
+ return `${quoteLines(item.text)}\n${noteLine}`;
1114
+ }
1115
+ /** The full context block for every active annotation, in creation order. */
1116
+ function buildQuoteBlock(items) {
1117
+ return items.map(formatQuoteItem).join("\n\n");
1118
+ }
1119
+ /** The 「在侧边聊天中提问」 seed: quote + optional note(与主对话注释同构). */
1120
+ function buildSideChatQuote(text, note = "") {
1121
+ return formatQuoteItem({
1122
+ text,
1123
+ note
1124
+ });
1125
+ }
1126
+ /**
1127
+ * Recompute a session composer draft from its managed head.
1128
+ *
1129
+ * The managed head (quote block + one blank line) is maintained at the very
1130
+ * top of the draft while annotations are active; the user's own text follows
1131
+ * it untouched. `lastHead` is the head we last wrote: when the draft still
1132
+ * starts with it we strip exactly that span; otherwise the user meddled with
1133
+ * the head region and the whole draft is treated as user text (the fresh head
1134
+ * is simply re-prepended — a documented, non-crashing degradation).
1135
+ */
1136
+ function nextManagedDraft(draft, lastHead, block) {
1137
+ const userText = lastHead !== "" && draft.startsWith(lastHead) ? draft.slice(lastHead.length) : draft;
1138
+ const head = block === "" ? "" : `${block}\n\n`;
1139
+ return {
1140
+ head,
1141
+ draft: head + userText
1142
+ };
1143
+ }
1144
+ /**
1145
+ * The send-edge predicate: the composer draft flipping non-empty → empty
1146
+ * while annotations were attached is the reliable「发送后」signal (submit
1147
+ * clears the draft as a commit; queueing does too). Whitespace-only drafts
1148
+ * count as empty.
1149
+ */
1150
+ function isSendEdge(previousDraft, nextDraft) {
1151
+ return previousDraft.trim() !== "" && nextDraft.trim() === "";
1152
+ }
1153
+ //#endregion
1154
+ //#region src/client/annotate/selection.ts
1155
+ /**
1156
+ * Selection capture layer (Workitem 02): document-level listeners validate
1157
+ * the live selection and publish the latest valid one to the floating
1158
+ * toolbar through a small external-store controller. Mirrors the
1159
+ * production-proven dsh-sidebar-qa contract, narrowed to assistant messages:
1160
+ *
1161
+ * - both selection endpoints must sit in the SAME `[data-chat-flow-kind=
1162
+ * "assistant-step"]` message node (跨消息划选不弹);
1163
+ * - `[data-streaming]` messages (still streaming) are excluded;
1164
+ * - `[data-dsh-better-sidebar]` and our own `[data-dsh-sidenote]` DOM are
1165
+ * excluded (与侧边栏 viewer 浮层互斥);
1166
+ * - the text must be non-blank; over-length selections are admitted with the
1167
+ * quote truncated (见 format.ts), never rejected.
1168
+ *
1169
+ * These are DSH-internal DOM attributes rather than a public contract — an
1170
+ * upstream redesign silently degrades the feature (no popup), never crashes.
1171
+ */
1172
+ /** The assistant message node kind (v1 实测纠正值 — 勿引旧值 `assistant`). */
1173
+ const ASSISTANT_KIND = "assistant-step";
1174
+ /** Selector roots our UI must never react to (sidebar viewer + our own root). */
1175
+ const EXCLUDED_ROOTS = "[data-dsh-better-sidebar], [data-dsh-sidenote]";
1176
+ /** Pure validation of one candidate selection (unit-tested without DOM). */
1177
+ function isEligibleSelection(input) {
1178
+ return !input.blank && input.sameMessage && input.kind === "assistant-step" && !input.streaming && !input.excluded && input.hasSession;
1179
+ }
1180
+ /** Resolve the assistant message element owning a DOM node (text nodes → parent). */
1181
+ function messageOf(node) {
1182
+ const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
1183
+ if (element === null || typeof element.closest !== "function") return null;
1184
+ return element.closest(`[data-chat-flow-kind="${ASSISTANT_KIND}"]`);
1185
+ }
1186
+ /** Whether the message is still streaming (unfinished). */
1187
+ function isStreaming(message) {
1188
+ return message.hasAttribute("data-streaming") || message.querySelector("[data-streaming]") !== null;
1189
+ }
1190
+ /**
1191
+ * The 0-based ordinal of the selection's text among its occurrences in the
1192
+ * anchor element — the disambiguator for repeated phrases (re-anchor key).
1193
+ */
1194
+ function occurrenceOf(anchor, range, text) {
1195
+ try {
1196
+ const pre = document.createRange();
1197
+ pre.selectNodeContents(anchor);
1198
+ pre.setEnd(range.startContainer, range.startOffset);
1199
+ const before = pre.toString();
1200
+ let occurrence = 0;
1201
+ let at = before.indexOf(text);
1202
+ while (at !== -1) {
1203
+ occurrence += 1;
1204
+ at = before.indexOf(text, at + text.length);
1205
+ }
1206
+ return occurrence;
1207
+ } catch {
1208
+ return 0;
1209
+ }
1210
+ }
1211
+ /** Capture and validate the current window selection, or null when invalid. */
1212
+ function captureSelection(currentSessionId) {
1213
+ const sel = window.getSelection();
1214
+ if (sel === null || sel.isCollapsed || sel.rangeCount === 0) return null;
1215
+ const anchorText = sel.toString();
1216
+ const range = sel.getRangeAt(0);
1217
+ const startMessage = messageOf(range.startContainer);
1218
+ const endMessage = messageOf(range.endContainer);
1219
+ const message = startMessage;
1220
+ const anchor = message?.closest("[data-chat-anchor-key]") ?? null;
1221
+ if (!isEligibleSelection({
1222
+ blank: anchorText.trim() === "",
1223
+ sameMessage: message !== null && message === endMessage,
1224
+ kind: message?.dataset.chatFlowKind ?? "",
1225
+ streaming: message !== null && isStreaming(message),
1226
+ excluded: anchor !== null ? anchor.closest(EXCLUDED_ROOTS) !== null : message?.closest(EXCLUDED_ROOTS) !== null,
1227
+ hasSession: currentSessionId !== ""
1228
+ }) || message === null) return null;
1229
+ const rect = range.getBoundingClientRect();
1230
+ return {
1231
+ text: truncateQuote(anchorText),
1232
+ anchorText,
1233
+ anchorKey: anchor?.dataset.chatAnchorKey,
1234
+ occurrence: anchor === null ? 0 : occurrenceOf(anchor, range, anchorText),
1235
+ rect: {
1236
+ left: rect.left,
1237
+ top: rect.top,
1238
+ width: rect.width
1239
+ },
1240
+ range: range.cloneRange(),
1241
+ sessionId: currentSessionId
1242
+ };
1243
+ }
1244
+ /** Debounce for selectionchange (ms). */
1245
+ const DEBOUNCE_MS = 200;
1246
+ /** Create one selection controller (call once per plugin activation). */
1247
+ function createSelectionController(getSessionId) {
1248
+ let selection = null;
1249
+ const listeners = /* @__PURE__ */ new Set();
1250
+ let debounceTimer;
1251
+ let state = { selection: null };
1252
+ const notify = () => {
1253
+ state = { selection };
1254
+ for (const fn of [...listeners]) fn();
1255
+ };
1256
+ const recompute = () => {
1257
+ let next = null;
1258
+ try {
1259
+ next = captureSelection(getSessionId());
1260
+ } catch {
1261
+ next = null;
1262
+ }
1263
+ if (!(selection === null !== (next === null) || selection !== null && next !== null && (selection.anchorText !== next.anchorText || selection.anchorKey !== next.anchorKey || selection.sessionId !== next.sessionId || selection.rect.top !== next.rect.top || selection.rect.left !== next.rect.left || selection.rect.width !== next.rect.width))) return;
1264
+ selection = next;
1265
+ notify();
1266
+ };
1267
+ const onSelectionChange = () => {
1268
+ if (debounceTimer !== void 0) window.clearTimeout(debounceTimer);
1269
+ debounceTimer = window.setTimeout(recompute, DEBOUNCE_MS);
1270
+ };
1271
+ document.addEventListener("selectionchange", onSelectionChange);
1272
+ document.addEventListener("mouseup", recompute);
1273
+ document.addEventListener("keyup", onSelectionChange);
1274
+ return {
1275
+ getSnapshot: () => state,
1276
+ subscribe(fn) {
1277
+ listeners.add(fn);
1278
+ return () => {
1279
+ listeners.delete(fn);
1280
+ };
1281
+ },
1282
+ clear() {
1283
+ selection = null;
1284
+ notify();
1285
+ },
1286
+ dispose() {
1287
+ document.removeEventListener("selectionchange", onSelectionChange);
1288
+ document.removeEventListener("mouseup", recompute);
1289
+ document.removeEventListener("keyup", onSelectionChange);
1290
+ if (debounceTimer !== void 0) window.clearTimeout(debounceTimer);
1291
+ }
1292
+ };
1293
+ }
1294
+ //#endregion
1295
+ //#region src/client/annotate/anchor.ts
1296
+ /** Find the message element for an anchor key (connected DOM only). */
1297
+ function findAnchorElement(anchorKey) {
1298
+ try {
1299
+ const el = document.querySelector(`[data-chat-anchor-key="${CSS.escape(anchorKey)}"]`);
1300
+ return el !== null && el.isConnected ? el : null;
1301
+ } catch {
1302
+ return null;
1303
+ }
1304
+ }
1305
+ /**
1306
+ * Rebuild a range covering the `occurrence`-th appearance of `text` inside
1307
+ * `root`'s text nodes. Falls back to the LAST occurrence when the ordinal is
1308
+ * out of range(消息内容变短时,最后一次出现更可能是原选区).
1309
+ */
1310
+ function rangeOfOccurrence(root, text, occurrence) {
1311
+ if (text === "") return null;
1312
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1313
+ const haystack = root.textContent ?? "";
1314
+ let start = -1;
1315
+ let at = haystack.indexOf(text);
1316
+ let seen = 0;
1317
+ while (at !== -1) {
1318
+ if (seen === occurrence) {
1319
+ start = at;
1320
+ break;
1321
+ }
1322
+ seen += 1;
1323
+ at = haystack.indexOf(text, at + text.length);
1324
+ }
1325
+ if (start === -1 && occurrence > 0) start = haystack.lastIndexOf(text);
1326
+ if (start === -1) return null;
1327
+ const end = start + text.length;
1328
+ let offset = 0;
1329
+ let startNode = null;
1330
+ let startOffset = 0;
1331
+ let endNode = null;
1332
+ let endOffset = 0;
1333
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
1334
+ const length = (node.textContent ?? "").length;
1335
+ const next = offset + length;
1336
+ if (startNode === null && start < next) {
1337
+ startNode = node;
1338
+ startOffset = start - offset;
1339
+ }
1340
+ if (startNode !== null && end <= next) {
1341
+ endNode = node;
1342
+ endOffset = end - offset;
1343
+ break;
1344
+ }
1345
+ offset = next;
1346
+ }
1347
+ if (startNode === null || endNode === null) return null;
1348
+ const range = document.createRange();
1349
+ range.setStart(startNode, startOffset);
1350
+ range.setEnd(endNode, endOffset);
1351
+ return range;
1352
+ }
1353
+ /**
1354
+ * Resolve a live range for an annotation: the cached range while its nodes
1355
+ * are still connected, otherwise a re-anchored one (cached on success).
1356
+ * Returns null when the anchor is gone — callers skip rendering, never throw.
1357
+ */
1358
+ function resolveRange(annotation, cache) {
1359
+ try {
1360
+ const cached = cache.get(annotation.id);
1361
+ if (cached !== void 0 && cached.startContainer.isConnected) return cached;
1362
+ cache.delete(annotation.id);
1363
+ if (annotation.anchorKey === void 0 || annotation.anchorText === "") return null;
1364
+ const anchor = findAnchorElement(annotation.anchorKey);
1365
+ if (anchor === null) return null;
1366
+ const range = rangeOfOccurrence(anchor, annotation.anchorText, annotation.occurrence);
1367
+ if (range !== null) cache.set(annotation.id, range);
1368
+ return range;
1369
+ } catch {
1370
+ return null;
1371
+ }
1372
+ }
1373
+ /**
1374
+ * The badge anchor point of a range: 选区矩形右缘 (max right edge across
1375
+ * client rects — the message column's right edge for full-line selections,
1376
+ * the selection's text end for inline ones) at 选区首行高度 (first rect's
1377
+ * vertical center). Null when the range lays out to nothing.
1378
+ */
1379
+ function badgeAnchorOf(range) {
1380
+ try {
1381
+ const rects = range.getClientRects();
1382
+ if (rects.length === 0) return null;
1383
+ let right = -Infinity;
1384
+ for (const rect of rects) right = Math.max(right, rect.right);
1385
+ const first = rects[0];
1386
+ if (first === void 0 || !Number.isFinite(right)) return null;
1387
+ return {
1388
+ right,
1389
+ centerY: first.top + first.height / 2
1390
+ };
1391
+ } catch {
1392
+ return null;
1393
+ }
1394
+ }
1395
+ /** Client rects of a range for the active-highlight overlay (empty on failure). */
1396
+ function highlightRectsOf(range) {
1397
+ try {
1398
+ const out = [];
1399
+ for (const rect of range.getClientRects()) {
1400
+ if (rect.width <= 0 || rect.height <= 0) continue;
1401
+ out.push({
1402
+ left: rect.left,
1403
+ top: rect.top,
1404
+ width: rect.width,
1405
+ height: rect.height
1406
+ });
1407
+ }
1408
+ return out;
1409
+ } catch {
1410
+ return [];
1411
+ }
1412
+ }
1413
+ /**
1414
+ * 角标落点错开:与已放置角标(|Δx|、|Δy| 均小于碰撞半径)冲突时按
1415
+ * BADGE_SPREAD_STEP 逐级下移——同一选区/相邻行的多个注释角标不再叠罗汉
1416
+ * (叠放时上层角标拦截下层角标的点击)。
1417
+ */
1418
+ function spreadBadgePoint(point, placed) {
1419
+ let out = point;
1420
+ for (let guard = 0; guard < 50; guard++) {
1421
+ if (!placed.some((p) => Math.abs(p.x - out.x) < 18 && Math.abs(p.y - out.y) < 18)) return out;
1422
+ out = {
1423
+ x: out.x,
1424
+ y: out.y + 24
1425
+ };
1426
+ }
1427
+ return out;
1428
+ }
1429
+ //#endregion
1430
+ //#region \0dsh-css:/Users/gaoyixuan3/gyx_personal_files/dsh_project/dsh-sidenote/src/client/annotate/annotate.module.css.mjs
1431
+ const css = "._6Uc2-G_toolbar{z-index:50;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#00000014);user-select:none;border-radius:10px;align-items:stretch;display:flex;position:fixed;overflow:hidden;transform:translate(-50%,-100%);box-shadow:0 4px 16px #0000001f}._6Uc2-G_toolbarButton{appearance:none;color:var(--dsw-alias-label-primary,#1f2329);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:7px 12px;font-size:13px;line-height:20px}._6Uc2-G_toolbarButton+._6Uc2-G_toolbarButton{border-left:1px solid var(--dsw-alias-border-l1,#00000014)}._6Uc2-G_toolbarButton:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d)}._6Uc2-G_badge{z-index:50;color:#fff;text-align:center;cursor:pointer;user-select:none;background:#2f88ff;border:none;border-radius:999px;width:18px;height:18px;padding:0;font-size:11px;font-weight:600;line-height:18px;position:fixed;transform:translateY(-50%);box-shadow:0 1px 4px #2f88ff73}._6Uc2-G_badge:hover{background:#1f78f0}._6Uc2-G_highlight{z-index:49;pointer-events:none;background:#2f88ff38;border-radius:2px;position:fixed}._6Uc2-G_editorNew{z-index:50;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#00000014);border-radius:999px;align-items:center;gap:8px;padding:6px 8px 6px 12px;display:flex;position:fixed;box-shadow:0 6px 24px #00000024}._6Uc2-G_editorEdit{z-index:50;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#00000014);border-radius:14px;flex-direction:column;display:flex;position:fixed;overflow:hidden;box-shadow:0 8px 28px #00000029}._6Uc2-G_editorInput{min-width:0;color:var(--dsw-alias-label-primary,#1f2329);background:0 0;border:none;outline:none;flex:auto;font-size:13px;line-height:22px}._6Uc2-G_editorTextarea{resize:none;color:var(--dsw-alias-label-primary,#1f2329);background:0 0;border:none;outline:none;padding:10px 12px 4px;font-family:inherit;font-size:13px;line-height:20px}._6Uc2-G_editorInput::placeholder,._6Uc2-G_editorTextarea::placeholder{color:var(--dsw-alias-label-caption,#8f959e)}._6Uc2-G_confirmButton{color:#fff;cursor:pointer;background:#1f2329;border:none;border-radius:999px;flex:none;place-items:center;width:26px;height:26px;padding:0;display:grid}._6Uc2-G_confirmButton:hover{background:#000}._6Uc2-G_editorFooter{align-items:center;gap:8px;padding:6px 10px 8px;display:flex}._6Uc2-G_editorSpacer{flex:auto}._6Uc2-G_deleteButton{width:26px;height:26px;color:var(--dsw-alias-label-tertiary,#8f959e);cursor:pointer;background:0 0;border:none;border-radius:999px;place-items:center;padding:0;display:grid}._6Uc2-G_deleteButton:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-state-error-primary,#d53f3f)}._6Uc2-G_cancelButton,._6Uc2-G_saveButton{cursor:pointer;border:none;border-radius:999px;padding:4px 14px;font-size:13px;line-height:20px}._6Uc2-G_cancelButton{background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-primary,#1f2329)}._6Uc2-G_saveButton{color:#fff;background:#1f2329}._6Uc2-G_saveButton:hover{background:#000}._6Uc2-G_chipWrap{flex-direction:column;align-items:flex-start;gap:4px;display:flex}._6Uc2-G_chip{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2329);cursor:pointer;user-select:none;border-radius:999px;align-items:center;gap:5px;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}._6Uc2-G_chip:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d)}._6Uc2-G_chipPanel{border:1px solid var(--dsw-alias-border-l1,#00000014);background:var(--dsw-alias-bg-layer-1,#fff);border-radius:12px;min-width:220px;max-width:420px;margin:0;padding:4px;list-style:none;box-shadow:0 6px 20px #0000001a}._6Uc2-G_chipRow{border-radius:8px;align-items:center;gap:8px;padding:4px 6px;display:flex}._6Uc2-G_chipRow:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000a)}._6Uc2-G_chipNumber{color:#fff;text-align:center;background:#2f88ff;border-radius:999px;flex:none;width:16px;height:16px;font-size:10px;font-weight:600;line-height:16px}._6Uc2-G_chipText{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary,#1f2329);flex:auto;font-size:12px;overflow:hidden}._6Uc2-G_chipNote{color:var(--dsw-alias-label-tertiary,#8f959e)}._6Uc2-G_chipRemove{width:20px;height:20px;color:var(--dsw-alias-label-tertiary,#8f959e);cursor:pointer;background:0 0;border:none;border-radius:999px;flex:none;place-items:center;padding:0;display:grid}._6Uc2-G_chipRemove:hover{background:var(--dsw-alias-interactive-bg-hover,#00000014);color:var(--dsw-alias-state-error-primary,#d53f3f)}._6Uc2-G_editorError{color:#e5484d;white-space:nowrap;font-size:12px;position:absolute;top:calc(100% + 4px);left:12px}";
1432
+ const tagId = "dsh-sidenote/annotate.module.css";
1433
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1434
+ const tag = document.createElement("style");
1435
+ tag.dataset.plugin = "dsh-sidenote";
1436
+ tag.dataset.pluginCss = tagId;
1437
+ tag.textContent = css;
1438
+ document.head.appendChild(tag);
1439
+ }
1440
+ var annotate_module_css_default = {
1441
+ "chipNote": "_6Uc2-G_chipNote",
1442
+ "chipPanel": "_6Uc2-G_chipPanel",
1443
+ "chipRemove": "_6Uc2-G_chipRemove",
1444
+ "highlight": "_6Uc2-G_highlight",
1445
+ "cancelButton": "_6Uc2-G_cancelButton",
1446
+ "editorInput": "_6Uc2-G_editorInput",
1447
+ "toolbar": "_6Uc2-G_toolbar",
1448
+ "toolbarButton": "_6Uc2-G_toolbarButton",
1449
+ "saveButton": "_6Uc2-G_saveButton",
1450
+ "chipRow": "_6Uc2-G_chipRow",
1451
+ "confirmButton": "_6Uc2-G_confirmButton",
1452
+ "editorFooter": "_6Uc2-G_editorFooter",
1453
+ "chipText": "_6Uc2-G_chipText",
1454
+ "editorNew": "_6Uc2-G_editorNew",
1455
+ "editorSpacer": "_6Uc2-G_editorSpacer",
1456
+ "editorEdit": "_6Uc2-G_editorEdit",
1457
+ "chipWrap": "_6Uc2-G_chipWrap",
1458
+ "badge": "_6Uc2-G_badge",
1459
+ "editorTextarea": "_6Uc2-G_editorTextarea",
1460
+ "chip": "_6Uc2-G_chip",
1461
+ "editorError": "_6Uc2-G_editorError",
1462
+ "deleteButton": "_6Uc2-G_deleteButton",
1463
+ "chipNumber": "_6Uc2-G_chipNumber"
1464
+ };
1465
+ //#endregion
1466
+ //#region src/client/annotate/overlay.tsx
1467
+ /**
1468
+ * The annotation overlay (Workitem 02): an independent React root appended to
1469
+ * `document.body` (marked `data-dsh-sidenote` so the selection listener
1470
+ * excludes it) rendering three fixed-position layers over the conversation:
1471
+ *
1472
+ * - the selection toolbar (Add to conversation / Ask in side chat),
1473
+ * - the numbered badge layer (锚定在选区矩形右缘、选区首行高度) plus the
1474
+ * 激活态高亮 (editor open only — 关闭后高亮消退只留角标),
1475
+ * - the annotation editor popover (新建态 ✓ / 重开态 🗑 取消 保存).
1476
+ *
1477
+ * Every layer is pointer-safe (toolbar buttons preventDefault their mousedown
1478
+ * so the selection survives until the click commits) and failure-safe (an
1479
+ * error boundary around the tree; layout math is wrapped in anchor.ts).
1480
+ */
1481
+ /** Crash guard: a render failure must never take the host page down. */
1482
+ var AnnotateErrorBoundary = class extends react.Component {
1483
+ state = { failed: false };
1484
+ static getDerivedStateFromError() {
1485
+ return { failed: true };
1486
+ }
1487
+ componentDidCatch(error) {
1488
+ console.error("[dsh-sidenote] annotate overlay crashed:", error);
1489
+ }
1490
+ render() {
1491
+ return this.state.failed ? null : this.props.children;
1492
+ }
1493
+ };
1494
+ /** Badge anchor of a freshly captured selection (editor placement on create). */
1495
+ function selectionBadgePoint(snapshot) {
1496
+ const anchor = badgeAnchorOf(snapshot.range);
1497
+ if (anchor === null) {
1498
+ const rect = snapshot.range.getBoundingClientRect();
1499
+ return {
1500
+ x: rect.right,
1501
+ y: rect.top + 10
1502
+ };
1503
+ }
1504
+ return {
1505
+ x: anchor.right,
1506
+ y: anchor.centerY
1507
+ };
1508
+ }
1509
+ function AnnotateOverlay(props) {
1510
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotateErrorBoundary, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotateOverlayInner, { ...props }) });
1511
+ }
1512
+ function AnnotateOverlayInner({ ctx, store, controller }) {
1513
+ useLocaleTick();
1514
+ const selectionState = (0, react.useSyncExternalStore)((0, react.useCallback)((cb) => controller.subscribe(cb), [controller]), () => controller.getSnapshot());
1515
+ (0, react.useSyncExternalStore)((0, react.useCallback)((cb) => store.subscribe(cb), [store]), () => store.getSnapshot());
1516
+ const currentSessionId = (0, react.useSyncExternalStore)((0, react.useCallback)((cb) => ctx.sessions.list.subscribe(cb), [ctx]), () => ctx.sessions.list.getSnapshot()).current ?? "";
1517
+ const [editor, setEditor] = (0, react.useState)(null);
1518
+ const [sideDraft, setSideDraft] = (0, react.useState)(null);
1519
+ const [, setGeometryTick] = (0, react.useState)(0);
1520
+ const rangeCache = (0, react.useRef)(/* @__PURE__ */ new Map());
1521
+ (0, react.useEffect)(() => {
1522
+ let raf = 0;
1523
+ let debounce = 0;
1524
+ const bumpNow = () => {
1525
+ if (raf !== 0) return;
1526
+ raf = window.requestAnimationFrame(() => {
1527
+ raf = 0;
1528
+ setGeometryTick((tick) => tick + 1);
1529
+ });
1530
+ };
1531
+ const bumpDebounced = () => {
1532
+ if (debounce !== 0) window.clearTimeout(debounce);
1533
+ debounce = window.setTimeout(() => {
1534
+ debounce = 0;
1535
+ bumpNow();
1536
+ }, 100);
1537
+ };
1538
+ const observer = new MutationObserver(bumpDebounced);
1539
+ try {
1540
+ observer.observe(document.body, {
1541
+ childList: true,
1542
+ subtree: true,
1543
+ characterData: true
1544
+ });
1545
+ } catch {}
1546
+ window.addEventListener("resize", bumpNow);
1547
+ document.addEventListener("scroll", bumpNow, true);
1548
+ return () => {
1549
+ observer.disconnect();
1550
+ window.removeEventListener("resize", bumpNow);
1551
+ document.removeEventListener("scroll", bumpNow, true);
1552
+ if (raf !== 0) window.cancelAnimationFrame(raf);
1553
+ if (debounce !== 0) window.clearTimeout(debounce);
1554
+ };
1555
+ }, []);
1556
+ const selection = selectionState.selection;
1557
+ const addToConversation = (snapshot) => {
1558
+ const anchor = selectionBadgePoint(snapshot);
1559
+ const annotation = store.add({
1560
+ sessionId: snapshot.sessionId,
1561
+ anchorKey: snapshot.anchorKey,
1562
+ text: snapshot.text,
1563
+ anchorText: snapshot.anchorText,
1564
+ occurrence: snapshot.occurrence,
1565
+ note: ""
1566
+ });
1567
+ const cached = snapshot.range.cloneRange();
1568
+ rangeCache.current.set(annotation.id, cached);
1569
+ setEditor({
1570
+ annotationId: annotation.id,
1571
+ mode: "new",
1572
+ x: anchor.x,
1573
+ y: anchor.y
1574
+ });
1575
+ controller.clear();
1576
+ window.getSelection()?.removeAllRanges();
1577
+ };
1578
+ const askInSideChat = (snapshot) => {
1579
+ const anchor = selectionBadgePoint(snapshot);
1580
+ setSideDraft({
1581
+ snapshot,
1582
+ x: anchor.x,
1583
+ y: anchor.y
1584
+ });
1585
+ controller.clear();
1586
+ window.getSelection()?.removeAllRanges();
1587
+ };
1588
+ const commitSideDraft = (note) => {
1589
+ if (sideDraft === null) return false;
1590
+ const ok = sideChatBridge.current?.askInSideChat(sideDraft.snapshot.sessionId, buildSideChatQuote(sideDraft.snapshot.text, note)) === true;
1591
+ if (ok) setSideDraft(null);
1592
+ return ok;
1593
+ };
1594
+ const reopenEditor = (annotation, point) => {
1595
+ setEditor({
1596
+ annotationId: annotation.id,
1597
+ mode: "edit",
1598
+ x: point.x,
1599
+ y: point.y
1600
+ });
1601
+ };
1602
+ const closeEditor = () => setEditor(null);
1603
+ const editingAnnotation = editor === null ? void 0 : store.get(editor.annotationId);
1604
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1605
+ selection !== null && editor === null && sideDraft === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectionToolbar, {
1606
+ snapshot: selection,
1607
+ sideChatAvailable: sideChatBridge.current !== null,
1608
+ onAdd: () => {
1609
+ addToConversation(selection);
1610
+ },
1611
+ onAsk: () => {
1612
+ askInSideChat(selection);
1613
+ }
1614
+ }),
1615
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BadgeLayer, {
1616
+ store,
1617
+ sessionId: currentSessionId,
1618
+ cache: rangeCache.current,
1619
+ editingId: editor?.annotationId ?? null,
1620
+ onOpen: reopenEditor
1621
+ }),
1622
+ editingAnnotation !== void 0 && editor !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationEditor, {
1623
+ annotation: editingAnnotation,
1624
+ mode: editor.mode,
1625
+ x: editor.x,
1626
+ y: editor.y,
1627
+ onSave: (note) => {
1628
+ store.setNote(editingAnnotation.id, note);
1629
+ closeEditor();
1630
+ },
1631
+ onDelete: () => {
1632
+ store.remove(editingAnnotation.id);
1633
+ rangeCache.current.delete(editingAnnotation.id);
1634
+ closeEditor();
1635
+ },
1636
+ onCancel: () => {
1637
+ if (editor.mode === "new") {
1638
+ store.remove(editingAnnotation.id);
1639
+ rangeCache.current.delete(editingAnnotation.id);
1640
+ }
1641
+ closeEditor();
1642
+ }
1643
+ }),
1644
+ sideDraft !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SideChatNoteEditor, {
1645
+ x: sideDraft.x,
1646
+ y: sideDraft.y,
1647
+ onSave: commitSideDraft,
1648
+ onCancel: () => {
1649
+ setSideDraft(null);
1650
+ }
1651
+ })
1652
+ ] });
1653
+ }
1654
+ /** 「Ask in side chat」的注解编辑器(新建态同构:输入框 + ✓,允许空注解)。
1655
+ * bridge 失败时不关编辑器、给出内联错误——用户写好的注解不丢。 */
1656
+ function SideChatNoteEditor(props) {
1657
+ useLocaleTick();
1658
+ const [note, setNote] = (0, react.useState)("");
1659
+ const [failed, setFailed] = (0, react.useState)(false);
1660
+ const rootRef = (0, react.useRef)(null);
1661
+ (0, react.useEffect)(() => {
1662
+ const onKeyDown = (event) => {
1663
+ if (event.key === "Escape") {
1664
+ event.stopPropagation();
1665
+ props.onCancel();
1666
+ }
1667
+ };
1668
+ const onMouseDown = (event) => {
1669
+ const root = rootRef.current;
1670
+ if (root === null || !(event.target instanceof Node)) return;
1671
+ if (root.contains(event.target)) return;
1672
+ if (event.target instanceof Element && event.target.closest("[data-dsh-sidenote]") !== null) return;
1673
+ props.onCancel();
1674
+ };
1675
+ document.addEventListener("keydown", onKeyDown, true);
1676
+ document.addEventListener("mousedown", onMouseDown, true);
1677
+ return () => {
1678
+ document.removeEventListener("keydown", onKeyDown, true);
1679
+ document.removeEventListener("mousedown", onMouseDown, true);
1680
+ };
1681
+ }, []);
1682
+ const width = 320;
1683
+ const left = Math.max(8, Math.min(window.innerWidth - width - 8, props.x + 16));
1684
+ const top = Math.max(8, Math.min(window.innerHeight - 120, props.y - 20));
1685
+ const save = () => {
1686
+ setFailed(!props.onSave(note));
1687
+ };
1688
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1689
+ ref: rootRef,
1690
+ className: annotate_module_css_default.editorNew,
1691
+ style: {
1692
+ left,
1693
+ top,
1694
+ width
1695
+ },
1696
+ children: [
1697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1698
+ className: annotate_module_css_default.editorInput,
1699
+ value: note,
1700
+ placeholder: t("sideNotePlaceholder"),
1701
+ "aria-label": t("sideNoteAria"),
1702
+ autoFocus: true,
1703
+ onChange: (event) => {
1704
+ setNote(event.target.value);
1705
+ setFailed(false);
1706
+ },
1707
+ onKeyDown: (event) => {
1708
+ if (event.key === "Enter") {
1709
+ event.preventDefault();
1710
+ save();
1711
+ }
1712
+ }
1713
+ }),
1714
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1715
+ type: "button",
1716
+ className: annotate_module_css_default.confirmButton,
1717
+ title: t("confirmTitle"),
1718
+ "aria-label": t("confirmAskAria"),
1719
+ onClick: save,
1720
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 })
1721
+ }),
1722
+ failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1723
+ className: annotate_module_css_default.editorError,
1724
+ children: t("openSideFailed")
1725
+ })
1726
+ ]
1727
+ });
1728
+ }
1729
+ /** The floating two-button toolbar above a validated selection. */
1730
+ function SelectionToolbar(props) {
1731
+ useLocaleTick();
1732
+ const { rect } = props.snapshot;
1733
+ const left = Math.max(8, Math.min(window.innerWidth - 8, rect.left + rect.width / 2));
1734
+ const top = Math.max(4, rect.top - 10);
1735
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1736
+ className: annotate_module_css_default.toolbar,
1737
+ style: {
1738
+ left,
1739
+ top
1740
+ },
1741
+ role: "toolbar",
1742
+ "aria-label": t("toolbarAria"),
1743
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1744
+ type: "button",
1745
+ className: annotate_module_css_default.toolbarButton,
1746
+ onMouseDown: (event) => {
1747
+ event.preventDefault();
1748
+ },
1749
+ onClick: (event) => {
1750
+ event.preventDefault();
1751
+ event.stopPropagation();
1752
+ props.onAdd();
1753
+ },
1754
+ children: t("addToConversation")
1755
+ }), props.sideChatAvailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1756
+ type: "button",
1757
+ className: annotate_module_css_default.toolbarButton,
1758
+ onMouseDown: (event) => {
1759
+ event.preventDefault();
1760
+ },
1761
+ onClick: (event) => {
1762
+ event.preventDefault();
1763
+ event.stopPropagation();
1764
+ props.onAsk();
1765
+ },
1766
+ children: t("askInSideChat")
1767
+ })]
1768
+ });
1769
+ }
1770
+ /** The badge layer (one numbered circle per annotation) + the active highlight. */
1771
+ function BadgeLayer(props) {
1772
+ const annotations = props.sessionId === "" ? [] : props.store.list(props.sessionId);
1773
+ const badges = [];
1774
+ const placed = [];
1775
+ let highlight = null;
1776
+ for (const annotation of annotations) {
1777
+ const range = resolveRange(annotation, props.cache);
1778
+ if (range === null) continue;
1779
+ if (annotation.id === props.editingId) highlight = highlightRectsOf(range).map((rect, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1780
+ className: annotate_module_css_default.highlight,
1781
+ style: {
1782
+ left: rect.left,
1783
+ top: rect.top,
1784
+ width: rect.width,
1785
+ height: rect.height
1786
+ }
1787
+ }, `hl-${index}`));
1788
+ const anchor = badgeAnchorOf(range);
1789
+ if (anchor === null) continue;
1790
+ if (anchor.centerY < 0 || anchor.centerY > window.innerHeight || anchor.right < 0 || anchor.right > window.innerWidth) continue;
1791
+ const point = spreadBadgePoint({
1792
+ x: anchor.right,
1793
+ y: anchor.centerY
1794
+ }, placed);
1795
+ placed.push(point);
1796
+ badges.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1797
+ type: "button",
1798
+ className: annotate_module_css_default.badge,
1799
+ style: {
1800
+ left: point.x + 6,
1801
+ top: point.y
1802
+ },
1803
+ title: annotation.note === "" ? annotation.text : `${annotation.text}\n${t("noteLine", { note: annotation.note })}`,
1804
+ onClick: (event) => {
1805
+ event.preventDefault();
1806
+ event.stopPropagation();
1807
+ props.onOpen(annotation, point);
1808
+ },
1809
+ children: annotation.number
1810
+ }, annotation.id));
1811
+ }
1812
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [highlight, badges] });
1813
+ }
1814
+ /**
1815
+ * The annotation editor popover. 新建态: input + ✓ 确认 (允许空注解直接保存;
1816
+ * 点击外部/Esc 取消且无显式取消按钮). 重开态: 已有注解 + 🗑 删除 + 取消/保存.
1817
+ */
1818
+ function AnnotationEditor(props) {
1819
+ useLocaleTick();
1820
+ const [note, setNote] = (0, react.useState)(props.annotation.note);
1821
+ const rootRef = (0, react.useRef)(null);
1822
+ (0, react.useEffect)(() => {
1823
+ const onKeyDown = (event) => {
1824
+ if (event.key === "Escape") {
1825
+ event.stopPropagation();
1826
+ props.onCancel();
1827
+ }
1828
+ };
1829
+ const onMouseDown = (event) => {
1830
+ const root = rootRef.current;
1831
+ if (root === null || !(event.target instanceof Node)) return;
1832
+ if (root.contains(event.target)) return;
1833
+ if (event.target instanceof Element && event.target.closest("[data-dsh-sidenote]") !== null) return;
1834
+ props.onCancel();
1835
+ };
1836
+ document.addEventListener("keydown", onKeyDown, true);
1837
+ document.addEventListener("mousedown", onMouseDown, true);
1838
+ return () => {
1839
+ document.removeEventListener("keydown", onKeyDown, true);
1840
+ document.removeEventListener("mousedown", onMouseDown, true);
1841
+ };
1842
+ }, []);
1843
+ const width = props.mode === "new" ? 320 : 340;
1844
+ const left = Math.max(8, Math.min(window.innerWidth - width - 8, props.x + 16));
1845
+ const top = Math.max(8, Math.min(window.innerHeight - 120, props.y - 20));
1846
+ const save = () => {
1847
+ props.onSave(note);
1848
+ };
1849
+ if (props.mode === "new") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1850
+ ref: rootRef,
1851
+ className: annotate_module_css_default.editorNew,
1852
+ style: {
1853
+ left,
1854
+ top,
1855
+ width
1856
+ },
1857
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1858
+ className: annotate_module_css_default.editorInput,
1859
+ value: note,
1860
+ placeholder: t("notePlaceholder"),
1861
+ autoFocus: true,
1862
+ onChange: (event) => {
1863
+ setNote(event.target.value);
1864
+ },
1865
+ onKeyDown: (event) => {
1866
+ if (event.key === "Enter") {
1867
+ event.preventDefault();
1868
+ save();
1869
+ }
1870
+ }
1871
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1872
+ type: "button",
1873
+ className: annotate_module_css_default.confirmButton,
1874
+ title: t("confirmTitle"),
1875
+ "aria-label": t("saveNoteAria"),
1876
+ onClick: save,
1877
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 })
1878
+ })]
1879
+ });
1880
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1881
+ ref: rootRef,
1882
+ className: annotate_module_css_default.editorEdit,
1883
+ style: {
1884
+ left,
1885
+ top,
1886
+ width
1887
+ },
1888
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1889
+ className: annotate_module_css_default.editorTextarea,
1890
+ value: note,
1891
+ placeholder: t("notePlaceholder"),
1892
+ rows: 3,
1893
+ autoFocus: true,
1894
+ onChange: (event) => {
1895
+ setNote(event.target.value);
1896
+ },
1897
+ onKeyDown: (event) => {
1898
+ if (event.key === "Enter" && !event.shiftKey) {
1899
+ event.preventDefault();
1900
+ save();
1901
+ }
1902
+ }
1903
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1904
+ className: annotate_module_css_default.editorFooter,
1905
+ children: [
1906
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1907
+ type: "button",
1908
+ className: annotate_module_css_default.deleteButton,
1909
+ title: t("deleteNote"),
1910
+ "aria-label": t("deleteNote"),
1911
+ onClick: props.onDelete,
1912
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 })
1913
+ }),
1914
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: annotate_module_css_default.editorSpacer }),
1915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1916
+ type: "button",
1917
+ className: annotate_module_css_default.cancelButton,
1918
+ onClick: props.onCancel,
1919
+ children: t("cancel")
1920
+ }),
1921
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1922
+ type: "button",
1923
+ className: annotate_module_css_default.saveButton,
1924
+ onClick: save,
1925
+ children: t("save")
1926
+ })
1927
+ ]
1928
+ })]
1929
+ });
1930
+ }
1931
+ //#endregion
1932
+ //#region src/client/annotate/chip.tsx
1933
+ /**
1934
+ * The 「N 条注释」 composer chip (Workitem 02): a `conversation.input.dock`
1935
+ * list entry (the official composer-attachment seat; todo dock lives at order
1936
+ * 0, the queue strip at 20 — we sit between at 10). The chip counts the
1937
+ * session's ACTIVE annotations, expands inline to preview/remove each one,
1938
+ * and watches the owner-provided input snapshot for the send edge (draft
1939
+ * non-empty → empty) to flip the annotations to 'sent' — the chip then
1940
+ * disappears while the badges stay on the message flow.
1941
+ */
1942
+ /** Create the slot component bound to one store instance. */
1943
+ function createAnnotationChip(store) {
1944
+ function AnnotationChip(props) {
1945
+ useLocaleTick();
1946
+ const sessionId = props.session.sessionId;
1947
+ (0, react.useSyncExternalStore)((0, react.useCallback)((cb) => store.subscribe(cb), [store]), () => store.getSnapshot());
1948
+ const [expanded, setExpanded] = (0, react.useState)(false);
1949
+ const previous = (0, react.useRef)({
1950
+ sessionId,
1951
+ draft: props.input.draft,
1952
+ phase: props.input.phase,
1953
+ queueLen: props.input.queue?.length ?? 0,
1954
+ running: props.session.running
1955
+ });
1956
+ (0, react.useEffect)(() => {
1957
+ const prev = previous.current;
1958
+ previous.current = {
1959
+ sessionId,
1960
+ draft: props.input.draft,
1961
+ phase: props.input.phase,
1962
+ queueLen: props.input.queue?.length ?? 0,
1963
+ running: props.session.running
1964
+ };
1965
+ if (prev.sessionId !== sessionId) return;
1966
+ if (store.countActive(sessionId) === 0) return;
1967
+ if (!isSendEdge(prev.draft, props.input.draft)) return;
1968
+ if (!(prev.phase !== "plain" || (props.input.queue?.length ?? 0) > prev.queueLen || props.session.running && !prev.running)) return;
1969
+ store.markSessionSent(sessionId);
1970
+ });
1971
+ const active = store.listActive(sessionId);
1972
+ if (active.length === 0) return null;
1973
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1974
+ className: annotate_module_css_default.chipWrap,
1975
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1976
+ type: "button",
1977
+ className: annotate_module_css_default.chip,
1978
+ "aria-expanded": expanded,
1979
+ onClick: () => {
1980
+ setExpanded((open) => !open);
1981
+ },
1982
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconListPenOutline16, { size: 12 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(active.length === 1 ? "chipOne" : "chipMany", { n: active.length }) })]
1983
+ }), expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1984
+ className: annotate_module_css_default.chipPanel,
1985
+ children: active.map((annotation) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1986
+ className: annotate_module_css_default.chipRow,
1987
+ children: [
1988
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1989
+ className: annotate_module_css_default.chipNumber,
1990
+ children: annotation.number
1991
+ }),
1992
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1993
+ className: annotate_module_css_default.chipText,
1994
+ title: annotation.text,
1995
+ children: [annotation.text, annotation.note !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1996
+ className: annotate_module_css_default.chipNote,
1997
+ children: [
1998
+ "(",
1999
+ annotation.note,
2000
+ ")"
2001
+ ]
2002
+ })]
2003
+ }),
2004
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2005
+ type: "button",
2006
+ className: annotate_module_css_default.chipRemove,
2007
+ title: t("removeTitle"),
2008
+ "aria-label": t("removeAria", { n: annotation.number }),
2009
+ onClick: () => {
2010
+ store.remove(annotation.id);
2011
+ },
2012
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 12 })
2013
+ })
2014
+ ]
2015
+ }, annotation.id))
2016
+ })]
2017
+ });
2018
+ }
2019
+ return function AnnotationChipEntry(props) {
2020
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotateErrorBoundary, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationChip, { ...props }) });
2021
+ };
2022
+ }
2023
+ //#endregion
2024
+ //#region src/client/annotate/draft.ts
2025
+ /** The head (quote block + blank line) last written into each session's draft. */
2026
+ const managedHeads = /* @__PURE__ */ new Map();
2027
+ /** Resolve the per-session input facade, degrading to undefined (never throws). */
2028
+ function resolveInput(ctx, sessionId) {
2029
+ try {
2030
+ const actx = ctx.sessions.scope(sessionId);
2031
+ if (actx === void 0) return void 0;
2032
+ return ctx.get("conversation")?.input.for(actx);
2033
+ } catch {
2034
+ return;
2035
+ }
2036
+ }
2037
+ /**
2038
+ * Rewrite one session's draft so its managed head matches the active
2039
+ * annotations. A no-op when nothing would change (the common case while the
2040
+ * user types their own message below the head).
2041
+ */
2042
+ function syncSessionDraft(ctx, store, sessionId) {
2043
+ try {
2044
+ const input = resolveInput(ctx, sessionId);
2045
+ if (input === void 0) return;
2046
+ const block = buildQuoteBlock(store.listActive(sessionId));
2047
+ const lastHead = managedHeads.get(sessionId) ?? "";
2048
+ const draft = input.state.getSnapshot().draft;
2049
+ const next = nextManagedDraft(draft, lastHead, block);
2050
+ if (block !== "") managedHeads.set(sessionId, next.head);
2051
+ if (next.draft === draft) return;
2052
+ input.setDraft(next.draft);
2053
+ } catch (error) {
2054
+ console.warn("[dsh-sidenote] draft sync failed:", error);
2055
+ }
2056
+ }
2057
+ /** Re-sync every session holding annotations (store-change fan-out). */
2058
+ function syncAllDrafts(ctx, store) {
2059
+ for (const sessionId of store.sessions()) syncSessionDraft(ctx, store, sessionId);
2060
+ }
2061
+ /** Forget all managed heads (page-level lifecycle; plugin teardown/testing). */
2062
+ function resetManagedHeads() {
2063
+ managedHeads.clear();
2064
+ }
2065
+ //#endregion
2066
+ //#region src/client/annotate/index.tsx
2067
+ /**
2068
+ * Workitem 02 — 划选注释:selection listener + numbered badges + annotation
2069
+ * editor + the composer「N 条注释」chip. Wiring only: one annotation store,
2070
+ * one selection controller, one overlay React root (appended to document.body
2071
+ * and marked `data-dsh-sidenote` so the listener excludes our own DOM), and
2072
+ * one `conversation.input.dock` slot entry. 全部 additive、页面级生命周期,
2073
+ * and every seam degrades to a logged no-op instead of crashing the host page.
2074
+ *
2075
+ * 整个模块包在一个 ctx.effect 里:HMR/插件禁用时 fiber 撤销即全部清理
2076
+ * (store 退订、document 监听器移除、overlay unmount、host div 移除、受管
2077
+ * 草稿头清空)——否则重挂后双重挂载(两层工具条、双份监听、新旧 store
2078
+ * 分裂)。slot 注册的 disposer 由 cordis 服务代理路由进调用方 fiber,
2079
+ * 无需手动回收。
2080
+ */
2081
+ function registerAnnotations(ctx) {
2082
+ ctx.effect(() => {
2083
+ try {
2084
+ const store = createAnnotationStore();
2085
+ const controller = createSelectionController(() => ctx.sessions.list.getSnapshot().current ?? "");
2086
+ const host = document.createElement("div");
2087
+ host.dataset.dshSidenote = "";
2088
+ document.body.appendChild(host);
2089
+ const root = (0, react_dom_client.createRoot)(host);
2090
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotateOverlay, {
2091
+ ctx,
2092
+ store,
2093
+ controller
2094
+ }));
2095
+ const offStore = store.subscribe(() => {
2096
+ syncAllDrafts(ctx, store);
2097
+ });
2098
+ ctx.slots.inject("conversation.input.dock", () => {
2099
+ return ctx.slots.register({
2100
+ name: "conversation.input.dock",
2101
+ id: "dsh-sidenote-annotations",
2102
+ order: 10,
2103
+ registrant: "dsh-sidenote"
2104
+ }, createAnnotationChip(store));
2105
+ });
2106
+ return () => {
2107
+ offStore();
2108
+ controller.dispose();
2109
+ resetManagedHeads();
2110
+ setTimeout(() => {
2111
+ root.unmount();
2112
+ });
2113
+ host.remove();
2114
+ };
2115
+ } catch (error) {
2116
+ console.error("[dsh-sidenote] annotate setup failed:", error);
2117
+ return () => {};
2118
+ }
2119
+ }, "dsh-sidenote: annotations");
2120
+ }
2121
+ //#endregion
2122
+ //#region src/client/index.tsx
2123
+ const inject = [
2124
+ "betterSidebar",
2125
+ "sessions",
2126
+ "workspaces",
2127
+ "slots",
2128
+ "connection",
2129
+ "locale"
2130
+ ];
2131
+ function apply(ctx) {
2132
+ attachLocale(ctx.locale);
2133
+ registerSideChat(ctx);
2134
+ registerAnnotations(ctx);
2135
+ }
2136
+ //#endregion
2137
+ exports.apply = apply;
2138
+ exports.inject = inject;
2139
+ return module.exports;
2140
+ }
2141
+ });
2142
+
2143
+ //# sourceMappingURL=client.js.map