dsh-sidenote 0.2.0 → 0.3.1

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 (82) hide show
  1. package/README.md +11 -4
  2. package/lib/client.js +2628 -749
  3. package/lib/types/client/annotate/chip.d.ts +1 -1
  4. package/lib/types/client/annotate/format.d.ts +5 -0
  5. package/lib/types/client/annotate/index.d.ts +1 -1
  6. package/lib/types/client/annotate/overlay.d.ts +1 -1
  7. package/lib/types/client/annotate/send.d.ts +1 -1
  8. package/lib/types/client/bridge.d.ts +1 -1
  9. package/lib/types/client/chat/FoldCard.d.ts +7 -0
  10. package/lib/types/client/chat/ReasoningRow.d.ts +7 -0
  11. package/lib/types/client/chat/ToolCard.d.ts +9 -0
  12. package/lib/types/client/chat/cards.d.ts +150 -0
  13. package/lib/types/client/chat/transcript.d.ts +65 -0
  14. package/lib/types/client/chat/viewState.d.ts +19 -0
  15. package/lib/types/{context-types.d.ts → client/host/contracts.d.ts} +62 -6
  16. package/lib/types/client/host/labels.d.ts +55 -0
  17. package/lib/types/client/host/markdown.d.ts +32 -0
  18. package/lib/types/client/host/probes.d.ts +13 -0
  19. package/lib/types/client/index.d.ts +1 -1
  20. package/lib/types/client/locale-tick.d.ts +2 -0
  21. package/lib/types/client/locales.d.ts +46 -2
  22. package/lib/types/client/reflow.d.ts +7 -3
  23. package/lib/types/client/sidechat/ComposerBar.d.ts +13 -0
  24. package/lib/types/client/sidechat/ModelMenu.d.ts +8 -0
  25. package/lib/types/client/sidechat/PermissionChip.d.ts +4 -0
  26. package/lib/types/client/sidechat/SideChatPanel.d.ts +1 -1
  27. package/lib/types/client/sidechat/composer/attach.d.ts +28 -0
  28. package/lib/types/client/sidechat/composer/slash.d.ts +61 -0
  29. package/lib/types/client/sidechat/composer.d.ts +19 -3
  30. package/lib/types/client/sidechat/header.d.ts +1 -1
  31. package/lib/types/client/sidechat/index.d.ts +1 -1
  32. package/lib/types/client/sidechat/lifecycle.d.ts +52 -0
  33. package/lib/types/client/sidechat/model.d.ts +12 -32
  34. package/lib/types/client/sidechat/open.d.ts +8 -1
  35. package/lib/types/client/sidechat/recentClosed.d.ts +18 -0
  36. package/lib/types/client/sidechat/referenceSource.d.ts +17 -0
  37. package/lib/types/client/sidechat/rows.d.ts +20 -0
  38. package/lib/types/client/sidechat/slash.d.ts +1 -1
  39. package/lib/types/client/sidechat/toast.d.ts +5 -0
  40. package/lib/types/index.d.ts +1 -1
  41. package/package.json +9 -6
  42. package/src/client/annotate/bubble.ts +2 -2
  43. package/src/client/annotate/chip.tsx +3 -2
  44. package/src/client/annotate/format.ts +10 -0
  45. package/src/client/annotate/hint.tsx +2 -1
  46. package/src/client/annotate/index.tsx +1 -1
  47. package/src/client/annotate/overlay.tsx +3 -2
  48. package/src/client/annotate/send.ts +8 -2
  49. package/src/client/bridge.ts +1 -1
  50. package/src/client/chat/FoldCard.tsx +33 -0
  51. package/src/client/chat/ReasoningRow.tsx +45 -0
  52. package/src/client/chat/ToolCard.tsx +158 -0
  53. package/src/client/chat/cards.ts +461 -0
  54. package/src/client/chat/transcript.ts +275 -0
  55. package/src/client/chat/viewState.ts +75 -0
  56. package/src/{context-types.ts → client/host/contracts.ts} +48 -6
  57. package/src/client/host/labels.ts +82 -0
  58. package/src/client/host/markdown.ts +48 -0
  59. package/src/client/host/probes.ts +78 -0
  60. package/src/client/index.tsx +4 -1
  61. package/src/client/locale-tick.ts +12 -0
  62. package/src/client/locales.ts +107 -7
  63. package/src/client/reflow.ts +24 -8
  64. package/src/client/sidechat/ComposerBar.tsx +165 -0
  65. package/src/client/sidechat/ModelMenu.tsx +84 -0
  66. package/src/client/sidechat/PermissionChip.tsx +85 -0
  67. package/src/client/sidechat/SideChatPanel.tsx +211 -313
  68. package/src/client/sidechat/composer/attach.ts +44 -0
  69. package/src/client/sidechat/composer/slash.tsx +156 -0
  70. package/src/client/sidechat/composer.ts +70 -12
  71. package/src/client/sidechat/header.tsx +3 -2
  72. package/src/client/sidechat/index.tsx +21 -1
  73. package/src/client/sidechat/lifecycle.ts +176 -0
  74. package/src/client/sidechat/model.ts +24 -167
  75. package/src/client/sidechat/open.ts +26 -1
  76. package/src/client/sidechat/recentClosed.ts +58 -0
  77. package/src/client/sidechat/referenceSource.ts +111 -0
  78. package/src/client/sidechat/rows.tsx +278 -0
  79. package/src/client/sidechat/sidechat.module.css +441 -26
  80. package/src/client/sidechat/slash.ts +14 -2
  81. package/src/client/sidechat/toast.tsx +60 -0
  82. package/src/index.ts +1 -1
package/lib/client.js CHANGED
@@ -4,10 +4,59 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let react = require("react");
8
7
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ let react = require("react");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
10
  let react_dom_client = require("react-dom/client");
11
+ //#region src/client/host/probes.ts
12
+ const PROBES = [
13
+ {
14
+ name: "Session.open()(非 staged 会话开窗)",
15
+ check: (ctx) => {
16
+ return typeof ctx.sessions?.binding === "function";
17
+ },
18
+ fallback: "侧边聊天消息流降级为只发不收"
19
+ },
20
+ {
21
+ name: "conversation.input 输入机器",
22
+ check: (ctx) => {
23
+ return typeof (ctx.get?.("conversation"))?.input?.for === "function";
24
+ },
25
+ fallback: "侧边 composer 降级为本地草稿 + session.prompt"
26
+ },
27
+ {
28
+ name: "commandUi(斜杠命令注册)",
29
+ check: (ctx) => {
30
+ return typeof (ctx.get?.("commandUi"))?.register === "function";
31
+ },
32
+ fallback: "/side 与「/侧边」命令不可用(菜单/顶栏入口仍在)"
33
+ },
34
+ {
35
+ name: "ctx.modules.import(动态组件复用)",
36
+ check: (ctx) => {
37
+ return typeof ctx.modules?.import === "function";
38
+ },
39
+ fallback: "ProducedFiles 等动态复用件降级为自绘"
40
+ }
41
+ ];
42
+ /** 激活时跑一遍探测清单;缺失项逐项 warn-once。返回缺失项数(测试可断言)。 */
43
+ function probeHost(ctx) {
44
+ let missing = 0;
45
+ for (const probe of PROBES) {
46
+ let ok = false;
47
+ try {
48
+ ok = probe.check(ctx);
49
+ } catch {
50
+ ok = false;
51
+ }
52
+ if (!ok) {
53
+ missing += 1;
54
+ console.warn(`[dsh-sidenote] 宿主能力缺失:${probe.name} → ${probe.fallback}`);
55
+ }
56
+ }
57
+ return missing;
58
+ }
59
+ //#endregion
11
60
  //#region src/client/locales.ts
12
61
  /** 英文词典(key 的权威清单)。 */
13
62
  const en = {
@@ -29,13 +78,24 @@ window.__ModuleLoader__.load({
29
78
  stopped: "Stopped",
30
79
  toolLabel: "Tool",
31
80
  failed: "failed",
81
+ todoTitle: "Tasks",
82
+ todoDone: "{n} done",
83
+ todoDoing: "{n} in progress",
84
+ todoPending: "{n} pending",
32
85
  running: "running…",
33
86
  inputPlaceholder: "Message… (Enter to send, Shift+Enter for a newline)",
34
87
  modelLabel: "Model: {name}",
88
+ modelSwitchTitle: "Switch the side chat model",
89
+ permChipTitle: "Permission mode",
90
+ permFullAccess: "Full access",
91
+ pendingNotice: "⚠ 侧边会话有审批/提问待处理——点击跳到主视图处理",
92
+ writeNotice: "侧边会话与主会话同权限,可以修改文件(当前模式见输入框旁 chip)",
35
93
  modelFollowsMain: "follows main session",
36
94
  stopReply: "Stop",
37
95
  stopReplyTitle: "Stop the current reply",
38
96
  send: "Send",
97
+ attachTitle: "Attach images",
98
+ attachRemove: "Remove",
39
99
  imagePlaceholder: "[Image]",
40
100
  toolFallback: "Tool",
41
101
  unknownError: "Unknown error",
@@ -50,6 +110,8 @@ window.__ModuleLoader__.load({
50
110
  cmdNewDetail: "Forked from the current session; evolves independently",
51
111
  cmdFocus: "Focus \"{title}\"",
52
112
  cmdFocusDetail: "Existing side chat",
113
+ cmdReopen: "Reopen \"{title}\"",
114
+ cmdReopenDetail: "Recently closed side chat",
53
115
  addToConversation: "Add to conversation",
54
116
  askInSideChat: "Ask in side chat",
55
117
  toolbarAria: "Selection annotations",
@@ -88,7 +150,37 @@ window.__ModuleLoader__.load({
88
150
  reflowChipMany: "{n} side-chat reflows",
89
151
  reflowReason: "brought back from a side chat by the user",
90
152
  reflowBubbleLabel: "Side-chat context",
91
- reflowRemoveTitle: "Remove reflow"
153
+ reflowRemoveTitle: "Remove reflow",
154
+ termSignal: "Terminated ({signal})",
155
+ termExitCode: "Exit code {code}",
156
+ termDone: "done",
157
+ termNoOutput: "(no output)",
158
+ termCollapse: "Collapse",
159
+ termCollapseAria: "Collapse the output",
160
+ termExpand: "Show all ({n} hidden lines)",
161
+ termExpandAria: "Show all ({n} hidden lines)",
162
+ inheritedLabel: "Inherited from main session · up to the fork point · {n}",
163
+ collapseAll: "Collapse all",
164
+ expandAll: "Expand all",
165
+ jumpToLatest: "↓ Jump to latest",
166
+ reflowAll: "Reflow all",
167
+ reflowAllTitle: "Bring all Q&A pairs back to the main session",
168
+ reflowAllDone: "Reflowed ✓",
169
+ promote: "Save as session",
170
+ promoteTitle: "Fork this side chat into a standalone session in the main list",
171
+ promoteDone: "Saved as a regular session — it's open in the main view now",
172
+ readWindow: "{shown} of {total} lines",
173
+ searchPathsSummary: "{shown} of {total} paths",
174
+ searchMatchesSummary: "{shown} of {total} matches in {files} files",
175
+ searchNoResults: "No results",
176
+ diffFiles: "{n} files",
177
+ webNoResults: "No results",
178
+ webSourcesTruncated: "Source list truncated",
179
+ webContentTruncated: "Content truncated",
180
+ parentStripTitle: "Main session status — click to jump back",
181
+ parentRunning: "Main: running",
182
+ parentPending: "Main: awaiting approval",
183
+ parentIdle: "Main: idle"
92
184
  };
93
185
  /** 中文词典(与 en 同 key)。 */
94
186
  const zh = {
@@ -110,13 +202,24 @@ window.__ModuleLoader__.load({
110
202
  stopped: "已停止",
111
203
  toolLabel: "工具",
112
204
  failed: "失败",
205
+ todoTitle: "任务",
206
+ todoDone: "{n} 已完成",
207
+ todoDoing: "{n} 进行中",
208
+ todoPending: "{n} 待处理",
113
209
  running: "执行中…",
114
210
  inputPlaceholder: "输入消息…(Enter 发送,Shift+Enter 换行)",
115
211
  modelLabel: "模型:{name}",
212
+ modelSwitchTitle: "切换侧边聊天模型",
213
+ permChipTitle: "权限模式",
214
+ permFullAccess: "Full access",
215
+ pendingNotice: "⚠ 侧边会话有审批/提问待处理——点击跳到主视图处理",
216
+ writeNotice: "侧边会话与主会话同权限,可以修改文件(当前模式见输入框旁 chip)",
116
217
  modelFollowsMain: "跟随主会话",
117
218
  stopReply: "停止",
118
219
  stopReplyTitle: "停止当前回复",
119
220
  send: "发送",
221
+ attachTitle: "添加图片附件",
222
+ attachRemove: "移除",
120
223
  imagePlaceholder: "[图片]",
121
224
  toolFallback: "工具",
122
225
  unknownError: "未知错误",
@@ -131,6 +234,8 @@ window.__ModuleLoader__.load({
131
234
  cmdNewDetail: "从当前会话 fork,独立演进",
132
235
  cmdFocus: "聚焦「{title}」",
133
236
  cmdFocusDetail: "已存在的侧边聊天",
237
+ cmdReopen: "重开「{title}」",
238
+ cmdReopenDetail: "最近关闭的侧边聊天",
134
239
  addToConversation: "添加到对话",
135
240
  askInSideChat: "在侧边聊天中提问",
136
241
  toolbarAria: "划选注释",
@@ -169,7 +274,37 @@ window.__ModuleLoader__.load({
169
274
  reflowChipMany: "{n} 条侧边回流",
170
275
  reflowReason: "用户选择从侧边聊天带回主线",
171
276
  reflowBubbleLabel: "含侧边回流上下文",
172
- reflowRemoveTitle: "移除回流"
277
+ reflowRemoveTitle: "移除回流",
278
+ termSignal: "已终止({signal})",
279
+ termExitCode: "退出码 {code}",
280
+ termDone: "完成",
281
+ termNoOutput: "(无输出)",
282
+ termCollapse: "收起",
283
+ termCollapseAria: "收起输出",
284
+ termExpand: "展开全部(隐藏 {n} 行)",
285
+ termExpandAria: "展开全部(隐藏 {n} 行)",
286
+ inheritedLabel: "继承自主会话 · 截至 fork 点 · {n} 条",
287
+ collapseAll: "全部折叠",
288
+ expandAll: "全部展开",
289
+ jumpToLatest: "↓ 跳到最新",
290
+ reflowAll: "整段回流",
291
+ reflowAllTitle: "把该侧聊全部问答对带回主会话(chip 可预览/逐条撤)",
292
+ reflowAllDone: "已回流 ✓",
293
+ promote: "保存为正式会话",
294
+ promoteTitle: "把侧边聊天转为会话列表里的正式会话",
295
+ promoteDone: "已保存为正式会话——已在主视图打开",
296
+ readWindow: "{shown}/{total} 行",
297
+ searchPathsSummary: "{shown}/{total} 个路径",
298
+ searchMatchesSummary: "{shown}/{total} 处匹配 · {files} 个文件",
299
+ searchNoResults: "无结果",
300
+ diffFiles: "{n} 个文件",
301
+ webNoResults: "无结果",
302
+ webSourcesTruncated: "来源列表已截断",
303
+ webContentTruncated: "内容已截断",
304
+ parentStripTitle: "主线会话状态——点击回到主对话",
305
+ parentRunning: "主线:运行中",
306
+ parentPending: "主线:待审批",
307
+ parentIdle: "主线:空闲"
173
308
  };
174
309
  let localeService;
175
310
  /** Attach the DSH locale service (call once from the client apply). */
@@ -184,16 +319,16 @@ window.__ModuleLoader__.load({
184
319
  function activeLocale() {
185
320
  return localeService?.getSnapshot().active ?? (typeof navigator !== "undefined" ? navigator.language : "") ?? "en";
186
321
  }
322
+ /** uSES 快照源(locale-tick.ts 专用出口):语言字符串本身即快照。 */
323
+ function localeSnapshot() {
324
+ return localeService?.getSnapshot().active ?? "en";
325
+ }
187
326
  /** Translate a copy key; `{name}` placeholders interpolate from `params`. */
188
327
  function t(key, params) {
189
328
  let text = (activeLocale().toLowerCase().startsWith("zh") ? zh : en)[key];
190
329
  if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, String(value));
191
330
  return text;
192
331
  }
193
- /** 语言切换时驱动组件重渲(useSyncExternalStore 标准接法)。 */
194
- function useLocaleTick() {
195
- (0, react.useSyncExternalStore)(subscribeLocale, () => localeService?.getSnapshot().active ?? "en");
196
- }
197
332
  //#endregion
198
333
  //#region src/client/reflow.ts
199
334
  /**
@@ -202,8 +337,8 @@ window.__ModuleLoader__.load({
202
337
  * 发送瞬间由 annotate/send.ts 拦截器与注释协议块一起序列化进消息。
203
338
  * 不经草稿文本流(用户裁定:直接进草稿「很脏」)。
204
339
  *
205
- * 气泡留痕:序列化形态是**单块 blockquote**(首行为来源说明),bubble.ts
206
- * 按首行标记识别并整块折叠为「含侧边回流上下文」标签。
340
+ * 气泡留痕:序列化形态是 XML 协议块(见 buildReflowBlock),随注释协议块
341
+ * 一起作为消息连续前缀注入,bubble.ts 反解析后折叠为「含侧边回流上下文」标签。
207
342
  *
208
343
  * 持久化:localStorage 按会话键(dsh-sidenote:reflow:v1:<sessionId>),
209
344
  * 与注释 store 同款纪律(刷新不丢、容错 revive、空删键)。
@@ -213,14 +348,19 @@ window.__ModuleLoader__.load({
213
348
  * 回流块序列化(XML 形态,与注释协议同族):
214
349
  *
215
350
  * <reflow source="侧边 2" reason="用户选择从侧边聊天带回主线">
216
- * 结论全文(不截断)
351
+ * <问>用户在侧边聊天里的提问</问>
352
+ * <答>结论全文(不截断)</答>
217
353
  * </reflow>
218
354
  *
355
+ * 问答成对:只有答没有问,模型理解打折;question 缺省时只有 <答>。
219
356
  * 来源与意图放在属性里,内容行保持纯净;模型对 XML 包裹的上下文理解最好
220
357
  * (Claude 官方推荐形态 / Codex additionalContext 同款思路)。
221
358
  */
222
359
  function buildReflowBlock(item) {
223
- return `<reflow source="${item.sideTitle}" reason="${t("reflowReason")}">\n${item.text}\n</reflow>`;
360
+ const parts = [];
361
+ if (item.question !== void 0 && item.question.trim() !== "") parts.push(`<问>${item.question}</问>`);
362
+ parts.push(`<答>${item.text}</答>`);
363
+ return `<reflow source="${item.sideTitle}" reason="${t("reflowReason")}">\n${parts.join("\n")}\n</reflow>`;
224
364
  }
225
365
  function revive(value) {
226
366
  if (typeof value !== "object" || value === null) return null;
@@ -232,6 +372,7 @@ window.__ModuleLoader__.load({
232
372
  sessionId: r.sessionId,
233
373
  sideTitle: r.sideTitle,
234
374
  text: r.text,
375
+ ...typeof r.question === "string" && r.question.trim() !== "" ? { question: r.question } : {},
235
376
  createdAt: typeof r.createdAt === "number" ? r.createdAt : 0
236
377
  };
237
378
  }
@@ -283,12 +424,13 @@ window.__ModuleLoader__.load({
283
424
  listeners.delete(fn);
284
425
  };
285
426
  },
286
- add(sessionId, sideTitle, text) {
427
+ add(sessionId, sideTitle, text, question) {
287
428
  const item = {
288
429
  id: nextId,
289
430
  sessionId,
290
431
  sideTitle,
291
432
  text,
433
+ ...typeof question === "string" && question.trim() !== "" ? { question } : {},
292
434
  createdAt: now()
293
435
  };
294
436
  nextId += 1;
@@ -317,6 +459,87 @@ window.__ModuleLoader__.load({
317
459
  //#region src/client/bridge.ts
318
460
  const sideChatBridge = { current: null };
319
461
  //#endregion
462
+ //#region src/client/annotate/format.ts
463
+ /** 协议头(双语):zh「我批注了以下 2 处内容:」/ en 见 locales。 */
464
+ function protocolHeader(n) {
465
+ return t("protocolHeader", { n });
466
+ }
467
+ /** One annotation's XML block(无注解时省略 <note>)。 */
468
+ function formatAnnotationXml(index, item) {
469
+ const id = item.number ?? index;
470
+ const note = item.note.trim();
471
+ return note === "" ? `<annotation id="${id}">\n<quote>${item.text}</quote>\n</annotation>` : `<annotation id="${id}">\n<quote>${item.text}</quote>\n<note>${note}</note>\n</annotation>`;
472
+ }
473
+ /** 注释协议块 = 头部句 + 每个注释一个 XML 块(行间无空行,与正文空一行)。 */
474
+ function buildProtocolBlock(items) {
475
+ return [protocolHeader(items.length), ...items.map((item, i) => formatAnnotationXml(i + 1, item))].join("\n");
476
+ }
477
+ /** 「在侧边聊天中提问」的种子引用(进侧边草稿,不是协议块——保持轻量)。 */
478
+ function buildSideChatQuote(text, note = "") {
479
+ return `${text.split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n")}\n${note.trim() === "" ? t("noNote") : t("noteLine", { note })}`;
480
+ }
481
+ /** 协议头识别(zh/en 都认):整行匹配。 */
482
+ const PROTOCOL_HEADER_RE = /^(?:我批注了以下 (\d+) 处内容:|I annotated (\d+) passage\(s\) of the conversation above:)$/;
483
+ const ANNOTATION_RE = /<annotation id="(\d+)">\s*<quote>([\s\S]*?)<\/quote>\s*(?:<note>([\s\S]*?)<\/note>\s*)?<\/annotation>/y;
484
+ const REFLOW_RE = /<reflow\s+source="([^"]*)"(?:\s+reason="([^"]*)")?\s*>([\s\S]*?)<\/reflow>/y;
485
+ /**
486
+ * 气泡 tooltip / 面板留痕 chip 的展示拍平:剥掉 <问>/<答> 协议标签只留内容行
487
+ * (协议标签是 wire 形态,不出现在 UI)。仅用于 title 悬浮文案。
488
+ */
489
+ function flattenReflowContent(content) {
490
+ return content.replace(/<问>([\s\S]*?)<\/问>/g, "$1").replace(/<答>([\s\S]*?)<\/答>/g, "$1");
491
+ }
492
+ /** 跳过空白(含换行),返回新位置。 */
493
+ function skipBlank(text, pos) {
494
+ while (pos < text.length && /\s/.test(text[pos])) pos += 1;
495
+ return pos;
496
+ }
497
+ /**
498
+ * 从消息文本开头解析协议前缀(回流块* → 注释块):任一命中即返回。
499
+ * 全部不命中返回 null(普通用户消息,零开销)。
500
+ * 注意:内容里恰好含字面 `</quote>` 会在该处截断解析(已知极限,聊天引用
501
+ * 场景可忽略;Codex 的 <external_KEY> 同款取舍)。
502
+ */
503
+ function splitProtocolPrefix(text) {
504
+ let pos = 0;
505
+ const reflows = [];
506
+ const annotations = [];
507
+ for (;;) {
508
+ REFLOW_RE.lastIndex = pos;
509
+ const m = REFLOW_RE.exec(text);
510
+ if (m === null) break;
511
+ reflows.push({
512
+ source: m[1] ?? "",
513
+ content: (m[3] ?? "").trim()
514
+ });
515
+ pos = skipBlank(text, REFLOW_RE.lastIndex);
516
+ }
517
+ const rest = text.slice(pos);
518
+ const headerMatch = /^([^\n]*)\n/.exec(rest);
519
+ if (headerMatch !== null && PROTOCOL_HEADER_RE.test(headerMatch[1] ?? "")) {
520
+ let cursor = pos + headerMatch[0].length;
521
+ for (;;) {
522
+ ANNOTATION_RE.lastIndex = cursor;
523
+ const m = ANNOTATION_RE.exec(text);
524
+ if (m === null) break;
525
+ annotations.push({
526
+ id: Number(m[1]),
527
+ quote: m[2] ?? "",
528
+ note: (m[3] ?? "").trim()
529
+ });
530
+ cursor = skipBlank(text, ANNOTATION_RE.lastIndex);
531
+ }
532
+ if (annotations.length > 0) pos = cursor;
533
+ else if (reflows.length === 0) return null;
534
+ } else if (reflows.length === 0) return null;
535
+ if (reflows.length === 0 && annotations.length === 0) return null;
536
+ return {
537
+ length: skipBlank(text, pos),
538
+ annotations,
539
+ reflows
540
+ };
541
+ }
542
+ //#endregion
320
543
  //#region src/client/sidechat/model.ts
321
544
  /** Tab 类型 id(better-sidebar 注册表键;带包前缀防冲突)。 */
322
545
  const SIDE_TAB_TYPE = "dsh-sidenote:side";
@@ -349,6 +572,7 @@ window.__ModuleLoader__.load({
349
572
  if (typeof raw.childId === "string" && raw.childId !== "") out.childId = raw.childId;
350
573
  if (typeof raw.parentSessionId === "string" && raw.parentSessionId !== "") out.parentSessionId = raw.parentSessionId;
351
574
  if (typeof raw.pendingDraft === "string" && raw.pendingDraft !== "") out.pendingDraft = raw.pendingDraft;
575
+ if (typeof raw.boundarySeq === "number" && Number.isInteger(raw.boundarySeq)) out.boundarySeq = raw.boundarySeq;
352
576
  return out;
353
577
  }
354
578
  /** 清除 meta.pendingDraft(其余字段原样保留)。 */
@@ -415,151 +639,46 @@ window.__ModuleLoader__.load({
415
639
  function appendDraftText(draft, text) {
416
640
  return draft.trim() === "" ? text : `${draft}\n${text}`;
417
641
  }
418
- /** 工具结果正文截断上限(面板是窄栏,超长输出不撑爆 DOM)。 */
419
- const TOOL_TEXT_LIMIT = 4e3;
420
- function truncateText(text, max) {
421
- return text.length <= max ? text : `${text.slice(0, max)}…`;
422
- }
423
- /** ContentBlock[] → 纯文本:text 块拼接;image 块降级占位;其余忽略。 */
424
- function contentTextOf(content) {
425
- if (!Array.isArray(content)) return "";
426
- const parts = [];
427
- for (const block of content) {
428
- if (typeof block !== "object" || block === null) continue;
429
- const b = block;
430
- if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
431
- else if (b.type === "image") parts.push(t("imagePlaceholder"));
432
- }
433
- return parts.join("\n");
434
- }
435
- function seqKey(prefix, node) {
436
- return `${prefix}:${typeof node.seq === "number" ? node.seq : "?"}`;
437
- }
438
- function assistantParts(blocks) {
439
- const texts = [];
440
- const reasonings = [];
441
- let hasToolCall = false;
442
- if (Array.isArray(blocks)) for (const block of blocks) {
443
- if (typeof block !== "object" || block === null) continue;
444
- const b = block;
445
- if (b.kind === "text" && typeof b.text === "string") texts.push(b.text);
446
- else if (b.kind === "reasoning" && typeof b.text === "string") reasonings.push(b.text);
447
- else if (b.kind === "tool-call") hasToolCall = true;
448
- }
449
- return {
450
- text: texts.join("\n\n"),
451
- reasoning: reasonings.join("\n\n"),
452
- hasToolCall
453
- };
454
- }
455
642
  /**
456
- * 单个 ConversationNode → ChatMessage;返回 null = 面板不渲染
457
- * (context 注入、未知面事件、已取消的重试等对读者无信息的节点)。
643
+ * 回流「问答成对」的数据源:为每条 assistant 消息配对它在回答的用户提问。
644
+ * 顺序扫描消息流,记录最近一条用户消息的可见文本(剥掉注释/回流协议前缀——
645
+ * fork 历史里的主会话消息可能携带),其后每条非流式 assistant 消息都配对到它;
646
+ * 中间的 tool/notice 节点不打断配对。此前没有用户提问(如 fork 历史结尾
647
+ * 恰是 assistant)则不配对,回流块退化为只有 <答>。
648
+ * 返回 Map:assistant 消息 key → 提问全文。
458
649
  */
459
- function nodeToMessage(node) {
460
- if (typeof node !== "object" || node === null) return null;
461
- const n = node;
462
- switch (n.kind) {
463
- case "user": return {
464
- key: seqKey("u", n),
465
- role: "user",
466
- text: contentTextOf(n.content)
467
- };
468
- case "steering": return {
469
- key: seqKey("s", n),
470
- role: "user",
471
- text: contentTextOf(n.content)
472
- };
473
- case "assistant": {
474
- const { text, reasoning, hasToolCall } = assistantParts(n.blocks);
475
- if (text === "" && reasoning === "" && hasToolCall) return null;
476
- return {
477
- key: seqKey("a", n),
478
- role: "assistant",
479
- text,
480
- ...reasoning !== "" ? { reasoning } : {},
481
- ...n.interrupted === true ? { interrupted: true } : {}
482
- };
483
- }
484
- case "tool-result": {
485
- const call = n.call;
486
- const toolName = typeof call?.name === "string" ? call.name : typeof n.callId === "string" ? n.callId : t("toolFallback");
487
- return {
488
- key: seqKey("t", n),
489
- role: "tool",
490
- toolName,
491
- text: truncateText(contentTextOf(n.content), TOOL_TEXT_LIMIT),
492
- ...n.isError === true ? { isError: true } : {}
493
- };
494
- }
495
- case "turn-error": return {
496
- key: seqKey("e", n),
497
- role: "error",
498
- text: typeof n.message === "string" ? n.message : t("unknownError")
499
- };
500
- case "model-retry":
501
- if (n.retryState === "cancelled") return null;
502
- return {
503
- key: seqKey("r", n),
504
- role: "notice",
505
- text: t(n.retryState === "started" ? "modelRetryStarted" : "modelRetryWaiting")
506
- };
507
- case "turn-max-tokens": return {
508
- key: seqKey("m", n),
509
- role: "notice",
510
- text: t("maxTokens")
511
- };
512
- case "command": {
513
- const name = typeof n.name === "string" && n.name !== "" ? n.name : t("commandNameFallback");
514
- const args = typeof n.args === "string" ? n.args.trimStart() : "";
515
- return {
516
- key: seqKey("c", n),
517
- role: "notice",
518
- text: t("runCommand", { cmd: `/${name}${args === "" ? "" : ` ${args}`}` })
519
- };
520
- }
521
- case "compaction": return {
522
- key: seqKey("k", n),
523
- role: "notice",
524
- text: t("compacted")
525
- };
526
- default: return null;
527
- }
650
+ function pairQuestions(messages) {
651
+ const pairs = /* @__PURE__ */ new Map();
652
+ let pending;
653
+ for (const message of messages) if (message.role === "user") {
654
+ const proto = splitProtocolPrefix(message.text);
655
+ const visible = (proto === null ? message.text : message.text.slice(proto.length)).trim();
656
+ pending = visible === "" ? void 0 : visible;
657
+ } else if (message.role === "assistant" && message.streaming !== true && pending !== void 0) pairs.set(message.key, pending);
658
+ return pairs;
528
659
  }
529
- /**
530
- * ConversationSnapshot → 渲染消息列表:终态节点 + 在途工具调用 + 流式部分。
531
- * 快照缺省(未绑定)时为空列表。
532
- */
533
- function transcriptOf(snapshot) {
534
- if (snapshot === void 0 || snapshot === null) return [];
535
- const out = [];
536
- for (const node of snapshot.nodes ?? []) {
537
- const message = nodeToMessage(node);
538
- if (message !== null) out.push(message);
539
- }
540
- if (Array.isArray(snapshot.runningCalls)) for (const call of snapshot.runningCalls) {
541
- if (typeof call !== "object" || call === null) continue;
542
- const c = call;
543
- out.push({
544
- key: `rc:${typeof c.callId === "string" ? c.callId : "?"}`,
545
- role: "tool",
546
- toolName: typeof c.name === "string" ? c.name : t("toolFallback"),
547
- text: "",
548
- streaming: true
549
- });
660
+ //#endregion
661
+ //#region src/client/sidechat/composer/attach.ts
662
+ /** 解析附件 API(缺失返回 null → 调用点不渲染附件入口)。 */
663
+ function resolveAttachmentApis(ctx) {
664
+ try {
665
+ const conv = ctx.get("conversation");
666
+ if (typeof conv?.createDraftImages !== "function" || typeof conv?.draftImages !== "function" || typeof conv?.releaseDraftImage !== "function") return null;
667
+ return conv;
668
+ } catch {
669
+ return null;
550
670
  }
551
- const partial = snapshot.partial;
552
- if (partial !== void 0 && partial !== null) {
553
- const { text, reasoning, hasToolCall } = assistantParts(partial.blocks);
554
- if (text !== "" || reasoning !== "" || !hasToolCall) out.push({
555
- key: "partial",
556
- role: "assistant",
557
- text,
558
- ...reasoning !== "" ? { reasoning } : {},
559
- streaming: true
560
- });
671
+ }
672
+ /** 选中文件 → 注册为草稿图片并挂进输入机器;返回是否成功挂上。 */
673
+ function attachFiles(apis, input, files) {
674
+ try {
675
+ const images = apis.createDraftImages(files);
676
+ if (images.length === 0) return false;
677
+ return input.addImages(images.map((i) => i.id));
678
+ } catch (error) {
679
+ console.warn("[dsh-sidenote] 附件注册失败:", error);
680
+ return false;
561
681
  }
562
- return out;
563
682
  }
564
683
  //#endregion
565
684
  //#region src/client/sidechat/composer.ts
@@ -614,10 +733,21 @@ window.__ModuleLoader__.load({
614
733
  */
615
734
  function useComposer(ctx, session, childId) {
616
735
  const input = (0, react.useMemo)(() => childId === void 0 ? null : resolveSessionInput(ctx, childId), [ctx, childId]);
617
- const machineDraft = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => input === null ? NOOP_UNSUBSCRIBE$1 : input.state.subscribe(notify), [input]), () => input === null ? "" : readInputDraft(input));
736
+ const machineState = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => input === null ? NOOP_UNSUBSCRIBE$1 : input.state.subscribe(notify), [input]), () => input === null ? null : input.state.getSnapshot());
737
+ const machineDraft = machineState?.draft ?? "";
618
738
  const [localDraft, setLocalDraft] = (0, react.useState)("");
619
739
  const [sendError, setSendError] = (0, react.useState)(null);
620
740
  const draft = input === null ? localDraft : machineDraft;
741
+ const apis = (0, react.useMemo)(() => resolveAttachmentApis(ctx), [ctx]);
742
+ const attachImages = (0, react.useCallback)((files) => {
743
+ if (input === null || apis === null) return false;
744
+ return attachFiles(apis, input, files);
745
+ }, [input, apis]);
746
+ const removeImage = (0, react.useCallback)((id) => {
747
+ if (input === null || apis === null) return;
748
+ input.removeImage(id);
749
+ apis.releaseDraftImage(id);
750
+ }, [input, apis]);
621
751
  const setDraft = (0, react.useCallback)((text) => {
622
752
  if (input === null) setLocalDraft(text);
623
753
  else input.setDraft(text);
@@ -626,11 +756,11 @@ window.__ModuleLoader__.load({
626
756
  if (input === null) setLocalDraft((d) => appendDraftText(d, text));
627
757
  else input.setDraft(appendDraftText(readInputDraft(input), text));
628
758
  }, [input]);
629
- const submit = (0, react.useCallback)(() => {
759
+ const submit = (0, react.useCallback)((mode) => {
630
760
  const text = draft.trim();
631
761
  if (text === "") return;
632
762
  if (input !== null) {
633
- input.submit();
763
+ input.submit(mode);
634
764
  return;
635
765
  }
636
766
  if (session === void 0) return;
@@ -639,11 +769,10 @@ window.__ModuleLoader__.load({
639
769
  session.prompt([{
640
770
  type: "text",
641
771
  text
642
- }], "queue").then((result) => {
643
- const r = result;
644
- if (r !== void 0 && r.ok === false) {
772
+ }], mode ?? "queue").then((result) => {
773
+ if (result.ok === false) {
645
774
  setLocalDraft((d) => d === "" ? text : d);
646
- setSendError(r.error?.message ?? "发送失败");
775
+ setSendError(result.error?.message ?? "发送失败");
647
776
  }
648
777
  }).catch((error) => {
649
778
  setLocalDraft((d) => d === "" ? text : d);
@@ -660,640 +789,2292 @@ window.__ModuleLoader__.load({
660
789
  sendError,
661
790
  setDraft,
662
791
  appendDraft,
663
- submit
792
+ submit,
793
+ phase: machineState?.phase,
794
+ draftRev: machineState?.draftRev,
795
+ imageIds: machineState?.imageIds ?? [],
796
+ imagePreviews: (() => {
797
+ const ids = machineState?.imageIds ?? [];
798
+ if (ids.length === 0 || apis === null) return [];
799
+ try {
800
+ return apis.draftImages(ids).map((i) => ({
801
+ id: i.id,
802
+ url: i.previewUrl
803
+ }));
804
+ } catch {
805
+ return [];
806
+ }
807
+ })(),
808
+ canAttach: input !== null && apis !== null,
809
+ attachImages,
810
+ removeImage
664
811
  };
665
812
  }
666
813
  //#endregion
667
- //#region src/client/sidechat/open.ts
668
- /** 从最新快照读一个 Tab(meta 合并写入前的读取面;布局即注册表)。 */
669
- function readTab(ctx, tabId) {
670
- return collectTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => tab.id === tabId);
814
+ //#region src/client/chat/cards.ts
815
+ /** ContentBlock[] → 纯文本(transcript.ts 同款的极简版,避免互依赖)。 */
816
+ function blocksText(content) {
817
+ if (!Array.isArray(content)) return void 0;
818
+ const parts = [];
819
+ for (const block of content) {
820
+ const b = block;
821
+ if (b?.type === "text" && typeof b.text === "string") parts.push(b.text);
822
+ }
823
+ return parts.length === 0 ? void 0 : parts.join("\n");
824
+ }
825
+ /** 未知 card 值的 warn-once 登记(T3 纪律:降级不静默)。 */
826
+ const warnedCards = /* @__PURE__ */ new Set();
827
+ function warnUnknownCard(value) {
828
+ if (warnedCards.has(value)) return;
829
+ warnedCards.add(value);
830
+ console.warn(`[dsh-sidenote] 未知工具卡种类 "${value}"——降级为 generic 卡(宿主可能扩展了 card union)`);
831
+ }
832
+ /** 相对 cwd 按会话工作区折成绝对路径(UI bridge 责任——presentation 注释明文)。 */
833
+ function resolveCwd(cwd, cwdBase) {
834
+ if (cwd === void 0) return cwdBase;
835
+ if (cwd.startsWith("/") || /^[A-Za-z]:[\\/]/.test(cwd)) return cwd;
836
+ if (cwdBase === void 0) return cwd;
837
+ return `${cwdBase.replace(/\/+$/, "")}/${cwd}`;
671
838
  }
672
839
  /**
673
- * 为 sessionId 新建或聚焦一个侧边聊天 Tab;draftText 给出时写入其草稿。
674
- * @returns false = 未能打开(调用方保持自己的流程)。
840
+ * 展示态路径缩短:>3 段的绝对路径只保末 3 段(≈仓库相对形态,与主区
841
+ * 「Read · dsh-sidenote/scripts/x.sh」同款)。窄栏标题位只够放语义尾部。
675
842
  */
676
- function openOrFocusSideChat(ctx, sessionId, draftText) {
677
- try {
678
- const snapshot = ctx.betterSidebar.getSnapshot();
679
- if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
680
- const existing = collectSideTabs(snapshot.state);
681
- if (existing.length > 0) {
682
- const target = existing[existing.length - 1];
683
- if (draftText !== void 0 && draftText !== "") {
684
- const meta = parseSideChatMeta(target.meta);
685
- const input = meta.childId === void 0 ? null : resolveSessionInput(ctx, meta.childId);
686
- if (input !== null) input.setDraft(appendDraftText(readInputDraft(input), draftText));
687
- else ctx.betterSidebar.updateTab(target.id, { meta: {
688
- ...meta,
689
- pendingDraft: appendDraftText(meta.pendingDraft ?? "", draftText)
690
- } });
691
- }
692
- ctx.betterSidebar.activateTab(target.id, { sessionId });
693
- return true;
694
- }
695
- return createSideChat(ctx, sessionId, draftText);
696
- } catch (error) {
697
- console.warn("[dsh-sidenote] 打开侧边聊天失败:", error);
698
- return false;
699
- }
843
+ function shortPath(p) {
844
+ if (!p.startsWith("/")) return p;
845
+ const segs = p.split("/").filter((s) => s !== "");
846
+ return segs.length > 3 ? segs.slice(-3).join("/") : p;
700
847
  }
701
848
  /**
702
- * 无条件新建一个侧边聊天 Tab(/side「新建侧边聊天」选项的语义——既有实例
703
- * 在弹层里另有聚焦项,「新建」必须真的新建,否则标签与行为不符)。
704
- * @returns false = 未能创建(调用方保持自己的流程)。
849
+ * 标题整形:「Tool /abs/path」→「Tool · 末3段」(主区同名同款);
850
+ * 裸绝对路径标题 → 末 3 段;不含路径的标题原样。
705
851
  */
706
- function createSideChat(ctx, sessionId, draftText) {
707
- try {
708
- const snapshot = ctx.betterSidebar.getSnapshot();
709
- if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
710
- const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
711
- ctx.betterSidebar.openTab({ type: SIDE_TAB_TYPE }, { sessionId });
712
- const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
713
- if (created === void 0) return false;
714
- if (draftText !== void 0 && draftText !== "") ctx.betterSidebar.updateTab(created.id, { meta: { pendingDraft: draftText } });
715
- return true;
716
- } catch (error) {
717
- console.warn("[dsh-sidenote] 新建侧边聊天失败:", error);
718
- return false;
852
+ function displayTitle(title) {
853
+ const m = /^([A-Za-z][\w-]*) (\/.+)$/.exec(title);
854
+ const name = m?.[1];
855
+ const path = m?.[2];
856
+ if (name !== void 0 && path !== void 0) return `${name} · ${shortPath(path)}`;
857
+ return title.startsWith("/") ? shortPath(title) : title;
858
+ }
859
+ /** todo_write 入参形状校验(字段不齐即放弃 todo 卡)。两种数据源形状:
860
+ * 0.1.1 wire:callView.rawInput = todos 数组本身(dsh-tool-todo presentCall 实证);
861
+ * 0.1.2 argsRaw:{ todos: [...] }。 */
862
+ function todoItemsOf(raw) {
863
+ const list = Array.isArray(raw) ? raw : typeof raw === "object" && raw !== null ? raw.todos : void 0;
864
+ if (!Array.isArray(list) || list.length === 0) return void 0;
865
+ const items = [];
866
+ for (const it of list) {
867
+ const o = it;
868
+ if (typeof o?.content !== "string" || typeof o?.status !== "string") return void 0;
869
+ items.push({
870
+ content: o.content,
871
+ status: o.status
872
+ });
719
873
  }
874
+ return items;
875
+ }
876
+ /** todo 卡标题:任务 · 非零状态组计数(主区「任务 · N 已完成 · …」同款)。 */
877
+ function todoTitleOf(items) {
878
+ let done = 0;
879
+ let doing = 0;
880
+ let pending = 0;
881
+ for (const it of items) if (it.status === "completed") done += 1;
882
+ else if (it.status === "in_progress") doing += 1;
883
+ else pending += 1;
884
+ const parts = [];
885
+ if (done > 0) parts.push(t("todoDone", { n: done }));
886
+ if (doing > 0) parts.push(t("todoDoing", { n: doing }));
887
+ if (pending > 0) parts.push(t("todoPending", { n: pending }));
888
+ return parts.length === 0 ? t("todoTitle") : `${t("todoTitle")} · ${parts.join(" · ")}`;
720
889
  }
721
890
  /**
722
- * 桥接/命令的目标预览:askInSideChat 此时若执行会落在哪个 Tab 的标题
723
- * (既有并存 = 最后一个;无 = 即将新建的首个标题)。用于编辑器上标明
724
- * 「发送至:侧边 N」。会话不在屏上/状态缺失时返回 undefined。
891
+ * callView + resultView → 一张卡的视图模型。三层判别全在这里;
892
+ * 结果态优先(title 等字段 result 覆盖 call),缺省逐字段回退。
725
893
  */
726
- function sideChatTargetTitle(ctx, sessionId) {
727
- try {
728
- const snapshot = ctx.betterSidebar.getSnapshot();
729
- if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return void 0;
730
- const existing = collectSideTabs(snapshot.state);
731
- if (existing.length > 0) return existing[existing.length - 1].title;
732
- return sideTabTitle([]);
733
- } catch {
734
- return;
894
+ function cardModelOf(input) {
895
+ const { callView, resultView } = input;
896
+ const call = callView ?? void 0;
897
+ const result = resultView ?? void 0;
898
+ const card = result?.card ?? call?.card ?? "generic";
899
+ switch (card) {
900
+ case "terminal": {
901
+ const rc = result?.card === "terminal" ? result : void 0;
902
+ const cc = call?.card === "terminal" ? call : void 0;
903
+ const command = rc?.title ?? cc?.title ?? input.toolName;
904
+ return {
905
+ kind: "terminal",
906
+ title: cc?.description !== void 0 ? `${displayToolName(input.toolName)} · ${cc.description}` : command,
907
+ command,
908
+ ...cc?.description !== void 0 ? { description: cc.description } : {},
909
+ ...resolveCwd(cc?.cwd, input.cwdBase) !== void 0 ? { cwd: resolveCwd(cc?.cwd, input.cwdBase) } : {},
910
+ ...rc?.output !== void 0 ? { output: rc.output } : {},
911
+ ...rc?.exitCode !== void 0 ? { exitCode: rc.exitCode } : {},
912
+ ...rc?.signal !== void 0 ? { signal: rc.signal } : {}
913
+ };
914
+ }
915
+ case "diff": {
916
+ const rc = result?.card === "diff" ? result : void 0;
917
+ const cc = call?.card === "diff" ? call : void 0;
918
+ return {
919
+ kind: "diff",
920
+ title: displayTitle(rc?.title ?? cc?.title ?? input.toolName),
921
+ diffs: rc?.diffs ?? cc?.diffs ?? [],
922
+ ...cc?.locations !== void 0 ? { locations: cc.locations } : {}
923
+ };
924
+ }
925
+ case "search": {
926
+ const rc = result?.card === "search" ? result : void 0;
927
+ if (rc === void 0) return genericModel(input, call, result);
928
+ if (rc.shape === "matches") return {
929
+ kind: "search",
930
+ title: rc.title ?? call?.title ?? input.toolName,
931
+ shape: "matches",
932
+ files: rc.files,
933
+ truncated: rc.truncated,
934
+ total: rc.total
935
+ };
936
+ if (rc.shape === "paths") return {
937
+ kind: "search",
938
+ title: rc.title ?? call?.title ?? input.toolName,
939
+ shape: "paths",
940
+ paths: rc.paths,
941
+ truncated: rc.truncated,
942
+ total: rc.total
943
+ };
944
+ warnUnknownCard(`search/${String(rc.shape)}`);
945
+ return genericModel(input, call, result);
946
+ }
947
+ case "read": {
948
+ const rc = result?.card === "read" ? result : void 0;
949
+ if (rc === void 0) return genericModel(input, call, result);
950
+ return {
951
+ kind: "read",
952
+ title: `Read · ${shortPath(rc.path)}`,
953
+ path: rc.path,
954
+ lines: rc.lines,
955
+ totalLines: rc.totalLines,
956
+ ...rc.lang !== void 0 ? { lang: rc.lang } : {}
957
+ };
958
+ }
959
+ case "web": {
960
+ const rc = result?.card === "web" ? result : void 0;
961
+ if (rc === void 0) return genericModel(input, call, result);
962
+ const base = {
963
+ kind: "web",
964
+ title: rc.title ?? call?.title ?? input.toolName
965
+ };
966
+ if (rc.kind === "search") {
967
+ const rs = rc;
968
+ return {
969
+ ...base,
970
+ webKind: "search",
971
+ sources: rs.sources,
972
+ truncated: rs.truncated,
973
+ ...rs.answer !== void 0 ? { answer: rs.answer } : {}
974
+ };
975
+ }
976
+ if (rc.kind === "fetch") {
977
+ const rf = rc;
978
+ return {
979
+ ...base,
980
+ webKind: "fetch",
981
+ url: rf.url,
982
+ statusCode: rf.statusCode,
983
+ truncated: rf.truncated
984
+ };
985
+ }
986
+ warnUnknownCard(`web/${String(rc.kind)}`);
987
+ return genericModel(input, call, result);
988
+ }
989
+ case "generic": return genericModel(input, call, result);
990
+ default:
991
+ warnUnknownCard(String(card));
992
+ return genericModel(input, call, result);
735
993
  }
736
994
  }
737
- //#endregion
738
- //#region src/client/annotate/format.ts
739
- /** 协议头(双语):zh「我批注了以下 2 处内容:」/ en 见 locales。 */
740
- function protocolHeader(n) {
741
- return t("protocolHeader", { n });
995
+ function genericModel(input, call, result) {
996
+ const gc = call?.card === "generic" ? call : void 0;
997
+ const gr = result?.card === "generic" ? result : void 0;
998
+ if (input.toolName === "todo_write") {
999
+ const items = todoItemsOf(gc?.rawInput);
1000
+ if (items !== void 0) return {
1001
+ kind: "todo",
1002
+ title: todoTitleOf(items),
1003
+ items
1004
+ };
1005
+ }
1006
+ return {
1007
+ kind: "generic",
1008
+ title: displayTitle(gr?.title ?? gc?.title ?? input.toolName),
1009
+ icon: gc?.kind ?? "other",
1010
+ ...gc?.rawInput !== void 0 ? { rawInput: gc.rawInput } : {},
1011
+ bodyText: blocksText(gr?.content) ?? blocksText(gc?.content) ?? input.rawText,
1012
+ ...gc?.locations !== void 0 ? { locations: gc.locations } : {}
1013
+ };
742
1014
  }
743
- /** One annotation's XML block(无注解时省略 <note>)。 */
744
- function formatAnnotationXml(index, item) {
745
- const id = item.number ?? index;
746
- const note = item.note.trim();
747
- return note === "" ? `<annotation id="${id}">\n<quote>${item.text}</quote>\n</annotation>` : `<annotation id="${id}">\n<quote>${item.text}</quote>\n<note>${note}</note>\n</annotation>`;
1015
+ /** 工具机器名 → 展示名(主区「Bash · …」的同款首字母大写)。 */
1016
+ function displayToolName(name) {
1017
+ return name.charAt(0).toUpperCase() + name.slice(1);
748
1018
  }
749
- /** 注释协议块 = 头部句 + 每个注释一个 XML 块(行间无空行,与正文空一行)。 */
750
- function buildProtocolBlock(items) {
751
- return [protocolHeader(items.length), ...items.map((item, i) => formatAnnotationXml(i + 1, item))].join("\n");
1019
+ /** 结果正文尾标解析(宿主 parseExitStatus 的线格式)。 */
1020
+ function parseExitStatus(text) {
1021
+ const m = /\n\[(?:exit code: (\d+)|killed by signal: ([A-Z]+))\]\s*$/.exec(text);
1022
+ if (m === null) return { output: text };
1023
+ const output = text.slice(0, m.index).replace(/\n+$/, "");
1024
+ if (m[1] !== void 0) return {
1025
+ output,
1026
+ exitCode: Number(m[1])
1027
+ };
1028
+ return {
1029
+ output,
1030
+ signal: m[2]
1031
+ };
752
1032
  }
753
- /** 「在侧边聊天中提问」的种子引用(进侧边草稿,不是协议块——保持轻量)。 */
754
- function buildSideChatQuote(text, note = "") {
755
- return `${text.split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n")}\n${note.trim() === "" ? t("noNote") : t("noteLine", { note })}`;
1033
+ /** FsReadMeta 形状+语义校验(readMetaFromMeta 的忠实子集)。 */
1034
+ function readMetaOf(meta) {
1035
+ if (typeof meta !== "object" || meta === null) return void 0;
1036
+ const m = meta;
1037
+ if (typeof m.path !== "string" || !Array.isArray(m.lines) || typeof m.totalLines !== "number") return void 0;
1038
+ if (!Number.isInteger(m.totalLines) || m.totalLines < 0) return void 0;
1039
+ let prev = 0;
1040
+ const lines = [];
1041
+ for (const raw of m.lines) {
1042
+ const l = raw;
1043
+ if (typeof l?.number !== "number" || typeof l?.text !== "string") return void 0;
1044
+ if (!Number.isInteger(l.number) || l.number <= prev || l.number > m.totalLines) return void 0;
1045
+ prev = l.number;
1046
+ lines.push({
1047
+ number: l.number,
1048
+ text: l.text
1049
+ });
1050
+ }
1051
+ return {
1052
+ path: m.path,
1053
+ lines,
1054
+ totalLines: m.totalLines,
1055
+ ...typeof m.lang === "string" ? { lang: m.lang } : {}
1056
+ };
756
1057
  }
757
- /** 协议头识别(zh/en 都认):整行匹配。 */
758
- const PROTOCOL_HEADER_RE = /^(?:我批注了以下 (\d+) 处内容:|I annotated (\d+) passage\(s\) of the conversation above:)$/;
759
- const ANNOTATION_RE = /<annotation id="(\d+)">\s*<quote>([\s\S]*?)<\/quote>\s*(?:<note>([\s\S]*?)<\/note>\s*)?<\/annotation>/y;
760
- const REFLOW_RE = /<reflow\s+source="([^"]*)"(?:\s+reason="([^"]*)")?\s*>([\s\S]*?)<\/reflow>/y;
761
- /** 跳过空白(含换行),返回新位置。 */
762
- function skipBlank(text, pos) {
763
- while (pos < text.length && /\s/.test(text[pos])) pos += 1;
764
- return pos;
1058
+ /** argsRaw(JSON 字符串)里尽力提取展示字段。 */
1059
+ function argsSummary(argsRaw) {
1060
+ if (argsRaw === void 0 || argsRaw === "") return {};
1061
+ try {
1062
+ const parsed = JSON.parse(argsRaw);
1063
+ if (typeof parsed !== "object" || parsed === null) return {};
1064
+ const a = parsed;
1065
+ return {
1066
+ ...typeof a.command === "string" ? { command: a.command } : {},
1067
+ ...typeof a.description === "string" ? { description: a.description } : {},
1068
+ ...typeof a.file_path === "string" ? { path: a.file_path } : typeof a.path === "string" ? { path: a.path } : {},
1069
+ ...Array.isArray(a.todos) ? { todos: a.todos } : {}
1070
+ };
1071
+ } catch {
1072
+ return {};
1073
+ }
765
1074
  }
766
1075
  /**
767
- * 从消息文本开头解析协议前缀(回流块* → 注释块):任一命中即返回。
768
- * 全部不命中返回 null(普通用户消息,零开销)。
769
- * 注意:内容里恰好含字面 `</quote>` 会在该处截断解析(已知极限,聊天引用
770
- * 场景可忽略;Codex 的 <external_KEY> 同款取舍)。
1076
+ * 0.1.2 推导路径:节点原始字段 → ToolCardModel。与 cardModelOf(0.1.1 wire
1077
+ * 面)平级;选择点在 transcript 层(节点带不带 callView 一眼可分)。
771
1078
  */
772
- function splitProtocolPrefix(text) {
773
- let pos = 0;
774
- const reflows = [];
775
- const annotations = [];
776
- for (;;) {
777
- REFLOW_RE.lastIndex = pos;
778
- const m = REFLOW_RE.exec(text);
779
- if (m === null) break;
780
- reflows.push({
781
- source: m[1] ?? "",
782
- content: (m[3] ?? "").trim()
783
- });
784
- pos = skipBlank(text, REFLOW_RE.lastIndex);
1079
+ function cardModelFromNode(input) {
1080
+ const args = argsSummary(input.argsRaw);
1081
+ const name = input.name;
1082
+ if (name === "bash" || name === "bash-persistent" || name === "pwsh" || name === "pwsh-persistent") {
1083
+ const { output, exitCode, signal } = parseExitStatus(input.rawText);
1084
+ const command = args.command ?? name;
1085
+ return {
1086
+ kind: "terminal",
1087
+ title: args.description !== void 0 ? `${displayToolName(name)} · ${args.description}` : command,
1088
+ command,
1089
+ ...args.description !== void 0 ? { description: args.description } : {},
1090
+ output,
1091
+ ...exitCode !== void 0 ? { exitCode } : {},
1092
+ ...signal !== void 0 ? { signal } : {}
1093
+ };
1094
+ }
1095
+ if (name === "read") {
1096
+ const meta = readMetaOf(input.meta);
1097
+ if (meta !== void 0) return {
1098
+ kind: "read",
1099
+ title: `Read · ${shortPath(meta.path)}`,
1100
+ path: meta.path,
1101
+ lines: meta.lines,
1102
+ totalLines: meta.totalLines,
1103
+ ...meta.lang !== void 0 ? { lang: meta.lang } : {}
1104
+ };
1105
+ }
1106
+ if (name === "todo_write") {
1107
+ const items = todoItemsOf(args.todos);
1108
+ if (items !== void 0) return {
1109
+ kind: "todo",
1110
+ title: todoTitleOf(items),
1111
+ items
1112
+ };
785
1113
  }
786
- const rest = text.slice(pos);
787
- const headerMatch = /^([^\n]*)\n/.exec(rest);
788
- if (headerMatch !== null && PROTOCOL_HEADER_RE.test(headerMatch[1] ?? "")) {
789
- let cursor = pos + headerMatch[0].length;
790
- for (;;) {
791
- ANNOTATION_RE.lastIndex = cursor;
792
- const m = ANNOTATION_RE.exec(text);
793
- if (m === null) break;
794
- annotations.push({
795
- id: Number(m[1]),
796
- quote: m[2] ?? "",
797
- note: (m[3] ?? "").trim()
798
- });
799
- cursor = skipBlank(text, ANNOTATION_RE.lastIndex);
800
- }
801
- if (annotations.length > 0) pos = cursor;
802
- else if (reflows.length === 0) return null;
803
- } else if (reflows.length === 0) return null;
804
- if (reflows.length === 0 && annotations.length === 0) return null;
805
1114
  return {
806
- length: skipBlank(text, pos),
807
- annotations,
808
- reflows
1115
+ kind: "generic",
1116
+ title: args.path !== void 0 ? `${name} · ${shortPath(args.path)}` : args.command ?? name,
1117
+ icon: KIND_BY_NAME[name] ?? "other",
1118
+ bodyText: input.rawText === "" ? void 0 : input.rawText
809
1119
  };
810
1120
  }
811
- //#endregion
812
- //#region \0dsh-css:/home/runner/work/dsh-sidenote/dsh-sidenote/src/client/sidechat/sidechat.module.css.mjs
813
- const css$1 = "._5KEEsG_root{height:100%;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:13px;display:flex}._5KEEsG_body{flex-direction:column;flex:1;padding:12px 10px;display:flex;overflow-y:auto}._5KEEsG_transcript{flex-direction:column;gap:12px;display:flex}._5KEEsG_empty,._5KEEsG_stateScreen{text-align:center;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px 16px;display:flex}._5KEEsG_emptyIcon{color:var(--dsw-alias-label-tertiary)}._5KEEsG_emptyTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600}._5KEEsG_emptyText{color:var(--dsw-alias-label-tertiary);max-width:240px;font-size:12px;line-height:1.6}._5KEEsG_stateDetail{color:var(--dsw-alias-state-error-primary);word-break:break-word;max-width:260px;font-size:12px;line-height:1.6}._5KEEsG_userRow{justify-content:flex-end;display:flex}._5KEEsG_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}._5KEEsG_assistantRow{justify-content:flex-start;display:flex;position:relative}._5KEEsG_rowActions{opacity:0;z-index:2;transition:opacity .12s ease-out;position:absolute;top:-8px;right:0}._5KEEsG_assistantRow:hover ._5KEEsG_rowActions{opacity:1}._5KEEsG_reflowButton{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-bg-layer-1,#fff);width:22px;height:22px;color:var(--dsw-alias-label-tertiary,#8f959e);cursor:pointer;border-radius:999px;place-items:center;padding:0;display:grid;box-shadow:0 2px 8px #00000014}._5KEEsG_reflowButton:hover{color:var(--dsw-alias-state-business-primary,#2f88ff);border-color:var(--dsw-alias-state-business-primary,#2f88ff)}._5KEEsG_assistantBody{word-break:break-word;max-width:100%;font-size:13px;line-height:1.6}._5KEEsG_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}._5KEEsG_reasoning>summary{cursor:pointer;user-select:none}._5KEEsG_reasoningBody{white-space:pre-wrap;word-break:break-word;margin-top:4px}._5KEEsG_streamingHint{color:var(--dsw-alias-label-dimmed)}._5KEEsG_toolCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;overflow:hidden}._5KEEsG_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}._5KEEsG_toolError{color:var(--dsw-alias-state-error-primary)}._5KEEsG_toolRunning{color:var(--dsw-alias-label-tertiary)}._5KEEsG_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}._5KEEsG_errorRow{color:var(--dsw-alias-state-error-primary);white-space:pre-wrap;padding:4px 2px;font-size:12px}._5KEEsG_noticeRow{color:var(--dsw-alias-label-dimmed);text-align:center;padding:2px 0;font-size:12px}._5KEEsG_composer{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:6px;padding:8px 10px;display:flex}._5KEEsG_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}._5KEEsG_input:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}._5KEEsG_composerFoot{justify-content:space-between;align-items:center;gap:8px;display:flex}._5KEEsG_modelLabel{color:var(--dsw-alias-label-dimmed);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}._5KEEsG_sendButton,._5KEEsG_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}._5KEEsG_sendButton:disabled{opacity:.55;cursor:default}._5KEEsG_headerSide{border:1px solid var(--dsw-alias-border-l1,#0000001a);color:var(--dsw-alias-label-secondary,#3f454d);cursor:pointer;user-select:none;background:0 0;border-radius:999px;align-items:center;gap:5px;padding:3px 10px;font-size:12px;line-height:18px;display:inline-flex}._5KEEsG_headerSide:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-primary,#1f2329)}._5KEEsG_sentChipRow{align-items:center;gap:6px;margin-right:8px;display:inline-flex}._5KEEsG_sentChip{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-secondary,#3f454d);user-select:none;border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;line-height:16px;display:inline-flex}";
814
- const tagId$1 = "dsh-sidenote/sidechat.module.css";
815
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
816
- const tag = document.createElement("style");
817
- tag.dataset.plugin = "dsh-sidenote";
818
- tag.dataset.pluginCss = tagId$1;
819
- tag.textContent = css$1;
820
- document.head.appendChild(tag);
821
- }
822
- var sidechat_module_css_default = {
823
- "empty": "_5KEEsG_empty",
824
- "assistantBody": "_5KEEsG_assistantBody",
825
- "root": "_5KEEsG_root",
826
- "headerSide": "_5KEEsG_headerSide",
827
- "sentChipRow": "_5KEEsG_sentChipRow",
828
- "toolRunning": "_5KEEsG_toolRunning",
829
- "errorRow": "_5KEEsG_errorRow",
830
- "composer": "_5KEEsG_composer",
831
- "toolCard": "_5KEEsG_toolCard",
832
- "reflowButton": "_5KEEsG_reflowButton",
833
- "sendButton": "_5KEEsG_sendButton",
834
- "reasoning": "_5KEEsG_reasoning",
835
- "rowActions": "_5KEEsG_rowActions",
836
- "emptyIcon": "_5KEEsG_emptyIcon",
837
- "input": "_5KEEsG_input",
838
- "streamingHint": "_5KEEsG_streamingHint",
839
- "stopButton": "_5KEEsG_stopButton",
840
- "toolError": "_5KEEsG_toolError",
841
- "body": "_5KEEsG_body",
842
- "composerFoot": "_5KEEsG_composerFoot",
843
- "sentChip": "_5KEEsG_sentChip",
844
- "modelLabel": "_5KEEsG_modelLabel",
845
- "transcript": "_5KEEsG_transcript",
846
- "noticeRow": "_5KEEsG_noticeRow",
847
- "toolHead": "_5KEEsG_toolHead",
848
- "assistantRow": "_5KEEsG_assistantRow",
849
- "toolBody": "_5KEEsG_toolBody",
850
- "emptyTitle": "_5KEEsG_emptyTitle",
851
- "reasoningBody": "_5KEEsG_reasoningBody",
852
- "stateDetail": "_5KEEsG_stateDetail",
853
- "stateScreen": "_5KEEsG_stateScreen",
854
- "emptyText": "_5KEEsG_emptyText",
855
- "userRow": "_5KEEsG_userRow",
856
- "userBubble": "_5KEEsG_userBubble"
1121
+ /** 工具名 → 图标类别的静态映射(0.1.2 没有 callView.kind 可用)。 */
1122
+ const KIND_BY_NAME = {
1123
+ read: "read",
1124
+ write: "edit",
1125
+ edit: "edit",
1126
+ "str-replace-editor": "edit",
1127
+ bash: "execute",
1128
+ "bash-persistent": "execute",
1129
+ pwsh: "execute",
1130
+ "pwsh-persistent": "execute",
1131
+ grep: "search",
1132
+ glob: "search",
1133
+ web_search: "fetch",
1134
+ web_fetch: "fetch"
857
1135
  };
858
1136
  //#endregion
859
- //#region src/client/sidechat/SideChatPanel.tsx
860
- /**
861
- * 侧边聊天 Tab 面板:fork 编排(首开)/ 绑定恢复(刷新后)/ 消息流 / composer。
862
- *
863
- * 打开流程(design.md 详细方案 2):组件挂载时 tab.meta 无 childId →
864
- * ctx.sessions.fork({ sessionId: scope.sessionId })(fork 时刻全量历史快照)
865
- * → ctx.workspaces.archiveSession(childId)(durable 隐藏出会话列表)
866
- * → ctx.betterSidebar.updateTab(tab.id, { meta: { childId, parentSessionId } })
867
- * (Tab meta 即注册表,随布局持久化,刷新/重启后恢复)。
868
- * fork 失败(blank 会话无已完成 turn 等)→ 中文错误态 + 关闭指引,不崩页面。
869
- *
870
- * 恢复流程:有 meta.childId → ctx.sessions.binding(childId) 直接绑定;
871
- * 列表就绪后仍不在列 → 「会话已不存在」态。
872
- *
873
- * 消息流:binding.session 快照订阅(useSyncExternalStore),visible=false 时
874
- * 暂停订阅。已知偏差(记录于此):client-runtime 只为 staged(当前选中)会话
875
- * 打开事件窗口,非 staged 会话 openState 停留 'cold' 且 acceptLiveEvent 丢弃
876
- * 事件 —— 因此绑定后对该会话调一次 concrete Session 的 open()(off-face、
877
- * feature-check、幂等),否则消息流永远为空。
878
- */
879
- /** MarkdownText 代码块复制按钮文案(跟随 DSH 语言;函数内读取保持引用稳定)。 */
880
- function codeLabels() {
1137
+ //#region src/client/chat/transcript.ts
1138
+ /** 工具结果正文截断上限(面板是窄栏,超长输出不撑爆 DOM)。 */
1139
+ const TOOL_TEXT_LIMIT = 4e3;
1140
+ function truncateText(text, max) {
1141
+ return text.length <= max ? text : `${text.slice(0, max)}…`;
1142
+ }
1143
+ /** ContentBlock[] → 纯文本:text 块拼接;image 块降级占位;其余忽略。 */
1144
+ function contentTextOf(content) {
1145
+ if (!Array.isArray(content)) return "";
1146
+ const parts = [];
1147
+ for (const block of content) {
1148
+ if (typeof block !== "object" || block === null) continue;
1149
+ const b = block;
1150
+ if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
1151
+ else if (b.type === "image") parts.push(t("imagePlaceholder"));
1152
+ }
1153
+ return parts.join("\n");
1154
+ }
1155
+ function seqKey(prefix, node) {
1156
+ return `${prefix}:${typeof node.seq === "number" ? node.seq : "?"}`;
1157
+ }
1158
+ /** 节点 seq 随行(D1 折叠边界用;缺省不带)。 */
1159
+ function seqOf(node) {
1160
+ return typeof node.seq === "number" ? { seq: node.seq } : {};
1161
+ }
1162
+ function assistantParts(blocks) {
1163
+ const texts = [];
1164
+ const reasonings = [];
1165
+ let hasToolCall = false;
1166
+ if (Array.isArray(blocks)) for (const block of blocks) {
1167
+ if (typeof block !== "object" || block === null) continue;
1168
+ const b = block;
1169
+ if (b.kind === "text" && typeof b.text === "string") texts.push(b.text);
1170
+ else if (b.kind === "reasoning" && typeof b.text === "string") reasonings.push(b.text);
1171
+ else if (b.kind === "tool-call") hasToolCall = true;
1172
+ }
881
1173
  return {
882
- copyLabel: t("codeCopy"),
883
- copiedLabel: t("codeCopied")
1174
+ text: texts.join("\n\n"),
1175
+ reasoning: reasonings.join("\n\n"),
1176
+ hasToolCall
884
1177
  };
885
1178
  }
886
- const NOOP_UNSUBSCRIBE = () => {};
887
1179
  /**
888
- * 打开非 staged 会话的事件窗口(off-face:open() 在 concrete Session 上是
889
- * public 且幂等,但不在 SessionFace 契约上 —— 契约面只有 staged 会话会被
890
- * 运行时自动 open)。feature-check + 吞错,运行时若移除则降级为只发不收。
1180
+ * 单个 ConversationNode → ChatMessage;返回 null = 面板不渲染
1181
+ * (context 注入、未知面事件、已取消的重试等对读者无信息的节点)。
891
1182
  */
892
- function openSessionWindow(session) {
893
- const openable = session;
894
- if (typeof openable?.open !== "function") return;
895
- openable.open().catch((error) => {
896
- console.warn("[dsh-sidenote] 会话窗口打开失败:", error);
897
- });
898
- }
899
- function SideChatPanel(props) {
900
- useLocaleTick();
901
- const { ctx, scope, tab, visible } = props;
902
- const meta = parseSideChatMeta(tab.meta);
903
- const childId = meta.childId;
904
- const [forkError, setForkError] = (0, react.useState)(null);
905
- const forkStarted = (0, react.useRef)(false);
906
- const { store } = props;
907
- (0, react.useEffect)(() => {
908
- const mutable = store;
909
- try {
910
- mutable?.update?.((state) => {
911
- if (state.panelOpen === false) state.panelOpen = true;
1183
+ function nodeToMessage(node) {
1184
+ if (typeof node !== "object" || node === null) return null;
1185
+ const n = node;
1186
+ switch (n.kind) {
1187
+ case "user": return {
1188
+ key: seqKey("u", n),
1189
+ ...seqOf(n),
1190
+ role: "user",
1191
+ text: contentTextOf(n.content)
1192
+ };
1193
+ case "steering": return {
1194
+ key: seqKey("s", n),
1195
+ ...seqOf(n),
1196
+ role: "user",
1197
+ text: contentTextOf(n.content)
1198
+ };
1199
+ case "assistant": {
1200
+ const { text, reasoning, hasToolCall } = assistantParts(n.blocks);
1201
+ if (text === "" && reasoning === "" && hasToolCall) return null;
1202
+ return {
1203
+ key: seqKey("a", n),
1204
+ ...seqOf(n),
1205
+ role: "assistant",
1206
+ text,
1207
+ ...reasoning !== "" ? { reasoning } : {},
1208
+ ...n.interrupted === true ? { interrupted: true } : {}
1209
+ };
1210
+ }
1211
+ case "tool-result": {
1212
+ const call = n.call;
1213
+ const toolName = typeof call?.name === "string" ? call.name : typeof n.callId === "string" ? n.callId : t("toolFallback");
1214
+ const text = truncateText(contentTextOf(n.content), TOOL_TEXT_LIMIT);
1215
+ const card = (n.callView ?? n.resultView) != null ? cardModelOf({
1216
+ toolName,
1217
+ callView: n.callView,
1218
+ resultView: n.resultView,
1219
+ rawText: text
1220
+ }) : cardModelFromNode({
1221
+ name: toolName,
1222
+ argsRaw: typeof call?.argsRaw === "string" ? call.argsRaw : void 0,
1223
+ meta: n.meta,
1224
+ rawText: text
1225
+ });
1226
+ return {
1227
+ key: seqKey("t", n),
1228
+ ...seqOf(n),
1229
+ role: "tool",
1230
+ toolName,
1231
+ text,
1232
+ card,
1233
+ ...n.isError === true ? { isError: true } : {}
1234
+ };
1235
+ }
1236
+ case "turn-error": return {
1237
+ key: seqKey("e", n),
1238
+ ...seqOf(n),
1239
+ role: "error",
1240
+ text: typeof n.message === "string" ? n.message : t("unknownError")
1241
+ };
1242
+ case "model-retry":
1243
+ if (n.retryState === "cancelled") return null;
1244
+ return {
1245
+ key: seqKey("r", n),
1246
+ ...seqOf(n),
1247
+ role: "notice",
1248
+ text: t(n.retryState === "started" ? "modelRetryStarted" : "modelRetryWaiting")
1249
+ };
1250
+ case "turn-max-tokens": return {
1251
+ key: seqKey("m", n),
1252
+ ...seqOf(n),
1253
+ role: "notice",
1254
+ text: t("maxTokens")
1255
+ };
1256
+ case "command": {
1257
+ const name = typeof n.name === "string" && n.name !== "" ? n.name : t("commandNameFallback");
1258
+ const args = typeof n.args === "string" ? n.args.trimStart() : "";
1259
+ return {
1260
+ key: seqKey("c", n),
1261
+ ...seqOf(n),
1262
+ role: "notice",
1263
+ text: t("runCommand", { cmd: `/${name}${args === "" ? "" : ` ${args}`}` })
1264
+ };
1265
+ }
1266
+ case "compaction": return {
1267
+ key: seqKey("k", n),
1268
+ ...seqOf(n),
1269
+ role: "notice",
1270
+ text: t("compacted")
1271
+ };
1272
+ default: return null;
1273
+ }
1274
+ }
1275
+ /**
1276
+ * ConversationSnapshot → 渲染消息列表:终态节点 + 在途项(按 turn/step 归并)。
1277
+ * 快照缺省(未绑定)时为空列表。
1278
+ *
1279
+ * 时序归并(WI-01):在途项不再恒追加尾部——真实交错是「partial 文本 → 它
1280
+ * 自己 step 发出的工具卡」:partial 与同 step 的 runningCall 并存时必须文本
1281
+ * 在前(原文早于调用),不同 step 按 (turn, step) 升序;并行多 call 保持
1282
+ * 宿主 dispatch 序(同 turn/step 内稳定)。终态节点自带 seq 全序,不参与
1283
+ * 归并(在途项永远属于当前 turn 的前沿,尾部插入点天然正确)。
1284
+ */
1285
+ function transcriptOf(snapshot) {
1286
+ if (snapshot === void 0 || snapshot === null) return [];
1287
+ const out = [];
1288
+ for (const node of snapshot.nodes ?? []) {
1289
+ const message = nodeToMessage(node);
1290
+ if (message !== null) out.push(message);
1291
+ }
1292
+ const inflight = [];
1293
+ const partial = snapshot.partial;
1294
+ if (partial !== void 0 && partial !== null) {
1295
+ const { text, reasoning, hasToolCall } = assistantParts(partial.blocks);
1296
+ if (text !== "" || reasoning !== "" || !hasToolCall) inflight.push({
1297
+ turn: typeof partial.turn === "number" ? partial.turn : Number.MAX_SAFE_INTEGER,
1298
+ step: typeof partial.step === "number" ? partial.step : Number.MAX_SAFE_INTEGER,
1299
+ order: 0,
1300
+ message: {
1301
+ key: "partial",
1302
+ role: "assistant",
1303
+ text,
1304
+ ...reasoning !== "" ? { reasoning } : {},
1305
+ streaming: true
1306
+ }
1307
+ });
1308
+ }
1309
+ if (Array.isArray(snapshot.runningCalls)) for (const call of snapshot.runningCalls) {
1310
+ if (typeof call !== "object" || call === null) continue;
1311
+ const c = call;
1312
+ const toolName = typeof c.name === "string" ? c.name : t("toolFallback");
1313
+ inflight.push({
1314
+ turn: typeof c.turn === "number" ? c.turn : Number.MAX_SAFE_INTEGER,
1315
+ step: typeof c.step === "number" ? c.step : Number.MAX_SAFE_INTEGER,
1316
+ order: 1,
1317
+ message: {
1318
+ key: `rc:${typeof c.callId === "string" ? c.callId : "?"}`,
1319
+ role: "tool",
1320
+ toolName,
1321
+ text: "",
1322
+ streaming: true,
1323
+ card: c.callView != null ? cardModelOf({
1324
+ toolName,
1325
+ callView: c.callView,
1326
+ resultView: null
1327
+ }) : cardModelFromNode({
1328
+ name: toolName,
1329
+ argsRaw: typeof c.argsRaw === "string" ? c.argsRaw : void 0,
1330
+ rawText: ""
1331
+ })
1332
+ }
1333
+ });
1334
+ }
1335
+ inflight.sort((a, b) => a.turn - b.turn || a.step - b.step || a.order - b.order);
1336
+ for (const item of inflight) out.push(item.message);
1337
+ return out;
1338
+ }
1339
+ /**
1340
+ * D1 父历史折叠的划分:seq <= boundarySeq 的消息为继承区(fork 时刻快照,
1341
+ * 折叠进指示卡),其余(含在途项——无 seq)为新鲜区。boundarySeq 缺省 =
1342
+ * 全新鲜(升级前老 Tab 的行为不变)。
1343
+ */
1344
+ function partitionInherited(messages, boundarySeq) {
1345
+ if (boundarySeq === void 0) return {
1346
+ inherited: [],
1347
+ fresh: [...messages]
1348
+ };
1349
+ const inherited = [];
1350
+ const fresh = [];
1351
+ for (const message of messages) if (message.seq !== void 0 && message.seq <= boundarySeq) inherited.push(message);
1352
+ else fresh.push(message);
1353
+ return {
1354
+ inherited,
1355
+ fresh
1356
+ };
1357
+ }
1358
+ //#endregion
1359
+ //#region src/client/host/markdown.ts
1360
+ let cache;
1361
+ /** 当前语言的 chrome 对象(同语言复用同一引用)。 */
1362
+ function markdownChrome() {
1363
+ const copyLabel = t("codeCopy");
1364
+ const copiedLabel = t("codeCopied");
1365
+ if (cache !== void 0 && cache.copyLabel === copyLabel && cache.copiedLabel === copiedLabel) return cache.chrome;
1366
+ const chrome = {
1367
+ copyLabel,
1368
+ copiedLabel,
1369
+ code: {
1370
+ copyLabel,
1371
+ copiedLabel
1372
+ },
1373
+ footnotes: ""
1374
+ };
1375
+ cache = {
1376
+ copyLabel,
1377
+ copiedLabel,
1378
+ chrome
1379
+ };
1380
+ return chrome;
1381
+ }
1382
+ /** MarkdownText 整组 props(双 prop 名承载同一 chrome;text/streaming 透传)。 */
1383
+ function markdownTextProps(text, streaming) {
1384
+ const chrome = markdownChrome();
1385
+ return {
1386
+ text,
1387
+ streaming,
1388
+ codeLabels: chrome,
1389
+ labels: chrome
1390
+ };
1391
+ }
1392
+ //#endregion
1393
+ //#region src/client/host/labels.ts
1394
+ /**
1395
+ * 工具卡叶子块的 labels 构建器(0.1.2 起各块 labels 为必填/无守卫读取——
1396
+ * 缺了在宿主运行时直接崩,e2e 实锤两次:TerminalBlock.copy、ReadBlock.copy)。
1397
+ *
1398
+ * 类型注记:我们构建期类型是 0.1.1(labels 字段不存在/可选),0.1.2 运行时
1399
+ * 要求必填——所以各 builder 返回类型按 0.1.2 形状写、调用点 cast(与
1400
+ * markdown.ts 双形状同款姿势)。
1401
+ */
1402
+ /** TerminalBlockLabels(0.1.1 与 0.1.2 同形状)。 */
1403
+ function terminalLabels() {
1404
+ return {
1405
+ signal: (signal) => t("termSignal", { signal }),
1406
+ exitCode: (code) => t("termExitCode", { code }),
1407
+ running: t("running"),
1408
+ failed: t("failed"),
1409
+ done: t("termDone"),
1410
+ copy: t("codeCopy"),
1411
+ copied: t("codeCopied"),
1412
+ noOutput: t("termNoOutput"),
1413
+ collapseAria: t("termCollapseAria"),
1414
+ collapse: t("termCollapse"),
1415
+ expandAria: (n) => t("termExpandAria", { n }),
1416
+ expand: (n) => t("termExpand", { n })
1417
+ };
1418
+ }
1419
+ /** ReadBlockLabels(0.1.2 必填)。 */
1420
+ function readLabels() {
1421
+ return {
1422
+ window: (shown, total) => t("readWindow", {
1423
+ shown,
1424
+ total
1425
+ }),
1426
+ copy: t("codeCopy"),
1427
+ copied: t("codeCopied"),
1428
+ collapseAria: t("termCollapseAria"),
1429
+ expandAria: (n) => t("termExpandAria", { n }),
1430
+ collapse: t("termCollapse"),
1431
+ expand: (n) => t("termExpand", { n })
1432
+ };
1433
+ }
1434
+ /** SearchBlockLabels(0.1.2 必填)。 */
1435
+ function searchLabels() {
1436
+ return {
1437
+ pathsSummary: (shown, total, truncated) => t("searchPathsSummary", {
1438
+ shown,
1439
+ total
1440
+ }) + (truncated ? "…" : ""),
1441
+ matchesSummary: (shown, total, files, truncated) => t("searchMatchesSummary", {
1442
+ shown,
1443
+ total,
1444
+ files
1445
+ }) + (truncated ? "…" : ""),
1446
+ copy: t("codeCopy"),
1447
+ copied: t("codeCopied"),
1448
+ noResults: t("searchNoResults"),
1449
+ collapseAria: t("termCollapseAria"),
1450
+ expandAria: (n) => t("termExpandAria", { n }),
1451
+ collapse: t("termCollapse"),
1452
+ expand: (n) => t("termExpand", { n })
1453
+ };
1454
+ }
1455
+ /** DiffBlockLabels(0.1.2 必填)。 */
1456
+ function diffLabels() {
1457
+ return {
1458
+ copy: t("codeCopy"),
1459
+ copied: t("codeCopied"),
1460
+ collapseAria: t("termCollapseAria"),
1461
+ expandAria: (n) => t("termExpandAria", { n }),
1462
+ collapse: t("termCollapse"),
1463
+ expand: (n) => t("termExpand", { n }),
1464
+ files: (n) => t("diffFiles", { n })
1465
+ };
1466
+ }
1467
+ /** WebBlockLabels(0.1.2 必填;markdown 子面复用双形状 chrome)。 */
1468
+ function webLabels() {
1469
+ return {
1470
+ noResults: t("webNoResults"),
1471
+ sourcesTruncated: t("webSourcesTruncated"),
1472
+ http: "HTTP",
1473
+ contentTruncated: t("webContentTruncated"),
1474
+ markdown: markdownChrome()
1475
+ };
1476
+ }
1477
+ //#endregion
1478
+ //#region \0dsh-css:/home/runner/work/dsh-sidenote/dsh-sidenote/src/client/sidechat/sidechat.module.css.mjs
1479
+ const css$1 = "._5KEEsG_root{height:100%;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:13px;display:flex;position:relative}._5KEEsG_body{flex-direction:column;flex:1;padding:12px 10px;display:flex;overflow-y:auto}._5KEEsG_transcript{flex-direction:column;gap:10px;display:flex}._5KEEsG_empty,._5KEEsG_stateScreen{text-align:center;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px 16px;display:flex}._5KEEsG_emptyIcon{color:var(--dsw-alias-label-tertiary)}._5KEEsG_emptyTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600}._5KEEsG_emptyText{color:var(--dsw-alias-label-tertiary);max-width:240px;font-size:12px;line-height:1.6}._5KEEsG_stateDetail{color:var(--dsw-alias-state-error-primary);word-break:break-word;max-width:260px;font-size:12px;line-height:1.6}._5KEEsG_userRow{justify-content:flex-end;display:flex}._5KEEsG_userBubble{background:var(--dsw-specific-bubble,#edf3fe);max-width:88%;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;border-radius:16px;padding:8px 12px;font-size:13px;line-height:1.55}._5KEEsG_assistantRow{justify-content:flex-start;display:flex;position:relative}._5KEEsG_rowActions{opacity:0;z-index:2;transition:opacity .12s ease-out;position:absolute;top:-8px;right:0}._5KEEsG_assistantRow:hover ._5KEEsG_rowActions{opacity:1}._5KEEsG_reflowButton{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-bg-layer-1,#fff);width:22px;height:22px;color:var(--dsw-alias-label-tertiary,#8f959e);cursor:pointer;border-radius:999px;place-items:center;padding:0;display:grid;box-shadow:0 2px 8px #00000014}._5KEEsG_reflowButton:hover{color:var(--dsw-alias-state-business-primary,#2f88ff);border-color:var(--dsw-alias-state-business-primary,#2f88ff)}._5KEEsG_assistantBody{word-break:break-word;max-width:100%;font-size:13px;line-height:1.6}._5KEEsG_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}._5KEEsG_reasoning>summary{cursor:pointer;user-select:none}._5KEEsG_reasoningBody{white-space:pre-wrap;word-break:break-word;color:var(--dsw-alias-label-tertiary);margin-top:2px;padding-bottom:2px;padding-left:22px}._5KEEsG_streamingHint{color:var(--dsw-alias-label-dimmed)}._5KEEsG_toolCard{font-size:12px}._5KEEsG_toolHead{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;display:flex}._5KEEsG_toolError{color:var(--dsw-alias-state-error-primary)}._5KEEsG_toolRunning{color:var(--dsw-alias-label-tertiary)}._5KEEsG_toolBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;max-height:160px;padding:2px 0 2px 22px;font-size:12px;overflow-y:auto}._5KEEsG_errorRow{color:var(--dsw-alias-state-error-primary);white-space:pre-wrap;padding:4px 2px;font-size:12px}._5KEEsG_noticeRow{color:var(--dsw-alias-label-dimmed);text-align:center;padding:2px 0;font-size:12px}._5KEEsG_composer{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:6px;padding:8px 10px;display:flex;position:relative}._5KEEsG_composerCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:14px;flex-direction:column;gap:2px;padding:4px 6px 6px;transition:border-color .12s;display:flex}._5KEEsG_composerCard:focus-within{border-color:var(--dsw-alias-state-business-primary)}._5KEEsG_input{resize:none;width:100%;min-height:36px;max-height:160px;color:var(--dsw-alias-label-primary);font:inherit;box-sizing:border-box;background:0 0;border:none;padding:6px 6px 2px}._5KEEsG_input:focus{outline:none}._5KEEsG_composerFoot{justify-content:space-between;align-items:center;gap:8px;padding:0 2px;display:flex}._5KEEsG_modelLabel{color:var(--dsw-alias-label-dimmed);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}._5KEEsG_sendButton,._5KEEsG_stopButton{appearance:none;background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;width:28px;height:28px;padding:0;display:inline-flex}._5KEEsG_sendButton:disabled{opacity:.45;cursor:default}._5KEEsG_headerSide{border:1px solid var(--dsw-alias-border-l1,#0000001a);color:var(--dsw-alias-label-secondary,#3f454d);cursor:pointer;user-select:none;background:0 0;border-radius:999px;align-items:center;gap:5px;padding:3px 10px;font-size:12px;line-height:18px;display:inline-flex}._5KEEsG_headerSide:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-primary,#1f2329)}._5KEEsG_sentChipRow{align-items:center;gap:6px;margin-right:8px;display:inline-flex}._5KEEsG_sentChip{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-secondary,#3f454d);user-select:none;border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;line-height:16px;display:inline-flex}._5KEEsG_flowRow{font-size:12px}._5KEEsG_rowPreview{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-dimmed);flex:1;margin-left:8px;font-size:12px;overflow:hidden}._5KEEsG_flowRowError{color:var(--dsw-alias-state-error-primary)}._5KEEsG_todoBody{flex-direction:column;gap:3px;padding:2px 0 4px 22px;display:flex}._5KEEsG_todoItem{align-items:baseline;gap:7px;min-width:0;font-size:12px;line-height:1.5;display:flex}._5KEEsG_todoDot{background:var(--dsw-alias-label-dimmed);border-radius:50%;flex:none;width:6px;height:6px;transform:translateY(-1px)}._5KEEsG_todoDot[data-status=completed]{background:var(--dsw-alias-state-success-primary,#34a853)}._5KEEsG_todoDot[data-status=in_progress]{background:var(--dsw-alias-state-business-primary)}._5KEEsG_todoText[data-status=completed]{color:var(--dsw-alias-label-dimmed)}._5KEEsG_toolGenericBody{flex-direction:column;gap:6px;padding:4px 0 4px 22px;display:flex}._5KEEsG_toolBodyText{color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.55;font-family:var(--dsw-alias-font-mono,ui-monospace, monospace);margin:0}._5KEEsG_toolLocations{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-all;font-size:11px}._5KEEsG_inheritedBody{border-top:1px dashed var(--dsw-alias-border-l2);opacity:.92;padding:6px 0 2px}._5KEEsG_densityRow{justify-content:flex-end;align-items:center;padding:2px 2px 4px;display:flex}._5KEEsG_densityToggle{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:4px;padding:2px 6px;font-size:11px}._5KEEsG_densityToggle:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}._5KEEsG_jumpBottom{z-index:5;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:999px;padding:3px 10px;font-size:11px;position:absolute;bottom:96px;right:12px;box-shadow:0 2px 8px #0000001f}._5KEEsG_jumpBottom:hover{background:var(--dsw-alias-bg-layer-2)}._5KEEsG_modelSelect{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:6px;align-items:center;gap:3px;max-width:180px;padding:3px 6px;font-size:12px;display:inline-flex;overflow:hidden}._5KEEsG_modelSelect>span:first-child{text-overflow:ellipsis;overflow:hidden}._5KEEsG_modelSelect>svg{color:var(--dsw-alias-label-dimmed);flex:none}._5KEEsG_modelSelect:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}._5KEEsG_composerChips{align-items:center;gap:4px;min-width:0;display:inline-flex;overflow:hidden}._5KEEsG_slashMenu{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);z-index:20;border-radius:10px;max-height:260px;margin-bottom:6px;padding:4px;position:absolute;bottom:100%;left:8px;right:8px;overflow-y:auto;box-shadow:0 8px 24px #00000024}._5KEEsG_slashGroupTitle{color:var(--dsw-alias-label-tertiary);text-transform:none;padding:4px 8px 2px;font-size:11px}._5KEEsG_slashItem,._5KEEsG_slashItemActive{text-align:left;cursor:pointer;width:100%;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:6px;align-items:baseline;gap:8px;padding:6px 8px;font-size:13px;display:flex}._5KEEsG_slashItemActive,._5KEEsG_slashItem:hover{background:var(--dsw-alias-interactive-bg-hover)}._5KEEsG_slashItemName{flex:none;font-weight:500}._5KEEsG_slashItemDetail{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}._5KEEsG_attachRail{flex-wrap:wrap;gap:6px;padding:2px 0;display:flex}._5KEEsG_attachThumb{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;width:44px;height:44px;position:relative;overflow:hidden}._5KEEsG_attachImg{object-fit:cover;width:100%;height:100%;display:block}._5KEEsG_attachRemove{color:#fff;cursor:pointer;background:#0000008c;border:none;border-radius:999px;place-items:center;width:16px;height:16px;padding:0;font-size:11px;line-height:1;display:grid;position:absolute;top:1px;right:1px}._5KEEsG_attachButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:6px;padding:4px;display:inline-flex}._5KEEsG_attachButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}._5KEEsG_pendingBar{border:1px solid var(--dsw-alias-state-warning-primary,#d4a017);background:var(--dsw-alias-bg-layer-2);width:calc(100% - 16px);color:var(--dsw-alias-state-warning-primary,#d4a017);text-align:left;cursor:pointer;border-radius:8px;margin:0 8px 6px;padding:6px 10px;font-size:12px;display:block}._5KEEsG_pendingBar:hover{background:var(--dsw-alias-bg-layer-3)}._5KEEsG_writeNotice{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);border-radius:8px;align-items:center;gap:8px;margin:0 8px 6px;padding:6px 10px;font-size:12px;display:flex}._5KEEsG_writeNoticeClose{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;margin-left:auto;padding:0 4px;font-size:13px}._5KEEsG_parentStrip{border:none;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-secondary);cursor:pointer;text-align:left;align-items:center;gap:6px;padding:5px 12px;font-size:11px;display:flex}._5KEEsG_parentStrip:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}._5KEEsG_dotRunning,._5KEEsG_dotPending,._5KEEsG_dotIdle{border-radius:999px;flex:none;width:7px;height:7px}._5KEEsG_dotRunning{background:var(--dsw-alias-state-business-primary,#2f88ff);animation:1.2s ease-in-out infinite _5KEEsG_sidechat-pulse}._5KEEsG_dotPending{background:var(--dsw-alias-state-warning-primary,#d4a017)}._5KEEsG_dotIdle{background:var(--dsw-alias-label-dimmed)}@keyframes _5KEEsG_sidechat-pulse{50%{opacity:.45}}._5KEEsG_actionGroup{gap:2px;margin-right:auto;display:inline-flex}";
1480
+ const tagId$1 = "dsh-sidenote/sidechat.module.css";
1481
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
1482
+ const tag = document.createElement("style");
1483
+ tag.dataset.plugin = "dsh-sidenote";
1484
+ tag.dataset.pluginCss = tagId$1;
1485
+ tag.textContent = css$1;
1486
+ document.head.appendChild(tag);
1487
+ }
1488
+ var sidechat_module_css_default = {
1489
+ "noticeRow": "_5KEEsG_noticeRow",
1490
+ "toolBody": "_5KEEsG_toolBody",
1491
+ "densityRow": "_5KEEsG_densityRow",
1492
+ "toolCard": "_5KEEsG_toolCard",
1493
+ "sidechat-pulse": "_5KEEsG_sidechat-pulse",
1494
+ "slashItemDetail": "_5KEEsG_slashItemDetail",
1495
+ "composerFoot": "_5KEEsG_composerFoot",
1496
+ "assistantRow": "_5KEEsG_assistantRow",
1497
+ "toolGenericBody": "_5KEEsG_toolGenericBody",
1498
+ "attachImg": "_5KEEsG_attachImg",
1499
+ "slashItemActive": "_5KEEsG_slashItemActive",
1500
+ "emptyIcon": "_5KEEsG_emptyIcon",
1501
+ "toolError": "_5KEEsG_toolError",
1502
+ "empty": "_5KEEsG_empty",
1503
+ "flowRow": "_5KEEsG_flowRow",
1504
+ "flowRowError": "_5KEEsG_flowRowError",
1505
+ "slashItem": "_5KEEsG_slashItem",
1506
+ "streamingHint": "_5KEEsG_streamingHint",
1507
+ "input": "_5KEEsG_input",
1508
+ "sendButton": "_5KEEsG_sendButton",
1509
+ "todoBody": "_5KEEsG_todoBody",
1510
+ "parentStrip": "_5KEEsG_parentStrip",
1511
+ "dotRunning": "_5KEEsG_dotRunning",
1512
+ "dotPending": "_5KEEsG_dotPending",
1513
+ "userRow": "_5KEEsG_userRow",
1514
+ "toolLocations": "_5KEEsG_toolLocations",
1515
+ "todoText": "_5KEEsG_todoText",
1516
+ "actionGroup": "_5KEEsG_actionGroup",
1517
+ "inheritedBody": "_5KEEsG_inheritedBody",
1518
+ "todoItem": "_5KEEsG_todoItem",
1519
+ "errorRow": "_5KEEsG_errorRow",
1520
+ "attachRemove": "_5KEEsG_attachRemove",
1521
+ "sentChipRow": "_5KEEsG_sentChipRow",
1522
+ "composerCard": "_5KEEsG_composerCard",
1523
+ "stateScreen": "_5KEEsG_stateScreen",
1524
+ "headerSide": "_5KEEsG_headerSide",
1525
+ "transcript": "_5KEEsG_transcript",
1526
+ "emptyText": "_5KEEsG_emptyText",
1527
+ "toolHead": "_5KEEsG_toolHead",
1528
+ "pendingBar": "_5KEEsG_pendingBar",
1529
+ "assistantBody": "_5KEEsG_assistantBody",
1530
+ "attachButton": "_5KEEsG_attachButton",
1531
+ "dotIdle": "_5KEEsG_dotIdle",
1532
+ "root": "_5KEEsG_root",
1533
+ "toolRunning": "_5KEEsG_toolRunning",
1534
+ "stateDetail": "_5KEEsG_stateDetail",
1535
+ "slashMenu": "_5KEEsG_slashMenu",
1536
+ "slashItemName": "_5KEEsG_slashItemName",
1537
+ "writeNotice": "_5KEEsG_writeNotice",
1538
+ "attachRail": "_5KEEsG_attachRail",
1539
+ "reasoningBody": "_5KEEsG_reasoningBody",
1540
+ "sentChip": "_5KEEsG_sentChip",
1541
+ "densityToggle": "_5KEEsG_densityToggle",
1542
+ "modelSelect": "_5KEEsG_modelSelect",
1543
+ "slashGroupTitle": "_5KEEsG_slashGroupTitle",
1544
+ "emptyTitle": "_5KEEsG_emptyTitle",
1545
+ "body": "_5KEEsG_body",
1546
+ "userBubble": "_5KEEsG_userBubble",
1547
+ "reflowButton": "_5KEEsG_reflowButton",
1548
+ "toolBodyText": "_5KEEsG_toolBodyText",
1549
+ "rowPreview": "_5KEEsG_rowPreview",
1550
+ "attachThumb": "_5KEEsG_attachThumb",
1551
+ "stopButton": "_5KEEsG_stopButton",
1552
+ "rowActions": "_5KEEsG_rowActions",
1553
+ "jumpBottom": "_5KEEsG_jumpBottom",
1554
+ "reasoning": "_5KEEsG_reasoning",
1555
+ "modelLabel": "_5KEEsG_modelLabel",
1556
+ "writeNoticeClose": "_5KEEsG_writeNoticeClose",
1557
+ "todoDot": "_5KEEsG_todoDot",
1558
+ "composerChips": "_5KEEsG_composerChips",
1559
+ "composer": "_5KEEsG_composer"
1560
+ };
1561
+ //#endregion
1562
+ //#region src/client/chat/ToolCard.tsx
1563
+ /**
1564
+ * 工具卡(L2 视图):DisclosureRow 窄栏壳(默认折叠——密度于默认态)+
1565
+ * 展开态用宿主同源叶子块(TerminalBlock/DiffBlock/ReadBlock——材质同源)。
1566
+ * 数据全部来自 cards.ts 的 ToolCardModel(本组件零判别逻辑)。
1567
+ *
1568
+ * P0 落地四卡:generic(自绘)/ terminal / diff / read;search/web 暂经
1569
+ * generic 化渲染(cards.ts 保留全量数据,后续 arm 扩展是加法不是返工)。
1570
+ */
1571
+ /** ToolCallKind → 图标(宿主 ui-tool 同族映射;primitives 图标全集见 icons/index.d.ts)。 */
1572
+ function kindIcon(kind) {
1573
+ switch (kind) {
1574
+ case "read": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconListPenOutline16, { size: 14 });
1575
+ case "edit": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 14 });
1576
+ case "delete": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 });
1577
+ case "move": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutline16, { size: 14 });
1578
+ case "search": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: 14 });
1579
+ case "execute": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlayOutline16, { size: 14 });
1580
+ case "fetch": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGlobeOutline14, { size: 14 });
1581
+ default: return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCordisPluginOutline14, { size: 14 });
1582
+ }
1583
+ }
1584
+ /** 折叠态读取 hook(viewState store 的 React 绑定,L2 侧)。 */
1585
+ function useFoldOpen(fold, rowKey) {
1586
+ return (0, react.useSyncExternalStore)((notify) => fold.subscribe(notify), () => fold.isOpen(rowKey));
1587
+ }
1588
+ function ToolCard(props) {
1589
+ const { model, rowKey, fold } = props;
1590
+ const open = useFoldOpen(fold, rowKey);
1591
+ const icon = model.kind === "terminal" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlayOutline16, { size: 14 }) : model.kind === "diff" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 14 }) : model.kind === "read" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCodeOutline16, { size: 14 }) : model.kind === "todo" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChecklistOutline14, { size: 14 }) : model.kind === "generic" ? kindIcon(model.icon) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCordisPluginOutline14, { size: 14 });
1592
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1593
+ className: sidechat_module_css_default.flowRow,
1594
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1595
+ icon,
1596
+ title: model.title,
1597
+ open,
1598
+ expandable: true,
1599
+ expandOnRowClick: true,
1600
+ previewChevron: true,
1601
+ ...props.error === true ? { titleClassName: sidechat_module_css_default.flowRowError } : {},
1602
+ onToggle: () => {
1603
+ fold.toggle(rowKey);
1604
+ },
1605
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardBody, {
1606
+ model,
1607
+ streaming: props.streaming
1608
+ })
1609
+ })
1610
+ });
1611
+ }
1612
+ function ToolCardBody({ model, streaming }) {
1613
+ switch (model.kind) {
1614
+ case "terminal": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
1615
+ command: model.command,
1616
+ labels: terminalLabels(),
1617
+ ...model.cwd !== void 0 ? { cwd: model.cwd } : {},
1618
+ ...model.output !== void 0 ? { output: model.output } : {},
1619
+ ...model.exitCode !== void 0 ? { exitCode: model.exitCode } : {},
1620
+ ...model.signal !== void 0 ? { signal: model.signal } : {},
1621
+ ...streaming === true ? { running: true } : {}
1622
+ });
1623
+ case "todo": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1624
+ className: sidechat_module_css_default.todoBody,
1625
+ children: model.items.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1626
+ className: sidechat_module_css_default.todoItem,
1627
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1628
+ className: sidechat_module_css_default.todoDot,
1629
+ "data-status": item.status
1630
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1631
+ className: sidechat_module_css_default.todoText,
1632
+ "data-status": item.status,
1633
+ children: item.content
1634
+ })]
1635
+ }, i))
1636
+ });
1637
+ case "diff": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
1638
+ labels: diffLabels(),
1639
+ diffs: model.diffs.map((d) => ({
1640
+ path: d.path,
1641
+ oldText: d.oldText,
1642
+ newText: d.newText
1643
+ }))
1644
+ });
1645
+ case "read": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ReadBlock, {
1646
+ label: model.path,
1647
+ labels: readLabels(),
1648
+ lines: model.lines.map((l) => ({
1649
+ number: l.number,
1650
+ text: l.text
1651
+ })),
1652
+ totalLines: model.totalLines,
1653
+ ...model.lang !== void 0 ? { lang: model.lang } : {}
1654
+ });
1655
+ case "search": return model.shape === "matches" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
1656
+ labels: searchLabels(),
1657
+ kind: "matches",
1658
+ files: [...model.files ?? []].map((f) => ({
1659
+ path: f.path,
1660
+ matches: [...f.matches]
1661
+ })),
1662
+ truncated: model.truncated,
1663
+ total: model.total
1664
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
1665
+ labels: searchLabels(),
1666
+ kind: "paths",
1667
+ paths: [...model.paths ?? []],
1668
+ truncated: model.truncated,
1669
+ total: model.total
1670
+ });
1671
+ case "web": return model.webKind === "search" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.WebBlock, {
1672
+ labels: webLabels(),
1673
+ kind: "search",
1674
+ sources: [...model.sources ?? []],
1675
+ truncated: model.truncated === true,
1676
+ ...model.answer !== void 0 ? { answer: model.answer } : {}
1677
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.WebBlock, {
1678
+ labels: webLabels(),
1679
+ kind: "fetch",
1680
+ url: model.url ?? "",
1681
+ statusCode: model.statusCode ?? 0,
1682
+ truncated: model.truncated === true
1683
+ });
1684
+ default: {
1685
+ const m = model;
1686
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1687
+ className: sidechat_module_css_default.toolGenericBody,
1688
+ children: [
1689
+ m.bodyText !== void 0 && m.bodyText !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1690
+ className: sidechat_module_css_default.toolBodyText,
1691
+ children: m.bodyText
1692
+ }),
1693
+ m.rawInput !== void 0 && (typeof m.rawInput === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1694
+ className: sidechat_module_css_default.toolBodyText,
1695
+ children: m.rawInput
1696
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonTree, { data: m.rawInput })),
1697
+ m.locations !== void 0 && m.locations.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1698
+ className: sidechat_module_css_default.toolLocations,
1699
+ children: m.locations.map((l) => `${l.path}${l.line !== void 0 ? `:${l.line}` : ""}`).join("\n")
1700
+ }),
1701
+ m.bodyText === void 0 && m.rawInput === void 0 && streaming === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1702
+ className: sidechat_module_css_default.toolBodyText,
1703
+ children: t("running")
1704
+ })
1705
+ ]
1706
+ });
1707
+ }
1708
+ }
1709
+ }
1710
+ //#endregion
1711
+ //#region src/client/chat/ReasoningRow.tsx
1712
+ /**
1713
+ * 思考折叠行(L2 视图):DisclosureRow 壳取代原生 <details>——与工具卡同
1714
+ * 一套镀铬(材质同源)。行为对齐宿主 ReasoningRow 语义:流式中默认展开
1715
+ * (用户在看推理),完成后默认折叠;用户显式收起过就不再自动展开
1716
+ * (fold.has 区分「未触碰」与「显式收起」)。
1717
+ */
1718
+ /** 首行非空文本(折叠态预览——主区「Think · 首行摘要」同款)。 */
1719
+ function firstLineOf(text) {
1720
+ for (const line of text.split("\n")) {
1721
+ const s = line.trim();
1722
+ if (s !== "") return s;
1723
+ }
1724
+ return "";
1725
+ }
1726
+ function ReasoningRow(props) {
1727
+ const { rowKey, fold } = props;
1728
+ const touched = (0, react.useSyncExternalStore)((fn) => fold.subscribe(fn), () => fold.has(rowKey));
1729
+ const open = (0, react.useSyncExternalStore)((fn) => fold.subscribe(fn), () => fold.isOpen(rowKey)) || props.streaming === true && !touched;
1730
+ const preview = firstLineOf(props.text);
1731
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1732
+ className: sidechat_module_css_default.flowRow,
1733
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1734
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutline16, { size: 14 }),
1735
+ title: t("thinking"),
1736
+ open,
1737
+ expandable: true,
1738
+ expandOnRowClick: true,
1739
+ previewChevron: true,
1740
+ ...preview !== "" ? { collapsedContent: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1741
+ className: sidechat_module_css_default.rowPreview,
1742
+ children: preview
1743
+ }) } : {},
1744
+ onToggle: () => {
1745
+ fold.toggle(rowKey);
1746
+ },
1747
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1748
+ className: sidechat_module_css_default.reasoningBody,
1749
+ children: props.text
1750
+ })
1751
+ })
1752
+ });
1753
+ }
1754
+ //#endregion
1755
+ //#region src/client/chat/FoldCard.tsx
1756
+ /**
1757
+ * 父历史折叠卡(D1,L2 视图):fork 继承的消息区默认折叠为一张指示卡
1758
+ * 「继承自主会话 · 截至 fork 点 · N 条」,点击展开查看(内容与主对话同源
1759
+ * 渲染——同一套 MessageRow)。
1760
+ *
1761
+ * 产品语义(design-draft v3 D1):模型侧全量继承不变(fork 即快照),UI 分层;
1762
+ * 文案标注「截至 fork 点」——fork 后主对话继续推进,本卡是静态快照。
1763
+ */
1764
+ function FoldCard(props) {
1765
+ const { rowKey, fold } = props;
1766
+ const open = (0, react.useSyncExternalStore)((fn) => fold.subscribe(fn), () => fold.isOpen(rowKey));
1767
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1768
+ className: sidechat_module_css_default.flowRow,
1769
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1770
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutline16, { size: 14 }),
1771
+ title: t("inheritedLabel", { n: props.count }),
1772
+ open,
1773
+ expandable: true,
1774
+ expandOnRowClick: true,
1775
+ previewChevron: true,
1776
+ onToggle: () => {
1777
+ fold.toggle(rowKey);
1778
+ },
1779
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1780
+ className: sidechat_module_css_default.inheritedBody,
1781
+ children: props.children
1782
+ })
1783
+ })
1784
+ });
1785
+ }
1786
+ //#endregion
1787
+ //#region src/client/locale-tick.ts
1788
+ /**
1789
+ * React 绑定点(从 locales.ts 拆出):字典/t() 是纯逻辑层(L0 可测),
1790
+ * 本 hook 是唯一的 react 依赖——拆开让 L0 不再经 locales 传递性引入 react
1791
+ * (WI-00 分层纪律,arch-audit §1.3)。
1792
+ */
1793
+ /** 语言切换时驱动组件重渲(useSyncExternalStore 标准接法)。 */
1794
+ function useLocaleTick() {
1795
+ (0, react.useSyncExternalStore)(subscribeLocale, localeSnapshot);
1796
+ }
1797
+ //#endregion
1798
+ //#region src/client/sidechat/rows.tsx
1799
+ /**
1800
+ * 侧边面板的消息行家族(L2 视图):MessageList/MessageRow/ReflowButton/
1801
+ * EmptyState/StateScreen。自 SideChatPanel.tsx 拆出(WI-00 预算纪律:
1802
+ * 面板壳只留编排)。渲染规则:MessageRow 全族 memo(字段值比较器)。
1803
+ */
1804
+ /** 空状态:💬 类图标 + 标题 + fork 语义文案(形态规格)。 */
1805
+ function EmptyState() {
1806
+ useLocaleTick();
1807
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1808
+ className: sidechat_module_css_default.empty,
1809
+ children: [
1810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1811
+ className: sidechat_module_css_default.emptyIcon,
1812
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutline16, { size: 32 })
1813
+ }),
1814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1815
+ className: sidechat_module_css_default.emptyTitle,
1816
+ children: t("emptyTitle")
1817
+ }),
1818
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1819
+ className: sidechat_module_css_default.emptyText,
1820
+ children: t("emptyText")
1821
+ })
1822
+ ]
1823
+ });
1824
+ }
1825
+ /** 加载 / 错误整屏态(标题 + 可选详情 + 可选指引)。 */
1826
+ function StateScreen(props) {
1827
+ useLocaleTick();
1828
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1829
+ className: sidechat_module_css_default.stateScreen,
1830
+ children: [
1831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1832
+ className: sidechat_module_css_default.emptyIcon,
1833
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutline16, { size: 32 })
1834
+ }),
1835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1836
+ className: sidechat_module_css_default.emptyTitle,
1837
+ children: props.title
1838
+ }),
1839
+ props.detail !== void 0 && props.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1840
+ className: sidechat_module_css_default.stateDetail,
1841
+ children: props.detail
1842
+ }),
1843
+ props.hint !== void 0 && props.hint !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1844
+ className: sidechat_module_css_default.emptyText,
1845
+ children: props.hint
1846
+ })
1847
+ ]
1848
+ });
1849
+ }
1850
+ function MessageList({ messages, fold, boundarySeq, reflow, parentSessionId, sideTitle, onPromote }) {
1851
+ const [reflowAllDone, setReflowAllDone] = (0, react.useState)(false);
1852
+ const questions = (0, react.useMemo)(() => pairQuestions(messages), [messages]);
1853
+ const { inherited, fresh } = (0, react.useMemo)(() => partitionInherited(messages, boundarySeq), [messages, boundarySeq]);
1854
+ const renderRow = (message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageRow, {
1855
+ message,
1856
+ question: questions.get(message.key),
1857
+ fold,
1858
+ reflow,
1859
+ parentSessionId,
1860
+ sideTitle
1861
+ }, message.key);
1862
+ const foldableKeys = (0, react.useMemo)(() => {
1863
+ const keys = [];
1864
+ if (inherited.length > 0) keys.push("inherited");
1865
+ for (const m of messages) {
1866
+ if (m.card !== void 0) keys.push(m.key);
1867
+ if (m.reasoning !== void 0 && m.reasoning !== "") keys.push(`${m.key}:thinking`);
1868
+ }
1869
+ return keys;
1870
+ }, [messages, inherited.length]);
1871
+ (0, react.useSyncExternalStore)((0, react.useCallback)((fn) => fold.subscribe(fn), [fold]), () => fold.getSnapshot());
1872
+ const anyOpen = foldableKeys.some((k) => fold.isOpen(k));
1873
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1874
+ className: sidechat_module_css_default.transcript,
1875
+ children: [
1876
+ (foldableKeys.length >= 2 || parentSessionId !== void 0 && messages.length > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1877
+ className: sidechat_module_css_default.densityRow,
1878
+ children: [parentSessionId !== void 0 && messages.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1879
+ className: sidechat_module_css_default.actionGroup,
1880
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1881
+ type: "button",
1882
+ className: sidechat_module_css_default.densityToggle,
1883
+ title: t("reflowAllTitle"),
1884
+ onClick: () => {
1885
+ let added = 0;
1886
+ for (const m of messages) {
1887
+ if (m.role !== "assistant" || m.text === "" || m.streaming === true) continue;
1888
+ reflow.add(parentSessionId, sideTitle, m.text, questions.get(m.key));
1889
+ added += 1;
1890
+ }
1891
+ if (added > 0) setReflowAllDone(true);
1892
+ },
1893
+ children: reflowAllDone ? t("reflowAllDone") : t("reflowAll")
1894
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1895
+ type: "button",
1896
+ className: sidechat_module_css_default.densityToggle,
1897
+ title: t("promoteTitle"),
1898
+ onClick: onPromote,
1899
+ children: t("promote")
1900
+ })]
1901
+ }), foldableKeys.length >= 2 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1902
+ type: "button",
1903
+ className: sidechat_module_css_default.densityToggle,
1904
+ onClick: () => {
1905
+ fold.setAll(foldableKeys, !anyOpen);
1906
+ },
1907
+ children: anyOpen ? t("collapseAll") : t("expandAll")
1908
+ })]
1909
+ }),
1910
+ inherited.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FoldCard, {
1911
+ count: inherited.length,
1912
+ rowKey: "inherited",
1913
+ fold,
1914
+ children: inherited.map(renderRow)
1915
+ }),
1916
+ fresh.map(renderRow)
1917
+ ]
1918
+ });
1919
+ }
1920
+ /** 回流按钮(W04 v2):把这条 assistant 结论收为主会话的受控回流对象
1921
+ * (主 composer 上方出现「侧边回流」chip),发送时随拦截器序列化。
1922
+ * 问答成对:带上它回答的那条用户提问(question 缺省时只有 <答>)。 */
1923
+ function ReflowButton({ reflow, parentSessionId, sideTitle, text, question }) {
1924
+ useLocaleTick();
1925
+ const [done, setDone] = (0, react.useState)(false);
1926
+ const timer = (0, react.useRef)(0);
1927
+ (0, react.useEffect)(() => () => {
1928
+ window.clearTimeout(timer.current);
1929
+ }, []);
1930
+ if (parentSessionId === void 0) return null;
1931
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1932
+ type: "button",
1933
+ className: sidechat_module_css_default.reflowButton,
1934
+ title: done ? t("reflowDone") : t("reflowToMain"),
1935
+ "aria-label": t("reflowToMain"),
1936
+ onClick: () => {
1937
+ reflow.add(parentSessionId, sideTitle, text, question);
1938
+ setDone(true);
1939
+ window.clearTimeout(timer.current);
1940
+ timer.current = window.setTimeout(() => {
1941
+ setDone(false);
1942
+ }, 1600);
1943
+ },
1944
+ children: done ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 12 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconShareOutline16, { size: 12 })
1945
+ });
1946
+ }
1947
+ const MessageRow = (0, react.memo)(function MessageRow({ message, question, fold, reflow, parentSessionId, sideTitle }) {
1948
+ useLocaleTick();
1949
+ switch (message.role) {
1950
+ case "user": {
1951
+ const proto = splitProtocolPrefix(message.text);
1952
+ if (proto !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1953
+ className: sidechat_module_css_default.userRow,
1954
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1955
+ className: sidechat_module_css_default.userBubble,
1956
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1957
+ className: sidechat_module_css_default.sentChipRow,
1958
+ children: [proto.annotations.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1959
+ className: sidechat_module_css_default.sentChip,
1960
+ title: proto.annotations.map((a) => `${a.id}. 「${a.quote}」${a.note}`).join("\n"),
1961
+ children: t("sentChipLabel", { n: proto.annotations.length })
1962
+ }), proto.reflows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1963
+ className: sidechat_module_css_default.sentChip,
1964
+ title: proto.reflows.map((r) => `${r.source}: ${flattenReflowContent(r.content).slice(0, 200)}`).join("\n"),
1965
+ children: t("reflowBubbleLabel")
1966
+ })]
1967
+ }), message.text.slice(proto.length)]
1968
+ })
1969
+ });
1970
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1971
+ className: sidechat_module_css_default.userRow,
1972
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1973
+ className: sidechat_module_css_default.userBubble,
1974
+ children: message.text
1975
+ })
912
1976
  });
913
- } catch {}
914
- }, [store]);
915
- (0, react.useEffect)(() => {
916
- if (childId !== void 0 || forkStarted.current) return;
917
- forkStarted.current = true;
918
- let cancelled = false;
919
- (async () => {
920
- try {
921
- const forked = await ctx.sessions.fork({ sessionId: scope.sessionId });
922
- const current = parseSideChatMeta(readTab(ctx, tab.id)?.meta);
923
- ctx.betterSidebar.updateTab(tab.id, { meta: {
924
- ...current,
925
- childId: forked,
926
- parentSessionId: scope.sessionId
1977
+ }
1978
+ case "assistant": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1979
+ className: sidechat_module_css_default.assistantRow,
1980
+ children: [message.text !== "" && message.streaming !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1981
+ className: sidechat_module_css_default.rowActions,
1982
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReflowButton, {
1983
+ reflow,
1984
+ parentSessionId,
1985
+ sideTitle,
1986
+ text: message.text,
1987
+ question
1988
+ })
1989
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1990
+ className: sidechat_module_css_default.assistantBody,
1991
+ children: [
1992
+ message.reasoning !== void 0 && message.reasoning !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningRow, {
1993
+ text: message.reasoning,
1994
+ rowKey: `${message.key}:thinking`,
1995
+ fold,
1996
+ ...message.streaming === true ? { streaming: true } : {}
1997
+ }),
1998
+ message.text !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, { ...markdownTextProps(message.text, message.streaming) }) : message.streaming === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1999
+ className: sidechat_module_css_default.streamingHint,
2000
+ children: t("writing")
2001
+ }),
2002
+ message.interrupted === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2003
+ className: sidechat_module_css_default.noticeRow,
2004
+ children: t("stopped")
2005
+ })
2006
+ ]
2007
+ })]
2008
+ });
2009
+ case "tool":
2010
+ if (message.card !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, {
2011
+ model: message.card,
2012
+ rowKey: message.key,
2013
+ fold,
2014
+ ...message.streaming === true ? { streaming: true } : {},
2015
+ ...message.isError === true ? { error: true } : {}
2016
+ });
2017
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2018
+ className: sidechat_module_css_default.toolCard,
2019
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2020
+ className: sidechat_module_css_default.toolHead,
2021
+ children: [
2022
+ t("toolLabel"),
2023
+ " · ",
2024
+ message.toolName,
2025
+ message.isError === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2026
+ className: sidechat_module_css_default.toolError,
2027
+ children: t("failed")
2028
+ }),
2029
+ message.streaming === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2030
+ className: sidechat_module_css_default.toolRunning,
2031
+ children: t("running")
2032
+ })
2033
+ ]
2034
+ }), message.text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2035
+ className: sidechat_module_css_default.toolBody,
2036
+ children: message.text
2037
+ })]
2038
+ });
2039
+ case "error": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2040
+ className: sidechat_module_css_default.errorRow,
2041
+ children: message.text
2042
+ });
2043
+ case "notice": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2044
+ className: sidechat_module_css_default.noticeRow,
2045
+ children: message.text
2046
+ });
2047
+ }
2048
+ }, rowPropsEqual);
2049
+ /** memo 比较器:transcriptOf 每次快照重建消息对象(引用必变),按字段值比。 */
2050
+ function rowPropsEqual(prev, next) {
2051
+ if (prev.fold !== next.fold || prev.reflow !== next.reflow) return false;
2052
+ if (prev.parentSessionId !== next.parentSessionId || prev.sideTitle !== next.sideTitle) return false;
2053
+ if (prev.question !== next.question) return false;
2054
+ const a = prev.message;
2055
+ const b = next.message;
2056
+ return a === b || a.key === b.key && a.role === b.role && a.text === b.text && a.reasoning === b.reasoning && a.streaming === b.streaming && a.isError === b.isError && a.interrupted === b.interrupted && a.toolName === b.toolName && a.card === b.card && a.seq === b.seq;
2057
+ }
2058
+ //#endregion
2059
+ //#region src/client/sidechat/open.ts
2060
+ /** 从最新快照读一个 Tab(meta 合并写入前的读取面;布局即注册表)。 */
2061
+ function readTab(ctx, tabId) {
2062
+ return collectTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => tab.id === tabId);
2063
+ }
2064
+ /**
2065
+ * 为 sessionId 新建或聚焦一个侧边聊天 Tab;draftText 给出时写入其草稿。
2066
+ * @returns false = 未能打开(调用方保持自己的流程)。
2067
+ */
2068
+ function openOrFocusSideChat(ctx, sessionId, draftText) {
2069
+ try {
2070
+ const snapshot = ctx.betterSidebar.getSnapshot();
2071
+ if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
2072
+ const existing = collectSideTabs(snapshot.state);
2073
+ if (existing.length > 0) {
2074
+ const target = existing[existing.length - 1];
2075
+ if (draftText !== void 0 && draftText !== "") {
2076
+ const meta = parseSideChatMeta(target.meta);
2077
+ const input = meta.childId === void 0 ? null : resolveSessionInput(ctx, meta.childId);
2078
+ if (input !== null) input.setDraft(appendDraftText(readInputDraft(input), draftText));
2079
+ else ctx.betterSidebar.updateTab(target.id, { meta: {
2080
+ ...meta,
2081
+ pendingDraft: appendDraftText(meta.pendingDraft ?? "", draftText)
927
2082
  } });
928
- try {
929
- await ctx.workspaces.archiveSession(forked);
930
- } catch (error) {
931
- console.warn("[dsh-sidenote] 归档侧边会话失败(会话列表可能短暂可见):", error);
932
- }
933
- try {
934
- const parentModels = await ctx.connection.api.sessions.models({ sessionId: scope.sessionId });
935
- if (parentModels.result.ok) {
936
- const current = parentModels.result.value.current;
937
- await ctx.connection.api.sessions.selectModel({
938
- sessionId: forked,
939
- provider: current.provider,
940
- model: current.model,
941
- ...current.reasoningEffort !== void 0 ? { reasoningEffort: current.reasoningEffort } : {}
942
- });
943
- }
944
- } catch (error) {
945
- console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型):", error);
946
- }
947
- } catch (error) {
948
- if (!cancelled) setForkError(error instanceof Error ? error.message : String(error));
949
2083
  }
950
- })();
951
- return () => {
952
- cancelled = true;
953
- };
954
- }, [
955
- ctx,
956
- scope.sessionId,
957
- tab.id,
958
- childId
959
- ]);
960
- const listSnap = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => ctx.sessions.list.subscribe(notify), [ctx]), () => ctx.sessions.list.getSnapshot());
961
- const listed = childId !== void 0 && listSnap.byId?.[childId] !== void 0;
962
- const session = (childId === void 0 ? void 0 : ctx.sessions.binding(childId))?.session;
963
- (0, react.useEffect)(() => {
964
- openSessionWindow(session);
965
- }, [session]);
966
- const phase = phaseOf({
967
- childId,
968
- forkError,
969
- bound: session !== void 0,
970
- listPhase: listSnap.phase,
971
- listed
972
- });
973
- 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());
974
- const messages = (0, react.useMemo)(() => transcriptOf(snapshot), [snapshot]);
975
- const composer = useComposer(ctx, session, childId);
976
- const [modelName, setModelName] = (0, react.useState)(null);
977
- (0, react.useEffect)(() => {
978
- if (childId === void 0 || session === void 0) return;
979
- let cancelled = false;
980
- ctx.connection.api.sessions.models({ sessionId: childId }).then((res) => {
981
- if (!cancelled && res.result.ok) setModelName(res.result.value.current.model);
982
- }).catch(() => {});
983
- return () => {
984
- cancelled = true;
985
- };
986
- }, [
987
- ctx,
988
- childId,
989
- session
990
- ]);
991
- const pendingDraft = meta.pendingDraft;
992
- const rootRef = (0, react.useRef)(null);
993
- (0, react.useEffect)(() => {
994
- if (pendingDraft === void 0 || pendingDraft === "" || phase !== "chat") return;
995
- composer.appendDraft(pendingDraft);
996
- const current = parseSideChatMeta(readTab(ctx, tab.id)?.meta);
997
- ctx.betterSidebar.updateTab(tab.id, { meta: clearPendingDraft(current) });
998
- requestAnimationFrame(() => {
999
- rootRef.current?.querySelector("textarea")?.focus();
2084
+ ctx.betterSidebar.activateTab(target.id, { sessionId });
2085
+ return true;
2086
+ }
2087
+ return createSideChat(ctx, sessionId, draftText);
2088
+ } catch (error) {
2089
+ console.warn("[dsh-sidenote] 打开侧边聊天失败:", error);
2090
+ return false;
2091
+ }
2092
+ }
2093
+ /**
2094
+ * 无条件新建一个侧边聊天 Tab(/side「新建侧边聊天」选项的语义——既有实例
2095
+ * 在弹层里另有聚焦项,「新建」必须真的新建,否则标签与行为不符)。
2096
+ * @returns false = 未能创建(调用方保持自己的流程)。
2097
+ */
2098
+ function createSideChat(ctx, sessionId, draftText) {
2099
+ try {
2100
+ const snapshot = ctx.betterSidebar.getSnapshot();
2101
+ if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
2102
+ const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
2103
+ ctx.betterSidebar.openTab({ type: SIDE_TAB_TYPE }, { sessionId });
2104
+ const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
2105
+ if (created === void 0) return false;
2106
+ if (draftText !== void 0 && draftText !== "") ctx.betterSidebar.updateTab(created.id, { meta: { pendingDraft: draftText } });
2107
+ return true;
2108
+ } catch (error) {
2109
+ console.warn("[dsh-sidenote] 新建侧边聊天失败:", error);
2110
+ return false;
2111
+ }
2112
+ }
2113
+ /**
2114
+ * 桥接/命令的目标预览:askInSideChat 此时若执行会落在哪个 Tab 的标题
2115
+ * (既有并存 = 最后一个;无 = 即将新建的首个标题)。用于编辑器上标明
2116
+ * 「发送至:侧边 N」。会话不在屏上/状态缺失时返回 undefined。
2117
+ */
2118
+ function sideChatTargetTitle(ctx, sessionId) {
2119
+ try {
2120
+ const snapshot = ctx.betterSidebar.getSnapshot();
2121
+ if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return void 0;
2122
+ const existing = collectSideTabs(snapshot.state);
2123
+ if (existing.length > 0) return existing[existing.length - 1].title;
2124
+ return sideTabTitle([]);
2125
+ } catch {
2126
+ return;
2127
+ }
2128
+ }
2129
+ /**
2130
+ * 重开最近关闭的侧边聊天(D3 后悔药):开 Tab + meta 预置 childId/
2131
+ * parentSessionId——面板挂载后走绑定恢复路径(不重新 fork)。
2132
+ * 会话本体必须仍 archived 在列(若被清理,面板落「会话已不存在」态——
2133
+ * 有现成错误态兜着)。
2134
+ */
2135
+ function reopenSideChat(ctx, sessionId, childId, title) {
2136
+ try {
2137
+ const snapshot = ctx.betterSidebar.getSnapshot();
2138
+ if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
2139
+ const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
2140
+ ctx.betterSidebar.openTab({ type: SIDE_TAB_TYPE }, { sessionId });
2141
+ const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
2142
+ if (created === void 0) return false;
2143
+ ctx.betterSidebar.updateTab(created.id, {
2144
+ ...title !== void 0 ? { title } : {},
2145
+ meta: {
2146
+ childId,
2147
+ parentSessionId: sessionId
2148
+ }
1000
2149
  });
1001
- }, [
1002
- pendingDraft,
1003
- phase,
1004
- ctx,
1005
- tab.id
1006
- ]);
1007
- const bodyRef = (0, react.useRef)(null);
1008
- const tailKey = messages.length === 0 ? "" : `${messages[messages.length - 1].key}:${messages[messages.length - 1].text.length}:${messages[messages.length - 1].reasoning?.length ?? 0}`;
1009
- (0, react.useEffect)(() => {
1010
- const el = bodyRef.current;
1011
- if (el !== null && visible) el.scrollTop = el.scrollHeight;
1012
- }, [tailKey, visible]);
1013
- if (phase === "fork-error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateScreen, {
1014
- title: t("forkErrorTitle"),
1015
- detail: forkError ?? void 0,
1016
- hint: t("forkErrorHint")
1017
- });
1018
- if (phase === "missing") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateScreen, {
1019
- title: t("missingTitle"),
1020
- detail: t("missingDetail"),
1021
- hint: t("missingHint")
2150
+ return true;
2151
+ } catch (error) {
2152
+ console.warn("[dsh-sidenote] 重开侧边聊天失败:", error);
2153
+ return false;
2154
+ }
2155
+ }
2156
+ //#endregion
2157
+ //#region src/client/sidechat/lifecycle.ts
2158
+ /**
2159
+ * 首开编排:fork(全量历史快照)→ 登记 meta(先行,绝不丢登记)→
2160
+ * 归档隐藏出会话列表(best-effort)→ 模型跟随主会话(best-effort)。
2161
+ * fork 失败抛错(面板组件转错误态);后两步失败只告警不阻断。
2162
+ * @returns fork 出的子会话 id。
2163
+ */
2164
+ async function forkAndRegister(ctx, parentSessionId, tabId) {
2165
+ let boundarySeq;
2166
+ try {
2167
+ const snap = chatSourceOf(ctx, ctx.sessions.binding(parentSessionId))?.getLegacy();
2168
+ let max = -1;
2169
+ for (const node of snap?.nodes ?? []) {
2170
+ const seq = node.seq;
2171
+ if (typeof seq === "number" && seq > max) max = seq;
2172
+ }
2173
+ if (max >= 0) boundarySeq = max;
2174
+ } catch {}
2175
+ const forked = await ctx.sessions.fork({ sessionId: parentSessionId });
2176
+ updateTabMeta(ctx, tabId, (current) => ({
2177
+ ...current,
2178
+ childId: forked,
2179
+ parentSessionId,
2180
+ ...boundarySeq !== void 0 ? { boundarySeq } : {}
2181
+ }));
2182
+ try {
2183
+ await ctx.workspaces.archiveSession(forked);
2184
+ } catch (error) {
2185
+ console.warn("[dsh-sidenote] 归档侧边会话失败(会话列表可能短暂可见):", error);
2186
+ }
2187
+ try {
2188
+ const parentModels = await ctx.connection.api.sessions.models({ sessionId: parentSessionId });
2189
+ if (parentModels.result.ok) {
2190
+ const current = parentModels.result.value.current;
2191
+ await ctx.connection.api.sessions.selectModel({
2192
+ sessionId: forked,
2193
+ provider: current.provider,
2194
+ model: current.model,
2195
+ ...current.reasoningEffort !== void 0 ? { reasoningEffort: current.reasoningEffort } : {}
2196
+ });
2197
+ }
2198
+ } catch (error) {
2199
+ console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型):", error);
2200
+ }
2201
+ return forked;
2202
+ }
2203
+ /** Tab meta 读-改-写(布局持久化寄存处);tab 已消失时 no-op。 */
2204
+ function updateTabMeta(ctx, tabId, mutate) {
2205
+ const current = parseSideChatMeta(readTab(ctx, tabId)?.meta);
2206
+ ctx.betterSidebar.updateTab(tabId, { meta: mutate(current) });
2207
+ }
2208
+ /**
2209
+ * 程序化入口(/side、bridge 划选提问)打开 Tab 时面板可能处于折叠态——
2210
+ * 类型型 openTab 不自动展开(better-sidebar 只对 path/url 内容型打开展开)。
2211
+ * 幂等展开(off-face:store.update 是 SidebarStore 公开 mutator 面;
2212
+ * feature-check + 吞错,兜底失败不影响功能)。
2213
+ */
2214
+ function ensurePanelOpen(store) {
2215
+ const mutable = store;
2216
+ try {
2217
+ mutable?.update?.((state) => {
2218
+ if (state.panelOpen === false) state.panelOpen = true;
2219
+ });
2220
+ } catch {}
2221
+ }
2222
+ /**
2223
+ * 打开非 staged 会话的事件窗口(off-face:open() 在 concrete Session 上是
2224
+ * public 且幂等,但不在 SessionFace 契约上——契约面只有 staged 会话会被
2225
+ * 运行时自动 open)。feature-check + 吞错,运行时若移除则降级为只发不收。
2226
+ */
2227
+ function openSessionWindow(session) {
2228
+ const openable = session;
2229
+ if (typeof openable?.open !== "function") return;
2230
+ openable.open().catch((error) => {
2231
+ console.warn("[dsh-sidenote] 会话窗口打开失败:", error);
1022
2232
  });
1023
- if (phase === "forking" || phase === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateScreen, { title: t("preparing") });
1024
- const running = snapshot?.running === true;
1025
- const openFailed = snapshot?.openState === "error";
1026
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1027
- ref: rootRef,
1028
- className: sidechat_module_css_default.root,
1029
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1030
- ref: bodyRef,
1031
- className: sidechat_module_css_default.body,
1032
- children: [messages.length === 0 && !running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, {
1033
- messages,
1034
- reflow: props.reflow,
1035
- parentSessionId: meta.parentSessionId,
1036
- sideTitle: tab.title
1037
- }), openFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1038
- className: sidechat_module_css_default.errorRow,
1039
- children: t("historyFailed")
1040
- })]
1041
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerBar, {
1042
- ctx,
1043
- session,
1044
- composer,
1045
- running,
1046
- visible,
1047
- modelName
1048
- })]
2233
+ }
2234
+ /** 读子会话当前模型名(composer 模型标签用);任何失败回退 null(默认文案)。 */
2235
+ async function readModelName(ctx, sessionId) {
2236
+ try {
2237
+ const res = await ctx.connection.api.sessions.models({ sessionId });
2238
+ return res.result.ok ? res.result.value.current.model : null;
2239
+ } catch {
2240
+ return null;
2241
+ }
2242
+ }
2243
+ /**
2244
+ * 0.1.2 优先:`uiConversation` 服务 → binding(会话 binding) → target('chat')
2245
+ * → 快照 .legacy 切片(0.1.2 把内容面从 Session 快照顶层拆到此处——
2246
+ * 根因与证据见 reports/ux-review/W00-fork-replay-012.md);0.1.1 回退
2247
+ * Session 快照顶层(nodes 直接在)。均 miss 返回 undefined(面板维持
2248
+ * 既有降级,不崩)。
2249
+ */
2250
+ function chatSourceOf(ctx, binding) {
2251
+ const session = binding?.session;
2252
+ if (session === void 0) return void 0;
2253
+ try {
2254
+ const target = ctx.get("uiConversation")?.binding?.(binding)?.target?.("chat");
2255
+ if (target !== void 0 && typeof target.getSnapshot === "function" && typeof target.subscribe === "function") return {
2256
+ subscribe: (fn) => target.subscribe(fn),
2257
+ getLegacy: () => {
2258
+ return target.getSnapshot()?.legacy ?? null;
2259
+ }
2260
+ };
2261
+ } catch {}
2262
+ const snap = session.getSnapshot();
2263
+ if (snap !== null && Array.isArray(snap.nodes)) return {
2264
+ subscribe: (fn) => session.subscribe(fn),
2265
+ getLegacy: () => session.getSnapshot()
2266
+ };
2267
+ }
2268
+ /** 拉会话模型目录(models RPC 容错;失败 null → 菜单保持只读标签态)。 */
2269
+ async function listModels(ctx, sessionId) {
2270
+ try {
2271
+ const res = await ctx.connection.api.sessions.models({ sessionId });
2272
+ return res.result.ok ? res.result.value : null;
2273
+ } catch {
2274
+ return null;
2275
+ }
2276
+ }
2277
+ /** 切换子会话模型;成功返回新模型展示名,失败 null(best-effort)。 */
2278
+ async function switchModel(ctx, sessionId, provider, model) {
2279
+ try {
2280
+ return (await ctx.connection.api.sessions.selectModel({
2281
+ sessionId,
2282
+ provider,
2283
+ model
2284
+ })).result.ok ? model : null;
2285
+ } catch {
2286
+ return null;
2287
+ }
2288
+ }
2289
+ //#endregion
2290
+ //#region src/client/sidechat/ModelMenu.tsx
2291
+ /**
2292
+ * 模型选择器(WI-02):composer 脚部的模型 chip → 两级 Menu(provider 分组
2293
+ * 标签 + 型号行)。数据走 wire 面(sessions.models/selectModel——fork 时
2294
+ * 同步主会话的同款通道);切的是子会话自己的选择,不回写主会话。
2295
+ *
2296
+ * 懒加载:首次打开才拉目录(models RPC 有成本);切换后本地标签立即更新
2297
+ * (selectModel 返回的 selected 为准)。
2298
+ */
2299
+ function ModelMenu(props) {
2300
+ useLocaleTick();
2301
+ const [open, setOpen] = (0, react.useState)(false);
2302
+ const [models, setModels] = (0, react.useState)(null);
2303
+ const [switching, setSwitching] = (0, react.useState)(false);
2304
+ const openMenu = () => {
2305
+ setOpen(true);
2306
+ if (models !== null) return;
2307
+ listModels(props.ctx, props.sessionId).then((m) => {
2308
+ if (m !== null) setModels(m);
2309
+ });
2310
+ };
2311
+ const items = [];
2312
+ if (models !== null) for (const group of models.groups) {
2313
+ items.push({
2314
+ type: "label",
2315
+ id: `g:${group.id}`,
2316
+ text: group.name
2317
+ });
2318
+ for (const m of group.models) {
2319
+ const selected = models.current.provider === group.id && models.current.model === m.id;
2320
+ items.push({
2321
+ id: `${group.id}/${m.id}`,
2322
+ label: m.name,
2323
+ ...selected ? { icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 12 }) } : {}
2324
+ });
2325
+ }
2326
+ }
2327
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2328
+ open,
2329
+ side: "top",
2330
+ portal: true,
2331
+ compact: true,
2332
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2333
+ type: "button",
2334
+ className: sidechat_module_css_default.modelSelect,
2335
+ title: t("modelSwitchTitle"),
2336
+ onClick: () => {
2337
+ open ? setOpen(false) : openMenu();
2338
+ },
2339
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("modelLabel", { name: props.modelName ?? t("modelFollowsMain") }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 12 })]
2340
+ }),
2341
+ items,
2342
+ onClose: () => {
2343
+ setOpen(false);
2344
+ },
2345
+ onSelect: (id) => {
2346
+ const slash = id.indexOf("/");
2347
+ if (slash === -1 || models === null) return;
2348
+ const provider = id.slice(0, slash);
2349
+ const model = id.slice(slash + 1);
2350
+ setOpen(false);
2351
+ setSwitching(true);
2352
+ switchModel(props.ctx, props.sessionId, provider, model).then((name) => {
2353
+ if (name !== null) props.onSwitched(name);
2354
+ }).finally(() => {
2355
+ setSwitching(false);
2356
+ });
2357
+ }
1049
2358
  });
1050
2359
  }
1051
- /** 空状态:💬 类图标 + 标题 + fork 语义文案(形态规格)。 */
1052
- function EmptyState() {
2360
+ //#endregion
2361
+ //#region src/client/sidechat/PermissionChip.tsx
2362
+ /**
2363
+ * 权限 chip(WI-02,D2 定档 c:同权 + 常显):显示子会话当前权限档位,
2364
+ * 点击弹 Menu 切换(原生同路径:`session.command('/permission <id>')`)。
2365
+ *
2366
+ * 数据:session.projections.faceOf('permissions')(off-face——缺席则 chip
2367
+ * 不渲染)。KnobState = {preset, sandbox, approval},preset null = 组合默认
2368
+ * (默认表 workspace-write/danger-full-access,dsh-permission-presets 实证)。
2369
+ */
2370
+ /** preset 机器值 → 展示名(宿主 displayPermissionPreset 同款:Full access 特例)。 */
2371
+ function presetLabel(preset) {
2372
+ if (preset === "danger-full-access") return t("permFullAccess");
2373
+ return preset.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2374
+ }
2375
+ /** 默认表(dsh-permission-presets bundle 实证,0.1.1/0.1.2 同)。 */
2376
+ const KNOWN_PRESETS = ["workspace-write", "danger-full-access"];
2377
+ function PermissionChip(props) {
1053
2378
  useLocaleTick();
1054
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1055
- className: sidechat_module_css_default.empty,
1056
- children: [
1057
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1058
- className: sidechat_module_css_default.emptyIcon,
1059
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutline16, { size: 32 })
1060
- }),
1061
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1062
- className: sidechat_module_css_default.emptyTitle,
1063
- children: t("emptyTitle")
1064
- }),
1065
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1066
- className: sidechat_module_css_default.emptyText,
1067
- children: t("emptyText")
1068
- })
1069
- ]
2379
+ const [open, setOpen] = (0, react.useState)(false);
2380
+ const face = (0, react.useMemo)(() => {
2381
+ const p = props.session;
2382
+ return typeof p?.projections?.faceOf === "function" ? p.projections.faceOf("permissions") : void 0;
2383
+ }, [props.session]);
2384
+ const knob = (0, react.useSyncExternalStore)((fn) => face?.subscribe(fn) ?? (() => {}), () => face?.getSnapshot() ?? null);
2385
+ if (face === void 0 || knob === null) return null;
2386
+ const effective = knob.preset ?? "workspace-write";
2387
+ const items = KNOWN_PRESETS.map((id) => ({
2388
+ id,
2389
+ label: presetLabel(id) + (id === effective ? " ✓" : "")
2390
+ }));
2391
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2392
+ open,
2393
+ side: "top",
2394
+ portal: true,
2395
+ compact: true,
2396
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2397
+ type: "button",
2398
+ className: sidechat_module_css_default.modelSelect,
2399
+ title: t("permChipTitle"),
2400
+ onClick: () => {
2401
+ setOpen(!open);
2402
+ },
2403
+ children: [
2404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconInspectOutline12, { size: 12 }),
2405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: presetLabel(effective) }),
2406
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 12 })
2407
+ ]
2408
+ }),
2409
+ items,
2410
+ onClose: () => {
2411
+ setOpen(false);
2412
+ },
2413
+ onSelect: (id) => {
2414
+ setOpen(false);
2415
+ props.session?.command(`/permission ${id}`).catch(() => {});
2416
+ }
1070
2417
  });
1071
2418
  }
1072
- /** 加载 / 错误整屏态(标题 + 可选详情 + 可选指引)。 */
1073
- function StateScreen(props) {
2419
+ //#endregion
2420
+ //#region src/client/sidechat/composer/slash.tsx
2421
+ /**
2422
+ * 侧边 composer 的斜杠/@ 触发菜单(WI-02):官方触发引擎(inputTriggers
2423
+ * 控制器的公开契约面:track/arbitrate/pick/dismiss + menu store)+
2424
+ * 自绘菜单皮(MenuView 不导出——技术审查 A5 证伪,primitives Menu 做皮)。
2425
+ *
2426
+ * 接线面(全是公开契约,非 off-face 键盘面):
2427
+ * - onChange → controller.track(draft, caret, guardOf(phase), draftRev)
2428
+ * - onKeyDown(菜单开着)→ controller.arbitrate(key, composing)
2429
+ * - 点击候选 → controller.pick(source, index)
2430
+ * - 外部点击/Esc → controller.dismiss()
2431
+ *
2432
+ * 全局 source 涌入裁决(C-6):侧边会话的菜单会涌入全局注册的 source
2433
+ * (/side 自己、skill、@file 等)——「不可嵌套」把 /side、/侧边 过滤掉
2434
+ * (source 名按注册名匹配),其余放行(与主对话同能力)。
2435
+ */
2436
+ /** TriggerGuard 推导(宿主 guardOf 的 3 行复刻:plain 全活 / claimed 只活 @ / 其余全灭)。 */
2437
+ function guardOf(phase) {
2438
+ if (phase === "plain") return "plain";
2439
+ if (phase === "claimed") return "claimed";
2440
+ return "frozen";
2441
+ }
2442
+ /** 侧边会话里要过滤的候选名(不可嵌套 P2-3:侧聊里不再开侧聊)。
2443
+ * 注意过滤粒度是**候选名**而非 source 组名——/side 是 commands 源里的一个
2444
+ * 候选,组名是宿主命令源(过滤组名会误杀整组)。 */
2445
+ const EXCLUDED_NAMES = /* @__PURE__ */ new Set(["side", "侧边"]);
2446
+ /** 解析侧边会话的触发控制器(缺席/异常 → null,菜单整体不出现)。 */
2447
+ function resolveTriggerController(ctx, childId) {
2448
+ if (childId === void 0) return null;
2449
+ try {
2450
+ const actx = ctx.sessions.scope(childId);
2451
+ if (actx === void 0) return null;
2452
+ const controller = ctx.get("inputTriggers")?.sessionOf?.(actx);
2453
+ if (controller === void 0 || controller === null) return null;
2454
+ if (typeof controller.track !== "function" || typeof controller.pick !== "function") return null;
2455
+ return controller;
2456
+ } catch {
2457
+ return null;
2458
+ }
2459
+ }
2460
+ /** 订阅控制器 menu store;controller 缺席返回 null。 */
2461
+ function useSlashMenuState(controller) {
2462
+ return (0, react.useSyncExternalStore)((fn) => controller?.menu.subscribe(fn) ?? (() => {}), () => controller?.menu.getSnapshot() ?? null);
2463
+ }
2464
+ /** 过滤后的可视分组(排除不可嵌套候选名 + 空组;候选保留原始索引供 pick)。 */
2465
+ function visibleGroups(state) {
2466
+ if (state === null || !state.open) return [];
2467
+ return state.groups.map((g) => ({
2468
+ ...g,
2469
+ items: g.items.map((item, index) => ({
2470
+ item,
2471
+ index
2472
+ })).filter((entry) => !EXCLUDED_NAMES.has(entry.item.name))
2473
+ })).filter((g) => g.items.length > 0);
2474
+ }
2475
+ /** 键盘键 → 仲裁键映射(非仲裁键返回 null = 放行)。 */
2476
+ function arbitrateKeyOf(key) {
2477
+ if (key === "ArrowUp") return "up";
2478
+ if (key === "ArrowDown") return "down";
2479
+ if (key === "Enter") return "enter";
2480
+ if (key === "Escape") return "escape";
2481
+ return null;
2482
+ }
2483
+ /**
2484
+ * 斜杠菜单浮层(composer 上方,绝对定位)。分组渲染(source 组头)+
2485
+ * 高亮跟随 controller 的 highlight store + 点击 pick。
2486
+ */
2487
+ function SlashMenuView(props) {
2488
+ const groups = visibleGroups(props.state);
2489
+ if (groups.length === 0) return null;
2490
+ const highlight = props.state.highlight;
2491
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2492
+ className: sidechat_module_css_default.slashMenu,
2493
+ role: "listbox",
2494
+ "aria-label": "commands",
2495
+ children: groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2496
+ className: sidechat_module_css_default.slashGroupTitle,
2497
+ children: group.source
2498
+ }), group.items.map(({ item, index }) => {
2499
+ const active = highlight !== null && highlight.source === group.source && highlight.index === index;
2500
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2501
+ type: "button",
2502
+ role: "option",
2503
+ "aria-selected": active,
2504
+ className: active ? sidechat_module_css_default.slashItemActive : sidechat_module_css_default.slashItem,
2505
+ onMouseDown: (event) => {
2506
+ event.preventDefault();
2507
+ props.onPick(group.source, index);
2508
+ },
2509
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2510
+ className: sidechat_module_css_default.slashItemName,
2511
+ children: [typeof item.icon === "string" && item.icon !== "" ? `${item.icon} ` : "", item.name]
2512
+ }), item.description !== void 0 && item.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2513
+ className: sidechat_module_css_default.slashItemDetail,
2514
+ children: item.description
2515
+ })]
2516
+ }, `${group.source}:${index}`);
2517
+ })] }, group.source))
2518
+ });
2519
+ }
2520
+ //#endregion
2521
+ //#region src/client/sidechat/ComposerBar.tsx
2522
+ /**
2523
+ * 侧边聊天 composer(L2):自绘输入壳 + 斜杠/@ 菜单接线 + 模型/权限 chip +
2524
+ * 附件 rail。自 SideChatPanel.tsx 拆出(WI-02 主战场,面板壳只留编排)。
2525
+ */
2526
+ /** 底部 composer:自绘输入框;模型标签显示子会话真实当前模型(fork 时同步主会话选择)。 */
2527
+ function ComposerBar(props) {
1074
2528
  useLocaleTick();
2529
+ const { session, composer, running, visible } = props;
2530
+ const childIdForMenu = props.childId;
2531
+ const inputRef = (0, react.useRef)(null);
2532
+ const fileInputRef = (0, react.useRef)(null);
2533
+ const triggerCtl = (0, react.useMemo)(() => resolveTriggerController(props.ctx, props.childId), [props.ctx, props.childId]);
2534
+ const menuState = useSlashMenuState(triggerCtl);
2535
+ const menuOpen = menuState?.open === true;
2536
+ (0, react.useEffect)(() => {
2537
+ if (visible) inputRef.current?.focus();
2538
+ }, [visible]);
1075
2539
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1076
- className: sidechat_module_css_default.stateScreen,
2540
+ className: sidechat_module_css_default.composer,
1077
2541
  children: [
1078
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1079
- className: sidechat_module_css_default.emptyIcon,
1080
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutline16, { size: 32 })
1081
- }),
1082
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1083
- className: sidechat_module_css_default.emptyTitle,
1084
- children: props.title
2542
+ menuOpen && menuState !== null && triggerCtl !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SlashMenuView, {
2543
+ state: menuState,
2544
+ onPick: (source, index) => {
2545
+ triggerCtl.pick(source, index);
2546
+ }
1085
2547
  }),
1086
- props.detail !== void 0 && props.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1087
- className: sidechat_module_css_default.stateDetail,
1088
- children: props.detail
2548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2549
+ className: sidechat_module_css_default.composerCard,
2550
+ children: [
2551
+ composer.imagePreviews.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2552
+ className: sidechat_module_css_default.attachRail,
2553
+ children: composer.imagePreviews.map((img) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2554
+ className: sidechat_module_css_default.attachThumb,
2555
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2556
+ src: img.url,
2557
+ alt: "",
2558
+ className: sidechat_module_css_default.attachImg
2559
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2560
+ type: "button",
2561
+ className: sidechat_module_css_default.attachRemove,
2562
+ "aria-label": t("attachRemove"),
2563
+ onClick: () => {
2564
+ composer.removeImage(img.id);
2565
+ },
2566
+ children: "×"
2567
+ })]
2568
+ }, img.id))
2569
+ }),
2570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2571
+ ref: inputRef,
2572
+ className: sidechat_module_css_default.input,
2573
+ placeholder: t("inputPlaceholder"),
2574
+ value: composer.draft,
2575
+ onChange: (event) => {
2576
+ composer.setDraft(event.target.value);
2577
+ triggerCtl?.track(event.target.value, event.target.selectionStart ?? event.target.value.length, guardOf(composer.phase), composer.draftRev ?? 0);
2578
+ },
2579
+ onKeyDown: (event) => {
2580
+ if (menuOpen && triggerCtl !== null) {
2581
+ const arbKey = arbitrateKeyOf(event.key);
2582
+ if (arbKey !== null) {
2583
+ if (triggerCtl.arbitrate(arbKey, event.nativeEvent.isComposing) !== "pass") {
2584
+ event.preventDefault();
2585
+ return;
2586
+ }
2587
+ }
2588
+ }
2589
+ if (event.key !== "Enter" || event.shiftKey) return;
2590
+ if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
2591
+ event.preventDefault();
2592
+ composer.submit(event.metaKey || event.ctrlKey ? "steer" : void 0);
2593
+ }
2594
+ }),
2595
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2596
+ className: sidechat_module_css_default.composerFoot,
2597
+ children: [
2598
+ composer.canAttach && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2599
+ type: "button",
2600
+ className: sidechat_module_css_default.attachButton,
2601
+ title: t("attachTitle"),
2602
+ "aria-label": t("attachTitle"),
2603
+ onClick: () => {
2604
+ fileInputRef.current?.click();
2605
+ },
2606
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPaperclipOutline16, { size: 14 })
2607
+ }),
2608
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2609
+ ref: fileInputRef,
2610
+ type: "file",
2611
+ accept: "image/*",
2612
+ multiple: true,
2613
+ hidden: true,
2614
+ onChange: (event) => {
2615
+ const files = [...event.target.files ?? []];
2616
+ if (files.length > 0) composer.attachImages(files);
2617
+ event.target.value = "";
2618
+ }
2619
+ }),
2620
+ childIdForMenu !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2621
+ className: sidechat_module_css_default.composerChips,
2622
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionChip, { session }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
2623
+ ctx: props.ctx,
2624
+ sessionId: childIdForMenu,
2625
+ modelName: props.modelName,
2626
+ onSwitched: props.onModelSwitched
2627
+ })]
2628
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2629
+ className: sidechat_module_css_default.modelLabel,
2630
+ children: t("modelLabel", { name: props.modelName ?? t("modelFollowsMain") })
2631
+ }),
2632
+ running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2633
+ type: "button",
2634
+ className: sidechat_module_css_default.stopButton,
2635
+ title: t("stopReplyTitle"),
2636
+ "aria-label": t("stopReplyTitle"),
2637
+ onClick: () => {
2638
+ session?.cancel().catch(() => {});
2639
+ },
2640
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFill16, { size: 12 })
2641
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2642
+ type: "button",
2643
+ className: sidechat_module_css_default.sendButton,
2644
+ title: t("send"),
2645
+ "aria-label": t("send"),
2646
+ disabled: composer.draft.trim() === "",
2647
+ onClick: () => {
2648
+ composer.submit();
2649
+ },
2650
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSendOutline16, { size: 14 })
2651
+ })
2652
+ ]
2653
+ })
2654
+ ]
1089
2655
  }),
1090
- props.hint !== void 0 && props.hint !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1091
- className: sidechat_module_css_default.emptyText,
1092
- children: props.hint
2656
+ composer.sendError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2657
+ className: sidechat_module_css_default.errorRow,
2658
+ children: composer.sendError
1093
2659
  })
1094
2660
  ]
1095
2661
  });
1096
2662
  }
1097
- function MessageList({ messages, reflow, parentSessionId, sideTitle }) {
1098
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1099
- className: sidechat_module_css_default.transcript,
1100
- children: messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageRow, {
1101
- message,
1102
- reflow,
1103
- parentSessionId,
1104
- sideTitle
1105
- }, message.key))
1106
- });
1107
- }
1108
- /** 回流按钮(W04 v2):把这条 assistant 结论收为主会话的受控回流对象
1109
- * (主 composer 上方出现「侧边回流」chip),发送时随拦截器序列化。 */
1110
- function ReflowButton({ reflow, parentSessionId, sideTitle, text }) {
1111
- useLocaleTick();
1112
- const [done, setDone] = (0, react.useState)(false);
1113
- const timer = (0, react.useRef)(0);
1114
- (0, react.useEffect)(() => () => {
1115
- window.clearTimeout(timer.current);
1116
- }, []);
1117
- if (parentSessionId === void 0) return null;
1118
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1119
- type: "button",
1120
- className: sidechat_module_css_default.reflowButton,
1121
- title: done ? t("reflowDone") : t("reflowToMain"),
1122
- "aria-label": t("reflowToMain"),
1123
- onClick: () => {
1124
- reflow.add(parentSessionId, sideTitle, text);
1125
- setDone(true);
1126
- window.clearTimeout(timer.current);
1127
- timer.current = window.setTimeout(() => {
1128
- setDone(false);
1129
- }, 1600);
2663
+ //#endregion
2664
+ //#region src/client/chat/viewState.ts
2665
+ function createFoldStore(storageKey) {
2666
+ const open = /* @__PURE__ */ new Map();
2667
+ let version = 0;
2668
+ const listeners = /* @__PURE__ */ new Set();
2669
+ if (storageKey !== void 0 && typeof localStorage !== "undefined") try {
2670
+ const raw = localStorage.getItem(storageKey);
2671
+ if (raw !== null) {
2672
+ const parsed = JSON.parse(raw);
2673
+ if (typeof parsed === "object" && parsed !== null) {
2674
+ for (const [k, v] of Object.entries(parsed)) if (v === true) open.set(k, true);
2675
+ }
2676
+ }
2677
+ } catch (error) {
2678
+ console.warn("[dsh-sidenote] 折叠态恢复失败(按空起步):", error);
2679
+ }
2680
+ const persist = () => {
2681
+ if (storageKey === void 0 || typeof localStorage === "undefined") return;
2682
+ try {
2683
+ if (open.size === 0) localStorage.removeItem(storageKey);
2684
+ else localStorage.setItem(storageKey, JSON.stringify(Object.fromEntries(open)));
2685
+ } catch {}
2686
+ };
2687
+ const notify = () => {
2688
+ version += 1;
2689
+ for (const fn of [...listeners]) fn();
2690
+ persist();
2691
+ };
2692
+ return {
2693
+ getSnapshot: () => version,
2694
+ subscribe(fn) {
2695
+ listeners.add(fn);
2696
+ return () => {
2697
+ listeners.delete(fn);
2698
+ };
1130
2699
  },
1131
- children: done ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 12 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconShareOutline16, { size: 12 })
1132
- });
2700
+ isOpen: (key) => open.get(key) === true,
2701
+ has: (key) => open.has(key),
2702
+ toggle(key) {
2703
+ open.set(key, open.get(key) !== true);
2704
+ notify();
2705
+ },
2706
+ setAll(keys, value) {
2707
+ for (const key of keys) open.set(key, value);
2708
+ notify();
2709
+ }
2710
+ };
1133
2711
  }
1134
- function MessageRow({ message, reflow, parentSessionId, sideTitle }) {
1135
- useLocaleTick();
1136
- switch (message.role) {
1137
- case "user": {
1138
- const proto = splitProtocolPrefix(message.text);
1139
- if (proto !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1140
- className: sidechat_module_css_default.userRow,
1141
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1142
- className: sidechat_module_css_default.userBubble,
1143
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1144
- className: sidechat_module_css_default.sentChipRow,
1145
- children: [proto.annotations.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1146
- className: sidechat_module_css_default.sentChip,
1147
- title: proto.annotations.map((a) => `${a.id}. 「${a.quote}」${a.note}`).join("\n"),
1148
- children: t("sentChipLabel", { n: proto.annotations.length })
1149
- }), proto.reflows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1150
- className: sidechat_module_css_default.sentChip,
1151
- title: proto.reflows.map((r) => `${r.source}: ${r.content.slice(0, 200)}`).join("\n"),
1152
- children: t("reflowBubbleLabel")
1153
- })]
1154
- }), message.text.slice(proto.length)]
1155
- })
1156
- });
1157
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1158
- className: sidechat_module_css_default.userRow,
1159
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1160
- className: sidechat_module_css_default.userBubble,
1161
- children: message.text
1162
- })
1163
- });
2712
+ //#endregion
2713
+ //#region src/client/sidechat/toast.tsx
2714
+ /**
2715
+ * 全局瞬态通知(P4 确认反馈):body 级独立 React 根,寿命不随任何 Tab——
2716
+ * 「保存为正式会话」后主视图跳转 + 侧聊 Tab 关闭,面板自己的组件树已卸载,
2717
+ * toast 必须挂在更长寿的宿主上(annotate overlay 同款 ctx.effect 模式:
2718
+ * HMR/插件禁用时 fiber 撤销即清理)。
2719
+ */
2720
+ let current = null;
2721
+ const listeners = /* @__PURE__ */ new Set();
2722
+ function emit() {
2723
+ for (const fn of listeners) fn();
2724
+ }
2725
+ /** 全局瞬态通知入口(任何模块可调;未挂载宿主时静默丢弃)。 */
2726
+ function showToast(text) {
2727
+ current = {
2728
+ seq: (current?.seq ?? 0) + 1,
2729
+ text
2730
+ };
2731
+ emit();
2732
+ }
2733
+ function subscribe(fn) {
2734
+ listeners.add(fn);
2735
+ return () => {
2736
+ listeners.delete(fn);
2737
+ };
2738
+ }
2739
+ function ToastHost() {
2740
+ const snap = (0, react.useSyncExternalStore)(subscribe, () => current);
2741
+ if (snap === null) return null;
2742
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
2743
+ text: snap.text,
2744
+ onDone: () => {
2745
+ current = null;
2746
+ emit();
1164
2747
  }
1165
- case "assistant": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1166
- className: sidechat_module_css_default.assistantRow,
1167
- children: [message.text !== "" && message.streaming !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1168
- className: sidechat_module_css_default.rowActions,
1169
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReflowButton, {
1170
- reflow,
1171
- parentSessionId,
1172
- sideTitle,
1173
- text: message.text
1174
- })
1175
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1176
- className: sidechat_module_css_default.assistantBody,
1177
- children: [
1178
- message.reasoning !== void 0 && message.reasoning !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
1179
- className: sidechat_module_css_default.reasoning,
1180
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("thinking") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1181
- className: sidechat_module_css_default.reasoningBody,
1182
- children: message.reasoning
1183
- })]
1184
- }),
1185
- message.text !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
1186
- text: message.text,
1187
- streaming: message.streaming,
1188
- codeLabels: codeLabels()
1189
- }) : message.streaming === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1190
- className: sidechat_module_css_default.streamingHint,
1191
- children: t("writing")
1192
- }),
1193
- message.interrupted === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1194
- className: sidechat_module_css_default.noticeRow,
1195
- children: t("stopped")
1196
- })
1197
- ]
1198
- })]
1199
- });
1200
- case "tool": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1201
- className: sidechat_module_css_default.toolCard,
1202
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1203
- className: sidechat_module_css_default.toolHead,
1204
- children: [
1205
- t("toolLabel"),
1206
- " · ",
1207
- message.toolName,
1208
- message.isError === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1209
- className: sidechat_module_css_default.toolError,
1210
- children: t("failed")
1211
- }),
1212
- message.streaming === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1213
- className: sidechat_module_css_default.toolRunning,
1214
- children: t("running")
1215
- })
1216
- ]
1217
- }), message.text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1218
- className: sidechat_module_css_default.toolBody,
1219
- children: message.text
1220
- })]
2748
+ }, snap.seq);
2749
+ }
2750
+ /** 挂载全局 toast 宿主(registerSideChat 里调用一次)。 */
2751
+ function registerToastHost(ctx) {
2752
+ ctx.effect(() => {
2753
+ const host = document.createElement("div");
2754
+ host.dataset.dshSidenoteToast = "";
2755
+ document.body.appendChild(host);
2756
+ const root = (0, react_dom_client.createRoot)(host);
2757
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToastHost, {}));
2758
+ return () => {
2759
+ root.unmount();
2760
+ host.remove();
2761
+ };
2762
+ }, "dsh-sidenote: toast host");
2763
+ }
2764
+ //#endregion
2765
+ //#region src/client/sidechat/SideChatPanel.tsx
2766
+ /**
2767
+ * 侧边聊天 Tab 面板(L2 薄视图):相位编排 + 消息流 + composer。
2768
+ * 宿主接线(fork/archive/模型同步/Tab meta/窗口打开)全部在 lifecycle.ts(L3)——
2769
+ * 本文件无直接宿主调用(WI-00 分层纪律)。
2770
+ *
2771
+ * 打开流程(design.md 详细方案 2):组件挂载时 tab.meta 无 childId →
2772
+ * lifecycle.forkAndRegister(fork 全量快照 → 归档隐藏 → meta 登记 → 模型同步)。
2773
+ * 恢复流程:有 meta.childId → ctx.sessions.binding(childId) 直接绑定;
2774
+ * 列表就绪后仍不在列 → 「会话已不存在」态。
2775
+ *
2776
+ * 消息流:binding.session 快照订阅(useSyncExternalStore),visible=false 时
2777
+ * 暂停订阅。非 staged 会话需 off-face open() 开窗(lifecycle.openSessionWindow),
2778
+ * 否则消息流永远为空(client-runtime 只为 staged 会话开窗的已知偏差)。
2779
+ */
2780
+ const NOOP_UNSUBSCRIBE = () => {};
2781
+ /** D2 写型工具名(无 diff 卡时的名字兜底)。 */
2782
+ const WRITE_TOOLS = /* @__PURE__ */ new Set([
2783
+ "edit",
2784
+ "write",
2785
+ "str-replace-editor",
2786
+ "str_replace_editor"
2787
+ ]);
2788
+ /** 首次写提示的一次性 localStorage 键。 */
2789
+ const WRITE_NOTICE_KEY = "dsh-sidenote:write-notice:v1";
2790
+ function SideChatPanel(props) {
2791
+ useLocaleTick();
2792
+ const { ctx, scope, tab, visible } = props;
2793
+ const meta = parseSideChatMeta(tab.meta);
2794
+ const childId = meta.childId;
2795
+ const [forkError, setForkError] = (0, react.useState)(null);
2796
+ const forkStarted = (0, react.useRef)(false);
2797
+ const { store } = props;
2798
+ (0, react.useEffect)(() => {
2799
+ ensurePanelOpen(store);
2800
+ }, [store]);
2801
+ (0, react.useEffect)(() => {
2802
+ if (childId !== void 0 || forkStarted.current) return;
2803
+ forkStarted.current = true;
2804
+ let cancelled = false;
2805
+ forkAndRegister(ctx, scope.sessionId, tab.id).catch((error) => {
2806
+ if (!cancelled) setForkError(error instanceof Error ? error.message : String(error));
1221
2807
  });
1222
- case "error": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1223
- className: sidechat_module_css_default.errorRow,
1224
- children: message.text
2808
+ return () => {
2809
+ cancelled = true;
2810
+ };
2811
+ }, [
2812
+ ctx,
2813
+ scope.sessionId,
2814
+ tab.id,
2815
+ childId
2816
+ ]);
2817
+ const listSnap = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => ctx.sessions.list.subscribe(notify), [ctx]), () => ctx.sessions.list.getSnapshot());
2818
+ const listed = childId !== void 0 && listSnap.byId?.[childId] !== void 0;
2819
+ const session = (childId === void 0 ? void 0 : ctx.sessions.binding(childId))?.session;
2820
+ (0, react.useEffect)(() => {
2821
+ openSessionWindow(session);
2822
+ }, [session]);
2823
+ const phase = phaseOf({
2824
+ childId,
2825
+ forkError,
2826
+ bound: session !== void 0,
2827
+ listPhase: listSnap.phase,
2828
+ listed
2829
+ });
2830
+ 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());
2831
+ const chat = (0, react.useMemo)(() => chatSourceOf(ctx, childId === void 0 ? void 0 : ctx.sessions.binding(childId)), [
2832
+ ctx,
2833
+ childId,
2834
+ session
2835
+ ]);
2836
+ const chatLegacy = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => visible && chat !== void 0 ? chat.subscribe(notify) : NOOP_UNSUBSCRIBE, [visible, chat]), () => chat?.getLegacy() ?? null);
2837
+ const messages = (0, react.useMemo)(() => transcriptOf(chatLegacy ?? snapshot), [chatLegacy, snapshot]);
2838
+ const fold = (0, react.useMemo)(() => createFoldStore(childId === void 0 ? void 0 : `dsh-sidenote:fold:v1:${childId}`), [childId]);
2839
+ const parentSession = meta.parentSessionId === void 0 ? void 0 : ctx.sessions.binding(meta.parentSessionId)?.session;
2840
+ const parentSnap = (0, react.useSyncExternalStore)((0, react.useCallback)((notify) => visible && parentSession !== void 0 ? parentSession.subscribe(notify) : NOOP_UNSUBSCRIBE, [visible, parentSession]), () => parentSession === void 0 ? null : parentSession.getSnapshot());
2841
+ const [writeNoticeDismissed, setWriteNoticeDismissed] = (0, react.useState)(() => typeof localStorage !== "undefined" && localStorage.getItem(WRITE_NOTICE_KEY) === "1");
2842
+ const composer = useComposer(ctx, session, childId);
2843
+ const [modelName, setModelName] = (0, react.useState)(null);
2844
+ (0, react.useEffect)(() => {
2845
+ if (childId === void 0 || session === void 0) return;
2846
+ let cancelled = false;
2847
+ readModelName(ctx, childId).then((name) => {
2848
+ if (!cancelled && name !== null) setModelName(name);
1225
2849
  });
1226
- case "notice": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1227
- className: sidechat_module_css_default.noticeRow,
1228
- children: message.text
2850
+ return () => {
2851
+ cancelled = true;
2852
+ };
2853
+ }, [
2854
+ ctx,
2855
+ childId,
2856
+ session
2857
+ ]);
2858
+ const pendingDraft = meta.pendingDraft;
2859
+ const rootRef = (0, react.useRef)(null);
2860
+ (0, react.useEffect)(() => {
2861
+ if (pendingDraft === void 0 || pendingDraft === "" || phase !== "chat") return;
2862
+ composer.appendDraft(pendingDraft);
2863
+ updateTabMeta(ctx, tab.id, clearPendingDraft);
2864
+ requestAnimationFrame(() => {
2865
+ rootRef.current?.querySelector("textarea")?.focus();
1229
2866
  });
1230
- }
1231
- }
1232
- /** 底部 composer:自绘输入框;模型标签显示子会话真实当前模型(fork 时同步主会话选择)。 */
1233
- function ComposerBar(props) {
1234
- useLocaleTick();
1235
- const { session, composer, running, visible } = props;
1236
- const inputRef = (0, react.useRef)(null);
2867
+ }, [
2868
+ pendingDraft,
2869
+ phase,
2870
+ ctx,
2871
+ tab.id
2872
+ ]);
2873
+ const promote = (0, react.useCallback)(() => {
2874
+ if (childId === void 0) return;
2875
+ (async () => {
2876
+ try {
2877
+ const promoted = await ctx.sessions.fork({
2878
+ sessionId: childId,
2879
+ increaseTitle: true
2880
+ });
2881
+ ctx.sessions.open(promoted);
2882
+ ctx.betterSidebar.closeTab(tab.id, { sessionId: scope.sessionId });
2883
+ showToast(t("promoteDone"));
2884
+ } catch (error) {
2885
+ console.warn("[dsh-sidenote] 保存为正式会话失败:", error);
2886
+ }
2887
+ })();
2888
+ }, [
2889
+ ctx,
2890
+ childId,
2891
+ tab.id,
2892
+ scope.sessionId
2893
+ ]);
1237
2894
  (0, react.useEffect)(() => {
1238
- if (visible) inputRef.current?.focus();
2895
+ if (!visible) return;
2896
+ const onKey = (event) => {
2897
+ if (!event.altKey || event.metaKey || event.ctrlKey || event.code !== "KeyJ") return;
2898
+ event.preventDefault();
2899
+ if (rootRef.current?.contains(document.activeElement) === true) document.querySelector("[data-composer-seat] textarea, [data-composer-seat] [contenteditable]")?.focus();
2900
+ else rootRef.current?.querySelector("textarea")?.focus();
2901
+ };
2902
+ document.addEventListener("keydown", onKey);
2903
+ return () => {
2904
+ document.removeEventListener("keydown", onKey);
2905
+ };
1239
2906
  }, [visible]);
2907
+ const bodyRef = (0, react.useRef)(null);
2908
+ const [nearBottom, setNearBottom] = (0, react.useState)(true);
2909
+ const tailKey = messages.length === 0 ? "" : `${messages[messages.length - 1].key}:${messages[messages.length - 1].text.length}:${messages[messages.length - 1].reasoning?.length ?? 0}`;
2910
+ (0, react.useEffect)(() => {
2911
+ const el = bodyRef.current;
2912
+ if (el !== null && visible && nearBottom) el.scrollTop = el.scrollHeight;
2913
+ }, [
2914
+ tailKey,
2915
+ visible,
2916
+ nearBottom
2917
+ ]);
2918
+ const scrollKey = childId === void 0 ? void 0 : `dsh-sidenote:scroll:v1:${childId}`;
2919
+ const scrollTimer = (0, react.useRef)(0);
2920
+ const onBodyScroll = (0, react.useCallback)(() => {
2921
+ const el = bodyRef.current;
2922
+ if (el === null) return;
2923
+ setNearBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 80);
2924
+ if (scrollKey !== void 0) {
2925
+ window.clearTimeout(scrollTimer.current);
2926
+ scrollTimer.current = window.setTimeout(() => {
2927
+ try {
2928
+ localStorage.setItem(scrollKey, String(el.scrollTop));
2929
+ } catch {}
2930
+ }, 300);
2931
+ }
2932
+ }, [scrollKey]);
2933
+ const jumpToLatest = (0, react.useCallback)(() => {
2934
+ const el = bodyRef.current;
2935
+ if (el !== null) el.scrollTop = el.scrollHeight;
2936
+ setNearBottom(true);
2937
+ }, []);
2938
+ const scrollRestored = (0, react.useRef)(false);
2939
+ (0, react.useEffect)(() => {
2940
+ if (scrollRestored.current || scrollKey === void 0 || messages.length === 0) return;
2941
+ scrollRestored.current = true;
2942
+ let saved = null;
2943
+ try {
2944
+ saved = localStorage.getItem(scrollKey);
2945
+ } catch {}
2946
+ if (saved !== null && bodyRef.current !== null) {
2947
+ bodyRef.current.scrollTop = Number(saved);
2948
+ const el = bodyRef.current;
2949
+ setNearBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 80);
2950
+ }
2951
+ }, [messages.length, scrollKey]);
2952
+ if (phase === "fork-error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateScreen, {
2953
+ title: t("forkErrorTitle"),
2954
+ detail: forkError ?? void 0,
2955
+ hint: t("forkErrorHint")
2956
+ });
2957
+ if (phase === "missing") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateScreen, {
2958
+ title: t("missingTitle"),
2959
+ detail: t("missingDetail"),
2960
+ hint: t("missingHint")
2961
+ });
2962
+ if (phase === "forking" || phase === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateScreen, { title: t("preparing") });
2963
+ const running = snapshot?.running === true;
2964
+ const openFailed = snapshot?.openState === "error";
2965
+ const hasWriteTool = messages.some((m) => m.role === "tool" && (m.card?.kind === "diff" || m.toolName !== void 0 && WRITE_TOOLS.has(m.toolName)));
2966
+ const pendingCount = (snapshot?.pending ?? chatLegacy?.pending ?? []).length;
2967
+ const parentPending = (parentSnap?.pending ?? []).length;
2968
+ const parentState = parentSession === void 0 ? null : parentPending > 0 ? "pending" : parentSnap?.running === true ? "running" : "idle";
1240
2969
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1241
- className: sidechat_module_css_default.composer,
2970
+ ref: rootRef,
2971
+ className: sidechat_module_css_default.root,
1242
2972
  children: [
1243
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1244
- ref: inputRef,
1245
- className: sidechat_module_css_default.input,
1246
- placeholder: t("inputPlaceholder"),
1247
- value: composer.draft,
1248
- onChange: (event) => {
1249
- composer.setDraft(event.target.value);
2973
+ parentState !== null && meta.parentSessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2974
+ type: "button",
2975
+ className: sidechat_module_css_default.parentStrip,
2976
+ title: t("parentStripTitle"),
2977
+ onClick: () => {
2978
+ if (meta.parentSessionId !== void 0) ctx.sessions.open(meta.parentSessionId);
1250
2979
  },
1251
- onKeyDown: (event) => {
1252
- if (event.key !== "Enter" || event.shiftKey) return;
1253
- if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
1254
- event.preventDefault();
1255
- composer.submit();
1256
- }
2980
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: parentState === "running" ? sidechat_module_css_default.dotRunning : parentState === "pending" ? sidechat_module_css_default.dotPending : sidechat_module_css_default.dotIdle }), t(parentState === "running" ? "parentRunning" : parentState === "pending" ? "parentPending" : "parentIdle")]
1257
2981
  }),
1258
2982
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1259
- className: sidechat_module_css_default.composerFoot,
1260
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1261
- className: sidechat_module_css_default.modelLabel,
1262
- children: t("modelLabel", { name: props.modelName ?? t("modelFollowsMain") })
1263
- }), running ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1264
- type: "button",
1265
- className: sidechat_module_css_default.stopButton,
1266
- title: t("stopReplyTitle"),
1267
- onClick: () => {
1268
- session?.cancel().catch(() => {});
1269
- },
1270
- children: [
1271
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFill16, { size: 14 }),
1272
- " ",
1273
- t("stopReply")
1274
- ]
1275
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2983
+ ref: bodyRef,
2984
+ className: sidechat_module_css_default.body,
2985
+ onScroll: onBodyScroll,
2986
+ children: [messages.length === 0 && !running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageList, {
2987
+ messages,
2988
+ fold,
2989
+ boundarySeq: meta.boundarySeq,
2990
+ reflow: props.reflow,
2991
+ parentSessionId: meta.parentSessionId,
2992
+ sideTitle: tab.title,
2993
+ onPromote: promote
2994
+ }), openFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2995
+ className: sidechat_module_css_default.errorRow,
2996
+ children: t("historyFailed")
2997
+ })]
2998
+ }),
2999
+ hasWriteTool && !writeNoticeDismissed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3000
+ className: sidechat_module_css_default.writeNotice,
3001
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("writeNotice") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1276
3002
  type: "button",
1277
- className: sidechat_module_css_default.sendButton,
1278
- disabled: composer.draft.trim() === "",
3003
+ className: sidechat_module_css_default.writeNoticeClose,
3004
+ "aria-label": t("hintClose"),
1279
3005
  onClick: () => {
1280
- composer.submit();
3006
+ setWriteNoticeDismissed(true);
3007
+ try {
3008
+ localStorage.setItem(WRITE_NOTICE_KEY, "1");
3009
+ } catch {}
1281
3010
  },
1282
- children: [
1283
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSendOutline16, { size: 14 }),
1284
- " ",
1285
- t("send")
1286
- ]
3011
+ children: "×"
1287
3012
  })]
1288
3013
  }),
1289
- composer.sendError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1290
- className: sidechat_module_css_default.errorRow,
1291
- children: composer.sendError
3014
+ pendingCount > 0 && childId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3015
+ type: "button",
3016
+ className: sidechat_module_css_default.pendingBar,
3017
+ onClick: () => {
3018
+ ctx.sessions.open(childId);
3019
+ },
3020
+ children: t("pendingNotice")
3021
+ }),
3022
+ !nearBottom && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3023
+ type: "button",
3024
+ className: sidechat_module_css_default.jumpBottom,
3025
+ onClick: jumpToLatest,
3026
+ children: t("jumpToLatest")
3027
+ }),
3028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerBar, {
3029
+ ctx,
3030
+ session,
3031
+ composer,
3032
+ running,
3033
+ visible,
3034
+ modelName,
3035
+ childId,
3036
+ onModelSwitched: setModelName
1292
3037
  })
1293
3038
  ]
1294
3039
  });
1295
3040
  }
1296
3041
  //#endregion
3042
+ //#region src/client/sidechat/recentClosed.ts
3043
+ const KEY_PREFIX = "dsh-sidenote:closed-side:v1:";
3044
+ const CAP = 5;
3045
+ function readAll(storageKey) {
3046
+ try {
3047
+ const raw = localStorage.getItem(storageKey);
3048
+ if (raw === null) return [];
3049
+ const parsed = JSON.parse(raw);
3050
+ if (!Array.isArray(parsed)) return [];
3051
+ return parsed.filter((e) => typeof e === "object" && e !== null && typeof e.childId === "string" && typeof e.parentSessionId === "string" && typeof e.title === "string");
3052
+ } catch {
3053
+ return [];
3054
+ }
3055
+ }
3056
+ function recordClosedSideChat(parentSessionId, entry) {
3057
+ try {
3058
+ const key = KEY_PREFIX + parentSessionId;
3059
+ const list = [entry, ...readAll(key).filter((e) => e.childId !== entry.childId)].slice(0, CAP);
3060
+ localStorage.setItem(key, JSON.stringify(list));
3061
+ } catch {}
3062
+ }
3063
+ /** 读某主会话的最近关闭列表(新者在前)。 */
3064
+ function listClosedSideChats(parentSessionId) {
3065
+ if (typeof localStorage === "undefined") return [];
3066
+ return readAll(KEY_PREFIX + parentSessionId);
3067
+ }
3068
+ /** 重开消费:从列表移除(重开后若再关会重新入列)。 */
3069
+ function dropClosedSideChat(parentSessionId, childId) {
3070
+ try {
3071
+ const key = KEY_PREFIX + parentSessionId;
3072
+ const list = readAll(key).filter((e) => e.childId !== childId);
3073
+ if (list.length === 0) localStorage.removeItem(key);
3074
+ else localStorage.setItem(key, JSON.stringify(list));
3075
+ } catch {}
3076
+ }
3077
+ //#endregion
1297
3078
  //#region src/client/sidechat/header.tsx
1298
3079
  /**
1299
3080
  * 顶栏「侧边」入口(Delivery_02 Workitem_02 发现性):注册进宿主的
@@ -1355,6 +3136,11 @@ window.__ModuleLoader__.load({
1355
3136
  label: t("cmdFocus", { title: tab.title }),
1356
3137
  detail: t("cmdFocusDetail")
1357
3138
  });
3139
+ for (const entry of listClosedSideChats(session.sessionId)) options.push({
3140
+ id: `reopen:${entry.childId}`,
3141
+ label: t("cmdReopen", { title: entry.title }),
3142
+ detail: t("cmdReopenDetail")
3143
+ });
1358
3144
  return Promise.resolve(options);
1359
3145
  },
1360
3146
  onSelect: (option, session) => {
@@ -1362,7 +3148,14 @@ window.__ModuleLoader__.load({
1362
3148
  createSideChat(ctx, session.sessionId);
1363
3149
  return;
1364
3150
  }
1365
- if (option.id.startsWith("focus:")) ctx.betterSidebar.activateTab(option.id.slice(6), { sessionId: session.sessionId });
3151
+ if (option.id.startsWith("focus:")) {
3152
+ ctx.betterSidebar.activateTab(option.id.slice(6), { sessionId: session.sessionId });
3153
+ return;
3154
+ }
3155
+ if (option.id.startsWith("reopen:")) {
3156
+ const childId = option.id.slice(7);
3157
+ if (reopenSideChat(ctx, session.sessionId, childId)) dropClosedSideChat(session.sessionId, childId);
3158
+ }
1366
3159
  }
1367
3160
  }
1368
3161
  };
@@ -1383,6 +3176,80 @@ window.__ModuleLoader__.load({
1383
3176
  }
1384
3177
  }
1385
3178
  //#endregion
3179
+ //#region src/client/sidechat/referenceSource.ts
3180
+ const TRIGGER = "@";
3181
+ const SOURCE = "sidenote-side-chats";
3182
+ /** 侧聊内容 → 结构化引用块(问答成对,全文不截断)。 */
3183
+ async function serializeSideChatRef(ctx, childId) {
3184
+ const snap = chatSourceOf(ctx, ctx.sessions.binding(childId))?.getLegacy();
3185
+ const messages = transcriptOf(snap);
3186
+ const pairs = pairQuestions(messages);
3187
+ const parts = [];
3188
+ for (const m of messages) {
3189
+ if (m.role !== "assistant" || m.text === "" || m.streaming === true) continue;
3190
+ const q = pairs.get(m.key);
3191
+ if (q !== void 0) parts.push(`<问>${q}</问>`);
3192
+ parts.push(`<答>${m.text}</答>`);
3193
+ }
3194
+ return `<side-chat-reference source="${(() => {
3195
+ try {
3196
+ for (const tab of collectSideTabs(ctx.betterSidebar.getSnapshot().state)) if (parseSideChatMeta(tab.meta).childId === childId) return tab.title;
3197
+ } catch {}
3198
+ return childId;
3199
+ })()}" note="用户 @ 引用的侧边聊天内容(问答成对)">\n${parts.join("\n")}\n</side-chat-reference>`;
3200
+ }
3201
+ /** 注册「@ 侧聊」触发源;服务缺席静默降级(不影响其他入口)。 */
3202
+ function registerSideChatReferenceSource(ctx) {
3203
+ let svc;
3204
+ try {
3205
+ svc = ctx.get("inputTriggers");
3206
+ } catch {
3207
+ return;
3208
+ }
3209
+ if (svc === void 0 || typeof svc.registerSource !== "function") return;
3210
+ try {
3211
+ ctx.effect(() => svc.registerSource({
3212
+ trigger: TRIGGER,
3213
+ name: SOURCE,
3214
+ order: 50,
3215
+ candidates: (session) => {
3216
+ try {
3217
+ const snapshot = ctx.betterSidebar.getSnapshot();
3218
+ if (session?.sessionId === void 0) return Promise.resolve([]);
3219
+ if (snapshot.sessionId !== session.sessionId || snapshot.state === void 0) return Promise.resolve([]);
3220
+ return Promise.resolve(collectSideTabs(snapshot.state).map((tab) => ({
3221
+ name: tab.title,
3222
+ description: "侧边聊天",
3223
+ icon: "💬"
3224
+ })));
3225
+ } catch {
3226
+ return Promise.resolve([]);
3227
+ }
3228
+ },
3229
+ onPick: (pick, session) => {
3230
+ const snapshot = ctx.betterSidebar.getSnapshot();
3231
+ if (session?.sessionId === void 0) return void 0;
3232
+ if (snapshot.sessionId !== session.sessionId || snapshot.state === void 0) return void 0;
3233
+ const tab = collectSideTabs(snapshot.state).find((t) => t.title === pick.candidate.name);
3234
+ const childId = tab === void 0 ? void 0 : parseSideChatMeta(tab.meta).childId;
3235
+ if (childId === void 0) return void 0;
3236
+ return { insert: {
3237
+ source: SOURCE,
3238
+ ref: childId,
3239
+ label: pick.candidate.name,
3240
+ clipboardText: `@${pick.candidate.name}`
3241
+ } };
3242
+ },
3243
+ codec: {
3244
+ clipboardText: (ref) => `@${ref}`,
3245
+ serialize: (ref, _signal) => serializeSideChatRef(ctx, ref)
3246
+ }
3247
+ }), "dsh-sidenote: @ side-chat reference source");
3248
+ } catch (error) {
3249
+ console.warn("[dsh-sidenote] @ 引用源注册失败:", error);
3250
+ }
3251
+ }
3252
+ //#endregion
1386
3253
  //#region src/client/sidechat/index.tsx
1387
3254
  /**
1388
3255
  * Workitem 01 — 侧边聊天面板。
@@ -1413,6 +3280,15 @@ window.__ModuleLoader__.load({
1413
3280
  type: SIDE_TAB_TYPE,
1414
3281
  title: sideTabTitle(collectSideTabs(state).map((tab) => tab.title))
1415
3282
  } }),
3283
+ onClose: (closedTab) => {
3284
+ const meta = parseSideChatMeta(closedTab.meta);
3285
+ if (meta.childId !== void 0 && meta.parentSessionId !== void 0) recordClosedSideChat(meta.parentSessionId, {
3286
+ childId: meta.childId,
3287
+ parentSessionId: meta.parentSessionId,
3288
+ title: closedTab.title,
3289
+ closedAt: Date.now()
3290
+ });
3291
+ },
1416
3292
  component: (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SideChatPanel, {
1417
3293
  ...props,
1418
3294
  reflow
@@ -1429,7 +3305,9 @@ window.__ModuleLoader__.load({
1429
3305
  };
1430
3306
  }, "dsh-sidenote: annotate bridge");
1431
3307
  registerSideCommand(ctx);
3308
+ registerSideChatReferenceSource(ctx);
1432
3309
  registerHeaderEntry(ctx);
3310
+ registerToastHost(ctx);
1433
3311
  }
1434
3312
  //#endregion
1435
3313
  //#region src/client/annotate/model.ts
@@ -1882,43 +3760,43 @@ window.__ModuleLoader__.load({
1882
3760
  document.head.appendChild(tag);
1883
3761
  }
1884
3762
  var annotate_module_css_default = {
1885
- "sentChip": "_SVViG_sentChip",
1886
- "chipWrap": "_SVViG_chipWrap",
1887
- "chipNumber": "_SVViG_chipNumber",
1888
- "chipPanel": "_SVViG_chipPanel",
1889
- "chipText": "_SVViG_chipText",
1890
- "editorEdit": "_SVViG_editorEdit",
1891
- "editorSpacer": "_SVViG_editorSpacer",
3763
+ "cancelButton": "_SVViG_cancelButton",
1892
3764
  "editorNew": "_SVViG_editorNew",
1893
- "editorAsk": "_SVViG_editorAsk",
3765
+ "editorFooter": "_SVViG_editorFooter",
3766
+ "confirmButton": "_SVViG_confirmButton",
3767
+ "chipClearAll": "_SVViG_chipClearAll",
1894
3768
  "editorTarget": "_SVViG_editorTarget",
3769
+ "chipNumber": "_SVViG_chipNumber",
3770
+ "chipText": "_SVViG_chipText",
3771
+ "editorAskRow": "_SVViG_editorAskRow",
3772
+ "chipWrap": "_SVViG_chipWrap",
3773
+ "chip": "_SVViG_chip",
3774
+ "highlight": "_SVViG_highlight",
1895
3775
  "toolbarButton": "_SVViG_toolbarButton",
1896
3776
  "chipNote": "_SVViG_chipNote",
1897
- "sentChipRow": "_SVViG_sentChipRow",
3777
+ "chipPanel": "_SVViG_chipPanel",
3778
+ "editorInput": "_SVViG_editorInput",
3779
+ "hint": "_SVViG_hint",
3780
+ "toolbar": "_SVViG_toolbar",
1898
3781
  "saveButton": "_SVViG_saveButton",
1899
- "dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
1900
- "editorFooter": "_SVViG_editorFooter",
1901
- "hintClose": "_SVViG_hintClose",
1902
- "deleteButton": "_SVViG_deleteButton",
1903
- "highlight": "_SVViG_highlight",
1904
- "cancelButton": "_SVViG_cancelButton",
1905
- "chip": "_SVViG_chip",
1906
3782
  "sentCardTitle": "_SVViG_sentCardTitle",
1907
- "confirmButton": "_SVViG_confirmButton",
3783
+ "sentCardNote": "_SVViG_sentCardNote",
3784
+ "deleteButton": "_SVViG_deleteButton",
3785
+ "chipRemove": "_SVViG_chipRemove",
3786
+ "editorAsk": "_SVViG_editorAsk",
3787
+ "hintClose": "_SVViG_hintClose",
1908
3788
  "toolbarBelow": "_SVViG_toolbarBelow",
3789
+ "sentChipRow": "_SVViG_sentChipRow",
3790
+ "sentChip": "_SVViG_sentChip",
1909
3791
  "editorTextarea": "_SVViG_editorTextarea",
1910
- "chipClearAll": "_SVViG_chipClearAll",
1911
- "toolbar": "_SVViG_toolbar",
1912
- "editorError": "_SVViG_editorError",
1913
- "badge": "_SVViG_badge",
1914
- "chipRow": "_SVViG_chipRow",
3792
+ "badgeSent": "_SVViG_badgeSent",
1915
3793
  "sentCardQuote": "_SVViG_sentCardQuote",
1916
- "hint": "_SVViG_hint",
1917
- "chipRemove": "_SVViG_chipRemove",
1918
- "sentCardNote": "_SVViG_sentCardNote",
1919
- "editorInput": "_SVViG_editorInput",
1920
- "editorAskRow": "_SVViG_editorAskRow",
1921
- "badgeSent": "_SVViG_badgeSent"
3794
+ "editorEdit": "_SVViG_editorEdit",
3795
+ "chipRow": "_SVViG_chipRow",
3796
+ "dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
3797
+ "editorSpacer": "_SVViG_editorSpacer",
3798
+ "editorError": "_SVViG_editorError",
3799
+ "badge": "_SVViG_badge"
1922
3800
  };
1923
3801
  //#endregion
1924
3802
  //#region src/client/annotate/hint.tsx
@@ -2817,7 +4695,7 @@ window.__ModuleLoader__.load({
2817
4695
  };
2818
4696
  const onKeyDown = (event) => {
2819
4697
  const target = event.target;
2820
- if (!(target instanceof HTMLTextAreaElement && target.closest("[data-composer-seat]") !== null)) return;
4698
+ if (!(target instanceof HTMLElement && (target.closest("[data-composer-input]") !== null || target instanceof HTMLTextAreaElement) && target.closest("[data-composer-seat]") !== null)) return;
2821
4699
  if (committing) {
2822
4700
  if (event.key === "Enter") {
2823
4701
  event.preventDefault();
@@ -2915,7 +4793,7 @@ window.__ModuleLoader__.load({
2915
4793
  const chip = document.createElement("span");
2916
4794
  chip.className = annotate_module_css_default.sentChip ?? "";
2917
4795
  chip.textContent = t("reflowBubbleLabel");
2918
- chip.title = proto.reflows.map((r) => `${r.source}: ${r.content.slice(0, 200)}`).join("\n");
4796
+ chip.title = proto.reflows.map((r) => `${r.source}: ${flattenReflowContent(r.content).slice(0, 200)}`).join("\n");
2919
4797
  labels.appendChild(chip);
2920
4798
  }
2921
4799
  hidden.insertAdjacentElement("afterend", labels);
@@ -3031,6 +4909,7 @@ window.__ModuleLoader__.load({
3031
4909
  ];
3032
4910
  function apply(ctx) {
3033
4911
  attachLocale(ctx.locale);
4912
+ probeHost(ctx);
3034
4913
  const reflow = createReflowStore();
3035
4914
  registerSideChat(ctx, reflow);
3036
4915
  registerAnnotations(ctx, reflow);