dsh-sidenote 0.1.0 → 0.1.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.
package/lib/client.js CHANGED
@@ -36,6 +36,15 @@ window.__ModuleLoader__.load({
36
36
  stopReply: "Stop",
37
37
  stopReplyTitle: "Stop the current reply",
38
38
  send: "Send",
39
+ imagePlaceholder: "[Image]",
40
+ toolFallback: "Tool",
41
+ unknownError: "Unknown error",
42
+ modelRetryStarted: "Model request failed, retrying…",
43
+ modelRetryWaiting: "Model request failed, waiting to retry…",
44
+ maxTokens: "Output hit the length limit; this turn was truncated.",
45
+ runCommand: "Run command {cmd}",
46
+ commandNameFallback: "command",
47
+ compacted: "Earlier conversation context was compacted.",
39
48
  cmdDesc: "Open a side chat (forked from the current session)",
40
49
  cmdNew: "New side chat",
41
50
  cmdNewDetail: "Forked from the current session; evolves independently",
@@ -51,6 +60,7 @@ window.__ModuleLoader__.load({
51
60
  sideNoteAria: "Side chat note",
52
61
  confirmAskAria: "Confirm and ask",
53
62
  openSideFailed: "Couldn't open the side chat. Try again.",
63
+ sendToTarget: "Send to: {title}",
54
64
  deleteNote: "Delete annotation",
55
65
  cancel: "Cancel",
56
66
  save: "Save",
@@ -88,6 +98,15 @@ window.__ModuleLoader__.load({
88
98
  stopReply: "停止",
89
99
  stopReplyTitle: "停止当前回复",
90
100
  send: "发送",
101
+ imagePlaceholder: "[图片]",
102
+ toolFallback: "工具",
103
+ unknownError: "未知错误",
104
+ modelRetryStarted: "模型请求失败,正在自动重试…",
105
+ modelRetryWaiting: "模型请求失败,等待自动重试…",
106
+ maxTokens: "输出达到长度上限,本轮回复已截断。",
107
+ runCommand: "执行命令 {cmd}",
108
+ commandNameFallback: "命令",
109
+ compacted: "已压缩更早的对话上下文。",
91
110
  cmdDesc: "打开侧边聊天(从当前会话 fork)",
92
111
  cmdNew: "新建侧边聊天",
93
112
  cmdNewDetail: "从当前会话 fork,独立演进",
@@ -103,6 +122,7 @@ window.__ModuleLoader__.load({
103
122
  sideNoteAria: "侧边聊天注解",
104
123
  confirmAskAria: "确认并提问",
105
124
  openSideFailed: "打开侧边聊天失败,请重试",
125
+ sendToTarget: "发送至:{title}",
106
126
  deleteNote: "删除注释",
107
127
  cancel: "取消",
108
128
  save: "保存",
@@ -195,13 +215,21 @@ window.__ModuleLoader__.load({
195
215
  }
196
216
  if (Array.isArray(n.children)) for (const child of n.children) walkTree(child, into);
197
217
  }
198
- /** 枚举侧栏状态(splits + bottomSplits 两棵树)里的全部 Tab。 */
218
+ /**
219
+ * 枚举侧栏状态里的全部 Tab:splits + bottomSplits 两棵树,外加 0.16 引入的
220
+ * 浮动窗 floats(元素形态 { id, tab, … },tab 为窗口持有的 SidebarTab)。
221
+ * floats 缺失(0.16 之前)时数组检查天然跳过,前后版本通吃。
222
+ */
199
223
  function collectTabs(state) {
200
224
  if (typeof state !== "object" || state === null) return [];
201
225
  const s = state;
202
226
  const into = [];
203
227
  walkTree(s.splits, into);
204
228
  walkTree(s.bottomSplits, into);
229
+ if (Array.isArray(s.floats)) for (const float of s.floats) {
230
+ const tab = float?.tab;
231
+ if (isTab(tab)) into.push(tab);
232
+ }
205
233
  return into;
206
234
  }
207
235
  /** 枚举当前并存的侧边聊天 Tab(树顺序 = 打开顺序的近似)。 */
@@ -243,7 +271,7 @@ window.__ModuleLoader__.load({
243
271
  if (typeof block !== "object" || block === null) continue;
244
272
  const b = block;
245
273
  if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
246
- else if (b.type === "image") parts.push("[图片]");
274
+ else if (b.type === "image") parts.push(t("imagePlaceholder"));
247
275
  }
248
276
  return parts.join("\n");
249
277
  }
@@ -298,7 +326,7 @@ window.__ModuleLoader__.load({
298
326
  }
299
327
  case "tool-result": {
300
328
  const call = n.call;
301
- const toolName = typeof call?.name === "string" ? call.name : typeof n.callId === "string" ? n.callId : "工具";
329
+ const toolName = typeof call?.name === "string" ? call.name : typeof n.callId === "string" ? n.callId : t("toolFallback");
302
330
  return {
303
331
  key: seqKey("t", n),
304
332
  role: "tool",
@@ -310,33 +338,33 @@ window.__ModuleLoader__.load({
310
338
  case "turn-error": return {
311
339
  key: seqKey("e", n),
312
340
  role: "error",
313
- text: typeof n.message === "string" ? n.message : "未知错误"
341
+ text: typeof n.message === "string" ? n.message : t("unknownError")
314
342
  };
315
343
  case "model-retry":
316
344
  if (n.retryState === "cancelled") return null;
317
345
  return {
318
346
  key: seqKey("r", n),
319
347
  role: "notice",
320
- text: n.retryState === "started" ? "模型请求失败,正在自动重试…" : "模型请求失败,等待自动重试…"
348
+ text: t(n.retryState === "started" ? "modelRetryStarted" : "modelRetryWaiting")
321
349
  };
322
350
  case "turn-max-tokens": return {
323
351
  key: seqKey("m", n),
324
352
  role: "notice",
325
- text: "输出达到长度上限,本轮回复已截断。"
353
+ text: t("maxTokens")
326
354
  };
327
355
  case "command": {
328
- const name = typeof n.name === "string" && n.name !== "" ? `/${n.name}` : "/命令";
329
- const args = typeof n.args === "string" ? n.args : "";
356
+ const name = typeof n.name === "string" && n.name !== "" ? n.name : t("commandNameFallback");
357
+ const args = typeof n.args === "string" ? n.args.trimStart() : "";
330
358
  return {
331
359
  key: seqKey("c", n),
332
360
  role: "notice",
333
- text: `执行命令 ${name}${args}`
361
+ text: t("runCommand", { cmd: `/${name}${args === "" ? "" : ` ${args}`}` })
334
362
  };
335
363
  }
336
364
  case "compaction": return {
337
365
  key: seqKey("k", n),
338
366
  role: "notice",
339
- text: "已压缩更早的对话上下文。"
367
+ text: t("compacted")
340
368
  };
341
369
  default: return null;
342
370
  }
@@ -358,7 +386,7 @@ window.__ModuleLoader__.load({
358
386
  out.push({
359
387
  key: `rc:${typeof c.callId === "string" ? c.callId : "?"}`,
360
388
  role: "tool",
361
- toolName: typeof c.name === "string" ? c.name : "工具",
389
+ toolName: typeof c.name === "string" ? c.name : t("toolFallback"),
362
390
  text: "",
363
391
  streaming: true
364
392
  });
@@ -507,6 +535,21 @@ window.__ModuleLoader__.load({
507
535
  ctx.betterSidebar.activateTab(target.id, { sessionId });
508
536
  return true;
509
537
  }
538
+ return createSideChat(ctx, sessionId, draftText);
539
+ } catch (error) {
540
+ console.warn("[dsh-sidenote] 打开侧边聊天失败:", error);
541
+ return false;
542
+ }
543
+ }
544
+ /**
545
+ * 无条件新建一个侧边聊天 Tab(/side「新建侧边聊天」选项的语义——既有实例
546
+ * 在弹层里另有聚焦项,「新建」必须真的新建,否则标签与行为不符)。
547
+ * @returns false = 未能创建(调用方保持自己的流程)。
548
+ */
549
+ function createSideChat(ctx, sessionId, draftText) {
550
+ try {
551
+ const snapshot = ctx.betterSidebar.getSnapshot();
552
+ if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
510
553
  const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
511
554
  ctx.betterSidebar.openTab({ type: SIDE_TAB_TYPE }, { sessionId });
512
555
  const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
@@ -514,10 +557,26 @@ window.__ModuleLoader__.load({
514
557
  if (draftText !== void 0 && draftText !== "") ctx.betterSidebar.updateTab(created.id, { meta: { pendingDraft: draftText } });
515
558
  return true;
516
559
  } catch (error) {
517
- console.warn("[dsh-sidenote] 打开侧边聊天失败:", error);
560
+ console.warn("[dsh-sidenote] 新建侧边聊天失败:", error);
518
561
  return false;
519
562
  }
520
563
  }
564
+ /**
565
+ * 桥接/命令的目标预览:askInSideChat 此时若执行会落在哪个 Tab 的标题
566
+ * (既有并存 = 最后一个;无 = 即将新建的首个标题)。用于编辑器上标明
567
+ * 「发送至:侧边 N」。会话不在屏上/状态缺失时返回 undefined。
568
+ */
569
+ function sideChatTargetTitle(ctx, sessionId) {
570
+ try {
571
+ const snapshot = ctx.betterSidebar.getSnapshot();
572
+ if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return void 0;
573
+ const existing = collectSideTabs(snapshot.state);
574
+ if (existing.length > 0) return existing[existing.length - 1].title;
575
+ return sideTabTitle([]);
576
+ } catch {
577
+ return;
578
+ }
579
+ }
521
580
  //#endregion
522
581
  //#region \0dsh-css:/Users/gaoyixuan3/gyx_personal_files/dsh_project/dsh-sidenote/src/client/sidechat/sidechat.module.css.mjs
523
582
  const css$1 = ".F1HU0W_root{height:100%;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:13px;display:flex}.F1HU0W_body{flex-direction:column;flex:1;padding:12px 10px;display:flex;overflow-y:auto}.F1HU0W_transcript{flex-direction:column;gap:12px;display:flex}.F1HU0W_empty,.F1HU0W_stateScreen{text-align:center;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px 16px;display:flex}.F1HU0W_emptyIcon{color:var(--dsw-alias-label-tertiary)}.F1HU0W_emptyTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600}.F1HU0W_emptyText{color:var(--dsw-alias-label-tertiary);max-width:240px;font-size:12px;line-height:1.6}.F1HU0W_stateDetail{color:var(--dsw-alias-state-error-primary);word-break:break-word;max-width:260px;font-size:12px;line-height:1.6}.F1HU0W_userRow{justify-content:flex-end;display:flex}.F1HU0W_userBubble{background:var(--dsw-alias-button-info-fill);color:#fff;white-space:pre-wrap;word-break:break-word;border-radius:12px 12px 2px;max-width:88%;padding:8px 12px}.F1HU0W_assistantRow{justify-content:flex-start;display:flex}.F1HU0W_assistantBody{word-break:break-word;max-width:100%;font-size:13px;line-height:1.6}.F1HU0W_reasoning{border-left:3px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);margin-bottom:6px;padding-left:8px;font-size:12px}.F1HU0W_reasoning>summary{cursor:pointer;user-select:none}.F1HU0W_reasoningBody{white-space:pre-wrap;word-break:break-word;margin-top:4px}.F1HU0W_streamingHint{color:var(--dsw-alias-label-dimmed)}.F1HU0W_toolCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;overflow:hidden}.F1HU0W_toolHead{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;padding:5px 10px;font-size:12px;display:flex}.F1HU0W_toolError{color:var(--dsw-alias-state-error-primary)}.F1HU0W_toolRunning{color:var(--dsw-alias-label-tertiary)}.F1HU0W_toolBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;max-height:160px;padding:8px 10px;font-size:12px;overflow-y:auto}.F1HU0W_errorRow{color:var(--dsw-alias-state-error-primary);white-space:pre-wrap;padding:4px 2px;font-size:12px}.F1HU0W_noticeRow{color:var(--dsw-alias-label-dimmed);text-align:center;padding:2px 0;font-size:12px}.F1HU0W_composer{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:6px;padding:8px 10px;display:flex}.F1HU0W_input{resize:vertical;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);width:100%;min-height:40px;max-height:160px;color:var(--dsw-alias-label-primary);font:inherit;box-sizing:border-box;border-radius:8px;padding:8px 10px}.F1HU0W_input:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}.F1HU0W_composerFoot{justify-content:space-between;align-items:center;gap:8px;display:flex}.F1HU0W_modelLabel{color:var(--dsw-alias-label-dimmed);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.F1HU0W_sendButton,.F1HU0W_stopButton{appearance:none;border:1px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;border-radius:8px;flex:none;align-items:center;gap:4px;padding:6px 12px;font-size:13px;display:inline-flex}.F1HU0W_sendButton:disabled{opacity:.55;cursor:default}";
@@ -530,35 +589,35 @@ window.__ModuleLoader__.load({
530
589
  document.head.appendChild(tag);
531
590
  }
532
591
  var sidechat_module_css_default = {
533
- "assistantRow": "F1HU0W_assistantRow",
592
+ "emptyIcon": "F1HU0W_emptyIcon",
593
+ "input": "F1HU0W_input",
594
+ "toolRunning": "F1HU0W_toolRunning",
595
+ "transcript": "F1HU0W_transcript",
596
+ "reasoningBody": "F1HU0W_reasoningBody",
534
597
  "composer": "F1HU0W_composer",
535
- "stateScreen": "F1HU0W_stateScreen",
598
+ "root": "F1HU0W_root",
599
+ "emptyText": "F1HU0W_emptyText",
600
+ "userRow": "F1HU0W_userRow",
536
601
  "empty": "F1HU0W_empty",
602
+ "streamingHint": "F1HU0W_streamingHint",
537
603
  "toolError": "F1HU0W_toolError",
538
604
  "errorRow": "F1HU0W_errorRow",
539
- "modelLabel": "F1HU0W_modelLabel",
540
605
  "toolBody": "F1HU0W_toolBody",
541
- "noticeRow": "F1HU0W_noticeRow",
542
- "stateDetail": "F1HU0W_stateDetail",
543
- "transcript": "F1HU0W_transcript",
544
- "assistantBody": "F1HU0W_assistantBody",
545
- "stopButton": "F1HU0W_stopButton",
606
+ "body": "F1HU0W_body",
607
+ "stateScreen": "F1HU0W_stateScreen",
546
608
  "sendButton": "F1HU0W_sendButton",
547
- "emptyText": "F1HU0W_emptyText",
548
609
  "emptyTitle": "F1HU0W_emptyTitle",
549
- "toolHead": "F1HU0W_toolHead",
610
+ "userBubble": "F1HU0W_userBubble",
611
+ "stateDetail": "F1HU0W_stateDetail",
612
+ "assistantBody": "F1HU0W_assistantBody",
613
+ "assistantRow": "F1HU0W_assistantRow",
550
614
  "composerFoot": "F1HU0W_composerFoot",
615
+ "toolHead": "F1HU0W_toolHead",
616
+ "stopButton": "F1HU0W_stopButton",
551
617
  "reasoning": "F1HU0W_reasoning",
552
- "root": "F1HU0W_root",
553
- "body": "F1HU0W_body",
554
- "toolRunning": "F1HU0W_toolRunning",
555
- "userBubble": "F1HU0W_userBubble",
556
- "reasoningBody": "F1HU0W_reasoningBody",
557
- "emptyIcon": "F1HU0W_emptyIcon",
558
- "userRow": "F1HU0W_userRow",
559
618
  "toolCard": "F1HU0W_toolCard",
560
- "input": "F1HU0W_input",
561
- "streamingHint": "F1HU0W_streamingHint"
619
+ "noticeRow": "F1HU0W_noticeRow",
620
+ "modelLabel": "F1HU0W_modelLabel"
562
621
  };
563
622
  //#endregion
564
623
  //#region src/client/sidechat/SideChatPanel.tsx
@@ -581,7 +640,6 @@ window.__ModuleLoader__.load({
581
640
  * 事件 —— 因此绑定后对该会话调一次 concrete Session 的 open()(off-face、
582
641
  * feature-check、幂等),否则消息流永远为空。
583
642
  */
584
- /** MarkdownText 代码块复制按钮文案(契约要求中文;引用稳定,变了会清流式缓存)。 */
585
643
  /** MarkdownText 代码块复制按钮文案(跟随 DSH 语言;函数内读取保持引用稳定)。 */
586
644
  function codeLabels() {
587
645
  return {
@@ -695,11 +753,15 @@ window.__ModuleLoader__.load({
695
753
  session
696
754
  ]);
697
755
  const pendingDraft = meta.pendingDraft;
756
+ const rootRef = (0, react.useRef)(null);
698
757
  (0, react.useEffect)(() => {
699
758
  if (pendingDraft === void 0 || pendingDraft === "" || phase !== "chat") return;
700
759
  composer.appendDraft(pendingDraft);
701
760
  const current = parseSideChatMeta(readTab(ctx, tab.id)?.meta);
702
761
  ctx.betterSidebar.updateTab(tab.id, { meta: clearPendingDraft(current) });
762
+ requestAnimationFrame(() => {
763
+ rootRef.current?.querySelector("textarea")?.focus();
764
+ });
703
765
  }, [
704
766
  pendingDraft,
705
767
  phase,
@@ -726,6 +788,7 @@ window.__ModuleLoader__.load({
726
788
  const running = snapshot?.running === true;
727
789
  const openFailed = snapshot?.openState === "error";
728
790
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
791
+ ref: rootRef,
729
792
  className: sidechat_module_css_default.root,
730
793
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
731
794
  ref: bodyRef,
@@ -963,7 +1026,7 @@ window.__ModuleLoader__.load({
963
1026
  },
964
1027
  onSelect: (option, session) => {
965
1028
  if (option.id === "new") {
966
- openOrFocusSideChat(ctx, session.sessionId);
1029
+ createSideChat(ctx, session.sessionId);
967
1030
  return;
968
1031
  }
969
1032
  if (option.id.startsWith("focus:")) ctx.betterSidebar.activateTab(option.id.slice(6), { sessionId: session.sessionId });
@@ -1008,7 +1071,10 @@ window.__ModuleLoader__.load({
1008
1071
  component: (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SideChatPanel, { ...props })
1009
1072
  }), "dsh-sidenote: side chat tab");
1010
1073
  ctx.effect(() => {
1011
- const impl = { askInSideChat: (sessionId, draftText) => openOrFocusSideChat(ctx, sessionId, draftText) };
1074
+ const impl = {
1075
+ askInSideChat: (sessionId, draftText) => openOrFocusSideChat(ctx, sessionId, draftText),
1076
+ peekTargetTitle: (sessionId) => sideChatTargetTitle(ctx, sessionId)
1077
+ };
1012
1078
  sideChatBridge.current = impl;
1013
1079
  return () => {
1014
1080
  if (sideChatBridge.current === impl) sideChatBridge.current = null;
@@ -1428,7 +1494,7 @@ window.__ModuleLoader__.load({
1428
1494
  }
1429
1495
  //#endregion
1430
1496
  //#region \0dsh-css:/Users/gaoyixuan3/gyx_personal_files/dsh_project/dsh-sidenote/src/client/annotate/annotate.module.css.mjs
1431
- const css = "._6Uc2-G_toolbar{z-index:50;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#00000014);user-select:none;border-radius:10px;align-items:stretch;display:flex;position:fixed;overflow:hidden;transform:translate(-50%,-100%);box-shadow:0 4px 16px #0000001f}._6Uc2-G_toolbarButton{appearance:none;color:var(--dsw-alias-label-primary,#1f2329);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:7px 12px;font-size:13px;line-height:20px}._6Uc2-G_toolbarButton+._6Uc2-G_toolbarButton{border-left:1px solid var(--dsw-alias-border-l1,#00000014)}._6Uc2-G_toolbarButton:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d)}._6Uc2-G_badge{z-index:50;color:#fff;text-align:center;cursor:pointer;user-select:none;background:#2f88ff;border:none;border-radius:999px;width:18px;height:18px;padding:0;font-size:11px;font-weight:600;line-height:18px;position:fixed;transform:translateY(-50%);box-shadow:0 1px 4px #2f88ff73}._6Uc2-G_badge:hover{background:#1f78f0}._6Uc2-G_highlight{z-index:49;pointer-events:none;background:#2f88ff38;border-radius:2px;position:fixed}._6Uc2-G_editorNew{z-index:50;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#00000014);border-radius:999px;align-items:center;gap:8px;padding:6px 8px 6px 12px;display:flex;position:fixed;box-shadow:0 6px 24px #00000024}._6Uc2-G_editorEdit{z-index:50;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#00000014);border-radius:14px;flex-direction:column;display:flex;position:fixed;overflow:hidden;box-shadow:0 8px 28px #00000029}._6Uc2-G_editorInput{min-width:0;color:var(--dsw-alias-label-primary,#1f2329);background:0 0;border:none;outline:none;flex:auto;font-size:13px;line-height:22px}._6Uc2-G_editorTextarea{resize:none;color:var(--dsw-alias-label-primary,#1f2329);background:0 0;border:none;outline:none;padding:10px 12px 4px;font-family:inherit;font-size:13px;line-height:20px}._6Uc2-G_editorInput::placeholder,._6Uc2-G_editorTextarea::placeholder{color:var(--dsw-alias-label-caption,#8f959e)}._6Uc2-G_confirmButton{color:#fff;cursor:pointer;background:#1f2329;border:none;border-radius:999px;flex:none;place-items:center;width:26px;height:26px;padding:0;display:grid}._6Uc2-G_confirmButton:hover{background:#000}._6Uc2-G_editorFooter{align-items:center;gap:8px;padding:6px 10px 8px;display:flex}._6Uc2-G_editorSpacer{flex:auto}._6Uc2-G_deleteButton{width:26px;height:26px;color:var(--dsw-alias-label-tertiary,#8f959e);cursor:pointer;background:0 0;border:none;border-radius:999px;place-items:center;padding:0;display:grid}._6Uc2-G_deleteButton:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-state-error-primary,#d53f3f)}._6Uc2-G_cancelButton,._6Uc2-G_saveButton{cursor:pointer;border:none;border-radius:999px;padding:4px 14px;font-size:13px;line-height:20px}._6Uc2-G_cancelButton{background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-primary,#1f2329)}._6Uc2-G_saveButton{color:#fff;background:#1f2329}._6Uc2-G_saveButton:hover{background:#000}._6Uc2-G_chipWrap{flex-direction:column;align-items:flex-start;gap:4px;display:flex}._6Uc2-G_chip{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2329);cursor:pointer;user-select:none;border-radius:999px;align-items:center;gap:5px;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}._6Uc2-G_chip:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d)}._6Uc2-G_chipPanel{border:1px solid var(--dsw-alias-border-l1,#00000014);background:var(--dsw-alias-bg-layer-1,#fff);border-radius:12px;min-width:220px;max-width:420px;margin:0;padding:4px;list-style:none;box-shadow:0 6px 20px #0000001a}._6Uc2-G_chipRow{border-radius:8px;align-items:center;gap:8px;padding:4px 6px;display:flex}._6Uc2-G_chipRow:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000a)}._6Uc2-G_chipNumber{color:#fff;text-align:center;background:#2f88ff;border-radius:999px;flex:none;width:16px;height:16px;font-size:10px;font-weight:600;line-height:16px}._6Uc2-G_chipText{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary,#1f2329);flex:auto;font-size:12px;overflow:hidden}._6Uc2-G_chipNote{color:var(--dsw-alias-label-tertiary,#8f959e)}._6Uc2-G_chipRemove{width:20px;height:20px;color:var(--dsw-alias-label-tertiary,#8f959e);cursor:pointer;background:0 0;border:none;border-radius:999px;flex:none;place-items:center;padding:0;display:grid}._6Uc2-G_chipRemove:hover{background:var(--dsw-alias-interactive-bg-hover,#00000014);color:var(--dsw-alias-state-error-primary,#d53f3f)}._6Uc2-G_editorError{color:#e5484d;white-space:nowrap;font-size:12px;position:absolute;top:calc(100% + 4px);left:12px}";
1497
+ const css = "._6Uc2-G_toolbar{z-index:50;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#00000014);user-select:none;border-radius:10px;align-items:stretch;display:flex;position:fixed;overflow:hidden;transform:translate(-50%,-100%);box-shadow:0 4px 16px #0000001f}._6Uc2-G_toolbarBelow{transform:translate(-50%)}._6Uc2-G_toolbarButton{appearance:none;color:var(--dsw-alias-label-primary,#1f2329);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:7px 12px;font-size:13px;line-height:20px}._6Uc2-G_toolbarButton+._6Uc2-G_toolbarButton{border-left:1px solid var(--dsw-alias-border-l1,#00000014)}._6Uc2-G_toolbarButton:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d)}._6Uc2-G_badge{z-index:50;color:#fff;text-align:center;cursor:pointer;user-select:none;background:#2f88ff;border:none;border-radius:999px;width:18px;height:18px;padding:0;font-size:11px;font-weight:600;line-height:18px;position:fixed;transform:translateY(-50%);box-shadow:0 1px 4px #2f88ff73}._6Uc2-G_badge:hover{background:#1f78f0}._6Uc2-G_highlight{z-index:49;pointer-events:none;background:#2f88ff38;border-radius:2px;position:fixed}._6Uc2-G_editorNew{z-index:50;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#00000014);border-radius:999px;align-items:center;gap:8px;padding:6px 8px 6px 12px;display:flex;position:fixed;box-shadow:0 6px 24px #00000024}._6Uc2-G_editorEdit{z-index:50;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#00000014);border-radius:14px;flex-direction:column;display:flex;position:fixed;overflow:hidden;box-shadow:0 8px 28px #00000029}._6Uc2-G_editorInput{min-width:0;color:var(--dsw-alias-label-primary,#1f2329);background:0 0;border:none;outline:none;flex:auto;font-size:13px;line-height:22px}._6Uc2-G_editorTextarea{resize:none;color:var(--dsw-alias-label-primary,#1f2329);background:0 0;border:none;outline:none;padding:10px 12px 4px;font-family:inherit;font-size:13px;line-height:20px}._6Uc2-G_editorInput::placeholder,._6Uc2-G_editorTextarea::placeholder{color:var(--dsw-alias-label-caption,#8f959e)}._6Uc2-G_editorAsk{z-index:50;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#00000014);border-radius:14px;flex-direction:column;gap:2px;padding:6px 8px 6px 12px;display:flex;position:fixed;box-shadow:0 6px 24px #00000024}._6Uc2-G_editorAskRow{align-items:center;gap:8px;display:flex}._6Uc2-G_editorTarget{color:var(--dsw-alias-label-caption,#8f959e);user-select:none;padding:2px 2px 0;font-size:11px;line-height:16px}._6Uc2-G_confirmButton{color:#fff;cursor:pointer;background:#1f2329;border:none;border-radius:999px;flex:none;place-items:center;width:26px;height:26px;padding:0;display:grid}._6Uc2-G_confirmButton:hover{background:#000}._6Uc2-G_editorFooter{align-items:center;gap:8px;padding:6px 10px 8px;display:flex}._6Uc2-G_editorSpacer{flex:auto}._6Uc2-G_deleteButton{width:26px;height:26px;color:var(--dsw-alias-label-tertiary,#8f959e);cursor:pointer;background:0 0;border:none;border-radius:999px;place-items:center;padding:0;display:grid}._6Uc2-G_deleteButton:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-state-error-primary,#d53f3f)}._6Uc2-G_cancelButton,._6Uc2-G_saveButton{cursor:pointer;border:none;border-radius:999px;padding:4px 14px;font-size:13px;line-height:20px}._6Uc2-G_cancelButton{background:var(--dsw-alias-interactive-bg-hover,#0000000d);color:var(--dsw-alias-label-primary,#1f2329)}._6Uc2-G_saveButton{color:#fff;background:#1f2329}._6Uc2-G_saveButton:hover{background:#000}._6Uc2-G_chipWrap{box-sizing:border-box;width:calc(100% - var(--dsh-composer-side-clearance,16px) - var(--dsh-composer-side-clearance,16px));max-width:var(--dsh-composer-card-max-width,780px);flex-direction:column;align-items:flex-start;gap:4px;margin:0 auto;display:flex;position:relative}._6Uc2-G_chip{border:1px solid var(--dsw-alias-border-l1,#0000001a);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2329);cursor:pointer;user-select:none;border-radius:999px;align-items:center;gap:5px;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}._6Uc2-G_chip:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000d)}._6Uc2-G_chipPanel{z-index:50;border:1px solid var(--dsw-alias-border-l1,#00000014);background:var(--dsw-alias-bg-layer-1,#fff);border-radius:12px;min-width:220px;max-width:420px;margin:0;padding:4px;list-style:none;position:absolute;bottom:calc(100% + 4px);left:0;box-shadow:0 6px 20px #0000001a}._6Uc2-G_chipRow{border-radius:8px;align-items:center;gap:8px;padding:4px 6px;display:flex}._6Uc2-G_chipRow:hover{background:var(--dsw-alias-interactive-bg-hover,#0000000a)}._6Uc2-G_chipNumber{color:#fff;text-align:center;background:#2f88ff;border-radius:999px;flex:none;width:16px;height:16px;font-size:10px;font-weight:600;line-height:16px}._6Uc2-G_chipText{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary,#1f2329);flex:auto;font-size:12px;overflow:hidden}._6Uc2-G_chipNote{color:var(--dsw-alias-label-tertiary,#8f959e)}._6Uc2-G_chipRemove{width:20px;height:20px;color:var(--dsw-alias-label-tertiary,#8f959e);cursor:pointer;background:0 0;border:none;border-radius:999px;flex:none;place-items:center;padding:0;display:grid}._6Uc2-G_chipRemove:hover{background:var(--dsw-alias-interactive-bg-hover,#00000014);color:var(--dsw-alias-state-error-primary,#d53f3f)}._6Uc2-G_editorError{color:#e5484d;white-space:nowrap;font-size:12px;position:absolute;top:calc(100% + 4px);left:12px}";
1432
1498
  const tagId = "dsh-sidenote/annotate.module.css";
1433
1499
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1434
1500
  const tag = document.createElement("style");
@@ -1438,29 +1504,33 @@ window.__ModuleLoader__.load({
1438
1504
  document.head.appendChild(tag);
1439
1505
  }
1440
1506
  var annotate_module_css_default = {
1441
- "chipNote": "_6Uc2-G_chipNote",
1507
+ "chipNumber": "_6Uc2-G_chipNumber",
1508
+ "deleteButton": "_6Uc2-G_deleteButton",
1509
+ "editorError": "_6Uc2-G_editorError",
1510
+ "editorEdit": "_6Uc2-G_editorEdit",
1511
+ "toolbar": "_6Uc2-G_toolbar",
1512
+ "editorTarget": "_6Uc2-G_editorTarget",
1513
+ "chipWrap": "_6Uc2-G_chipWrap",
1514
+ "editorAsk": "_6Uc2-G_editorAsk",
1515
+ "chip": "_6Uc2-G_chip",
1442
1516
  "chipPanel": "_6Uc2-G_chipPanel",
1443
- "chipRemove": "_6Uc2-G_chipRemove",
1444
1517
  "highlight": "_6Uc2-G_highlight",
1445
- "cancelButton": "_6Uc2-G_cancelButton",
1446
- "editorInput": "_6Uc2-G_editorInput",
1447
- "toolbar": "_6Uc2-G_toolbar",
1448
1518
  "toolbarButton": "_6Uc2-G_toolbarButton",
1449
- "saveButton": "_6Uc2-G_saveButton",
1450
1519
  "chipRow": "_6Uc2-G_chipRow",
1451
1520
  "confirmButton": "_6Uc2-G_confirmButton",
1452
1521
  "editorFooter": "_6Uc2-G_editorFooter",
1453
- "chipText": "_6Uc2-G_chipText",
1454
- "editorNew": "_6Uc2-G_editorNew",
1455
- "editorSpacer": "_6Uc2-G_editorSpacer",
1456
- "editorEdit": "_6Uc2-G_editorEdit",
1457
- "chipWrap": "_6Uc2-G_chipWrap",
1458
1522
  "badge": "_6Uc2-G_badge",
1523
+ "editorInput": "_6Uc2-G_editorInput",
1459
1524
  "editorTextarea": "_6Uc2-G_editorTextarea",
1460
- "chip": "_6Uc2-G_chip",
1461
- "editorError": "_6Uc2-G_editorError",
1462
- "deleteButton": "_6Uc2-G_deleteButton",
1463
- "chipNumber": "_6Uc2-G_chipNumber"
1525
+ "editorAskRow": "_6Uc2-G_editorAskRow",
1526
+ "chipNote": "_6Uc2-G_chipNote",
1527
+ "chipRemove": "_6Uc2-G_chipRemove",
1528
+ "chipText": "_6Uc2-G_chipText",
1529
+ "editorSpacer": "_6Uc2-G_editorSpacer",
1530
+ "editorNew": "_6Uc2-G_editorNew",
1531
+ "saveButton": "_6Uc2-G_saveButton",
1532
+ "toolbarBelow": "_6Uc2-G_toolbarBelow",
1533
+ "cancelButton": "_6Uc2-G_cancelButton"
1464
1534
  };
1465
1535
  //#endregion
1466
1536
  //#region src/client/annotate/overlay.tsx
@@ -1577,10 +1647,12 @@ window.__ModuleLoader__.load({
1577
1647
  };
1578
1648
  const askInSideChat = (snapshot) => {
1579
1649
  const anchor = selectionBadgePoint(snapshot);
1650
+ const targetTitle = sideChatBridge.current?.peekTargetTitle(snapshot.sessionId);
1580
1651
  setSideDraft({
1581
1652
  snapshot,
1582
1653
  x: anchor.x,
1583
- y: anchor.y
1654
+ y: anchor.y,
1655
+ ...targetTitle !== void 0 ? { targetTitle } : {}
1584
1656
  });
1585
1657
  controller.clear();
1586
1658
  window.getSelection()?.removeAllRanges();
@@ -1644,6 +1716,7 @@ window.__ModuleLoader__.load({
1644
1716
  sideDraft !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SideChatNoteEditor, {
1645
1717
  x: sideDraft.x,
1646
1718
  y: sideDraft.y,
1719
+ targetTitle: sideDraft.targetTitle,
1647
1720
  onSave: commitSideDraft,
1648
1721
  onCancel: () => {
1649
1722
  setSideDraft(null);
@@ -1687,37 +1760,43 @@ window.__ModuleLoader__.load({
1687
1760
  };
1688
1761
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1689
1762
  ref: rootRef,
1690
- className: annotate_module_css_default.editorNew,
1763
+ className: annotate_module_css_default.editorAsk,
1691
1764
  style: {
1692
1765
  left,
1693
1766
  top,
1694
1767
  width
1695
1768
  },
1696
1769
  children: [
1697
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1698
- className: annotate_module_css_default.editorInput,
1699
- value: note,
1700
- placeholder: t("sideNotePlaceholder"),
1701
- "aria-label": t("sideNoteAria"),
1702
- autoFocus: true,
1703
- onChange: (event) => {
1704
- setNote(event.target.value);
1705
- setFailed(false);
1706
- },
1707
- onKeyDown: (event) => {
1708
- if (event.key === "Enter") {
1770
+ props.targetTitle !== void 0 && props.targetTitle !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1771
+ className: annotate_module_css_default.editorTarget,
1772
+ children: t("sendToTarget", { title: props.targetTitle })
1773
+ }),
1774
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1775
+ className: annotate_module_css_default.editorAskRow,
1776
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1777
+ className: annotate_module_css_default.editorInput,
1778
+ value: note,
1779
+ placeholder: t("sideNotePlaceholder"),
1780
+ "aria-label": t("sideNoteAria"),
1781
+ autoFocus: true,
1782
+ onChange: (event) => {
1783
+ setNote(event.target.value);
1784
+ setFailed(false);
1785
+ },
1786
+ onKeyDown: (event) => {
1787
+ if (event.key !== "Enter") return;
1788
+ if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
1709
1789
  event.preventDefault();
1710
1790
  save();
1711
1791
  }
1712
- }
1713
- }),
1714
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1715
- type: "button",
1716
- className: annotate_module_css_default.confirmButton,
1717
- title: t("confirmTitle"),
1718
- "aria-label": t("confirmAskAria"),
1719
- onClick: save,
1720
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 })
1792
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1793
+ type: "button",
1794
+ className: annotate_module_css_default.confirmButton,
1795
+ title: t("confirmTitle"),
1796
+ "aria-label": t("confirmAskAria"),
1797
+ onClick: save,
1798
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 14 })
1799
+ })]
1721
1800
  }),
1722
1801
  failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1723
1802
  className: annotate_module_css_default.editorError,
@@ -1731,9 +1810,11 @@ window.__ModuleLoader__.load({
1731
1810
  useLocaleTick();
1732
1811
  const { rect } = props.snapshot;
1733
1812
  const left = Math.max(8, Math.min(window.innerWidth - 8, rect.left + rect.width / 2));
1734
- const top = Math.max(4, rect.top - 10);
1813
+ const flipBelow = rect.top < 64;
1814
+ const selectionBottom = props.snapshot.range.getBoundingClientRect().bottom;
1815
+ const top = flipBelow ? Math.max(4, Math.min(window.innerHeight - 48, selectionBottom + 10)) : Math.max(4, rect.top - 10);
1735
1816
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1736
- className: annotate_module_css_default.toolbar,
1817
+ className: flipBelow ? `${annotate_module_css_default.toolbar} ${annotate_module_css_default.toolbarBelow}` : annotate_module_css_default.toolbar,
1737
1818
  style: {
1738
1819
  left,
1739
1820
  top
@@ -1773,6 +1854,7 @@ window.__ModuleLoader__.load({
1773
1854
  const badges = [];
1774
1855
  const placed = [];
1775
1856
  let highlight = null;
1857
+ const composerRect = typeof document === "undefined" ? null : document.querySelector("[data-composer-seat]")?.getBoundingClientRect() ?? null;
1776
1858
  for (const annotation of annotations) {
1777
1859
  const range = resolveRange(annotation, props.cache);
1778
1860
  if (range === null) continue;
@@ -1788,6 +1870,7 @@ window.__ModuleLoader__.load({
1788
1870
  const anchor = badgeAnchorOf(range);
1789
1871
  if (anchor === null) continue;
1790
1872
  if (anchor.centerY < 0 || anchor.centerY > window.innerHeight || anchor.right < 0 || anchor.right > window.innerWidth) continue;
1873
+ if (composerRect !== null && anchor.centerY >= composerRect.top && anchor.centerY <= composerRect.bottom && anchor.right >= composerRect.left && anchor.right <= composerRect.right) continue;
1791
1874
  const point = spreadBadgePoint({
1792
1875
  x: anchor.right,
1793
1876
  y: anchor.centerY
@@ -1863,10 +1946,10 @@ window.__ModuleLoader__.load({
1863
1946
  setNote(event.target.value);
1864
1947
  },
1865
1948
  onKeyDown: (event) => {
1866
- if (event.key === "Enter") {
1867
- event.preventDefault();
1868
- save();
1869
- }
1949
+ if (event.key !== "Enter") return;
1950
+ if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
1951
+ event.preventDefault();
1952
+ save();
1870
1953
  }
1871
1954
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1872
1955
  type: "button",
@@ -1895,10 +1978,10 @@ window.__ModuleLoader__.load({
1895
1978
  setNote(event.target.value);
1896
1979
  },
1897
1980
  onKeyDown: (event) => {
1898
- if (event.key === "Enter" && !event.shiftKey) {
1899
- event.preventDefault();
1900
- save();
1901
- }
1981
+ if (event.key !== "Enter" || event.shiftKey) return;
1982
+ if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
1983
+ event.preventDefault();
1984
+ save();
1902
1985
  }
1903
1986
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1904
1987
  className: annotate_module_css_default.editorFooter,
@@ -1946,6 +2029,27 @@ window.__ModuleLoader__.load({
1946
2029
  const sessionId = props.session.sessionId;
1947
2030
  (0, react.useSyncExternalStore)((0, react.useCallback)((cb) => store.subscribe(cb), [store]), () => store.getSnapshot());
1948
2031
  const [expanded, setExpanded] = (0, react.useState)(false);
2032
+ const rootRef = (0, react.useRef)(null);
2033
+ (0, react.useEffect)(() => {
2034
+ if (!expanded) return;
2035
+ const onKeyDown = (event) => {
2036
+ if (event.key === "Escape") {
2037
+ event.stopPropagation();
2038
+ setExpanded(false);
2039
+ }
2040
+ };
2041
+ const onMouseDown = (event) => {
2042
+ const root = rootRef.current;
2043
+ if (root === null || !(event.target instanceof Node)) return;
2044
+ if (!root.contains(event.target)) setExpanded(false);
2045
+ };
2046
+ document.addEventListener("keydown", onKeyDown, true);
2047
+ document.addEventListener("mousedown", onMouseDown, true);
2048
+ return () => {
2049
+ document.removeEventListener("keydown", onKeyDown, true);
2050
+ document.removeEventListener("mousedown", onMouseDown, true);
2051
+ };
2052
+ }, [expanded]);
1949
2053
  const previous = (0, react.useRef)({
1950
2054
  sessionId,
1951
2055
  draft: props.input.draft,
@@ -1971,6 +2075,7 @@ window.__ModuleLoader__.load({
1971
2075
  const active = store.listActive(sessionId);
1972
2076
  if (active.length === 0) return null;
1973
2077
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2078
+ ref: rootRef,
1974
2079
  className: annotate_module_css_default.chipWrap,
1975
2080
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1976
2081
  type: "button",
@@ -13,6 +13,11 @@ export interface SideChatBridge {
13
13
  * when no side chat could be opened (caller keeps its own flow).
14
14
  */
15
15
  askInSideChat(sessionId: SessionId, draftText: string): boolean;
16
+ /**
17
+ * 预览 askInSideChat 此刻会落到哪个侧边聊天 Tab 的标题(编辑器上标明
18
+ * 「发送至:侧边 N」);目标不可预知时返回 undefined。
19
+ */
20
+ peekTargetTitle(sessionId: SessionId): string | undefined;
16
21
  }
17
22
  export declare const sideChatBridge: {
18
23
  current: SideChatBridge | null;
@@ -32,6 +32,15 @@ export declare const en: {
32
32
  stopReply: string;
33
33
  stopReplyTitle: string;
34
34
  send: string;
35
+ imagePlaceholder: string;
36
+ toolFallback: string;
37
+ unknownError: string;
38
+ modelRetryStarted: string;
39
+ modelRetryWaiting: string;
40
+ maxTokens: string;
41
+ runCommand: string;
42
+ commandNameFallback: string;
43
+ compacted: string;
35
44
  cmdDesc: string;
36
45
  cmdNew: string;
37
46
  cmdNewDetail: string;
@@ -47,6 +56,7 @@ export declare const en: {
47
56
  sideNoteAria: string;
48
57
  confirmAskAria: string;
49
58
  openSideFailed: string;
59
+ sendToTarget: string;
50
60
  deleteNote: string;
51
61
  cancel: string;
52
62
  save: string;
@@ -36,12 +36,14 @@ export interface SideChatMeta {
36
36
  export declare function parseSideChatMeta(meta: unknown): SideChatMeta;
37
37
  /** 清除 meta.pendingDraft(其余字段原样保留)。 */
38
38
  export declare function clearPendingDraft(meta: SideChatMeta): SideChatMeta;
39
- /** 枚举侧栏状态(splits + bottomSplits 两棵树)里的全部 Tab。 */
39
+ /**
40
+ * 枚举侧栏状态里的全部 Tab:splits + bottomSplits 两棵树,外加 0.16 引入的
41
+ * 浮动窗 floats(元素形态 { id, tab, … },tab 为窗口持有的 SidebarTab)。
42
+ * floats 缺失(0.16 之前)时数组检查天然跳过,前后版本通吃。
43
+ */
40
44
  export declare function collectTabs(state: unknown): SidebarTab[];
41
45
  /** 枚举当前并存的侧边聊天 Tab(树顺序 = 打开顺序的近似)。 */
42
46
  export declare function collectSideTabs(state: unknown): SidebarTab[];
43
- /** 并存数(createTab 编号标题的输入)。 */
44
- export declare function countSideTabs(state: unknown): number;
45
47
  /**
46
48
  * blank 着陆页会话没有已完成 turn,fork 必败(host 返回 fork-unavailable)
47
49
  * ——+ 菜单与 /side 命令的 available 用它禁用入口;面板内错误态兜底。
@@ -14,3 +14,15 @@ export declare function readTab(ctx: Context, tabId: string): SidebarTab | undef
14
14
  * @returns false = 未能打开(调用方保持自己的流程)。
15
15
  */
16
16
  export declare function openOrFocusSideChat(ctx: Context, sessionId: string, draftText?: string): boolean;
17
+ /**
18
+ * 无条件新建一个侧边聊天 Tab(/side「新建侧边聊天」选项的语义——既有实例
19
+ * 在弹层里另有聚焦项,「新建」必须真的新建,否则标签与行为不符)。
20
+ * @returns false = 未能创建(调用方保持自己的流程)。
21
+ */
22
+ export declare function createSideChat(ctx: Context, sessionId: string, draftText?: string): boolean;
23
+ /**
24
+ * 桥接/命令的目标预览:askInSideChat 此时若执行会落在哪个 Tab 的标题
25
+ * (既有并存 = 最后一个;无 = 即将新建的首个标题)。用于编辑器上标明
26
+ * 「发送至:侧边 N」。会话不在屏上/状态缺失时返回 undefined。
27
+ */
28
+ export declare function sideChatTargetTitle(ctx: Context, sessionId: string): string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidenote",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "DSH web plugin: Codex-style side chat (fork the current session into a persistent side panel) + selection annotations with numbered badges and a composer chip. A thin consumer of dsh-better-sidebar. / Codex 风格侧边聊天 + 划选注释的 DSH 插件。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "peerDependencies": {
45
45
  "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
46
46
  "cordis": "^4.0.0-rc.7",
47
- "dsh-better-sidebar": "^0.12.0",
47
+ "dsh-better-sidebar": ">=0.12.0 <1.0.0",
48
48
  "react": "^18.2.0",
49
49
  "react-dom": "^18.2.0"
50
50
  },
@@ -16,6 +16,11 @@
16
16
  user-select: none;
17
17
  }
18
18
 
19
+ /* 选区贴视口顶部时的翻转修饰类(与 .toolbar 叠加使用):渲染在选区下方。 */
20
+ .toolbarBelow {
21
+ transform: translate(-50%, 0);
22
+ }
23
+
19
24
  .toolbarButton {
20
25
  appearance: none;
21
26
  border: none;
@@ -121,6 +126,35 @@
121
126
  color: var(--dsw-alias-label-caption, #8f959e);
122
127
  }
123
128
 
129
+ /* 划选提问编辑器(带目标行,纵排卡片形态,与单行 pill 的 editorNew 区分)。 */
130
+ .editorAsk {
131
+ position: fixed;
132
+ z-index: 50;
133
+ display: flex;
134
+ flex-direction: column;
135
+ gap: 2px;
136
+ padding: 6px 8px 6px 12px;
137
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
138
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.08));
139
+ border-radius: 14px;
140
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.14);
141
+ }
142
+
143
+ .editorAskRow {
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 8px;
147
+ }
148
+
149
+ /* 注解编辑器里的投递目标提示行(「发送至:侧边 2」)。 */
150
+ .editorTarget {
151
+ padding: 2px 2px 0;
152
+ font-size: 11px;
153
+ line-height: 16px;
154
+ color: var(--dsw-alias-label-caption, #8f959e);
155
+ user-select: none;
156
+ }
157
+
124
158
  .confirmButton {
125
159
  flex: none;
126
160
  width: 26px;
@@ -194,11 +228,19 @@
194
228
 
195
229
  /* ── composer chip (conversation.input.dock) ─────────────────────────────── */
196
230
 
231
+ /* dock 槽容器是通栏的(composerStack 全宽),宿主内建 dock(todo/queue)靠
232
+ 同一组令牌公式自我对齐 composer 卡片——照抄该公式,让 chip 贴在输入框
233
+ 卡片正上方(左缘对齐),而不是漂在消息列左边缘。 */
197
234
  .chipWrap {
235
+ box-sizing: border-box;
236
+ width: calc(100% - var(--dsh-composer-side-clearance, 16px) - var(--dsh-composer-side-clearance, 16px));
237
+ max-width: var(--dsh-composer-card-max-width, 780px);
238
+ margin: 0 auto;
198
239
  display: flex;
199
240
  flex-direction: column;
200
241
  align-items: flex-start;
201
242
  gap: 4px;
243
+ position: relative;
202
244
  }
203
245
 
204
246
  .chip {
@@ -220,7 +262,12 @@
220
262
  background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05));
221
263
  }
222
264
 
265
+ /* chip 在输入框卡片正上方,展开面板向上打开(向下会盖住输入区)。 */
223
266
  .chipPanel {
267
+ position: absolute;
268
+ bottom: calc(100% + 4px);
269
+ left: 0;
270
+ z-index: 50;
224
271
  margin: 0;
225
272
  padding: 4px;
226
273
  list-style: none;
@@ -33,6 +33,29 @@ export function createAnnotationChip(store: AnnotationStore) {
33
33
  () => store.getSnapshot(),
34
34
  )
35
35
  const [expanded, setExpanded] = useState(false)
36
+ const rootRef = useRef<HTMLDivElement | null>(null)
37
+
38
+ // 展开态的关闭路径:Esc / 点击面板外部(与浮层编辑器同款心智)。
39
+ useEffect(() => {
40
+ if (!expanded) return
41
+ const onKeyDown = (event: KeyboardEvent): void => {
42
+ if (event.key === 'Escape') {
43
+ event.stopPropagation()
44
+ setExpanded(false)
45
+ }
46
+ }
47
+ const onMouseDown = (event: MouseEvent): void => {
48
+ const root = rootRef.current
49
+ if (root === null || !(event.target instanceof Node)) return
50
+ if (!root.contains(event.target)) setExpanded(false)
51
+ }
52
+ document.addEventListener('keydown', onKeyDown, true)
53
+ document.addEventListener('mousedown', onMouseDown, true)
54
+ return () => {
55
+ document.removeEventListener('keydown', onKeyDown, true)
56
+ document.removeEventListener('mousedown', onMouseDown, true)
57
+ }
58
+ }, [expanded])
36
59
 
37
60
  // 发送沿检测:草稿非空→空 且 伴随机器信号(提交相位/队列增长/running 启
38
61
  // 动)——纯「草稿清空」不算发送(用户手动全选删除满足前者,但没有机器信
@@ -69,7 +92,7 @@ export function createAnnotationChip(store: AnnotationStore) {
69
92
  if (active.length === 0) return null
70
93
 
71
94
  return (
72
- <div className={css.chipWrap}>
95
+ <div ref={rootRef} className={css.chipWrap}>
73
96
  <button
74
97
  type="button"
75
98
  className={css.chip}
@@ -40,6 +40,8 @@ interface SideDraftState {
40
40
  readonly snapshot: SelectionSnapshot
41
41
  readonly x: number
42
42
  readonly y: number
43
+ /** 目标侧边聊天 Tab 标题(编辑器上标明「发送至:侧边 N」)。 */
44
+ readonly targetTitle?: string
43
45
  }
44
46
 
45
47
  interface OverlayProps {
@@ -162,7 +164,8 @@ function AnnotateOverlayInner({ ctx, store, controller }: OverlayProps): ReactNo
162
164
  // WI-03 联动:先弹注解编辑器收集注解(可空),保存后经 bridge 注入侧边
163
165
  // 聊天草稿;本路径不产生主对话注释(无高亮/角标/chip)。
164
166
  const anchor = selectionBadgePoint(snapshot)
165
- setSideDraft({ snapshot, x: anchor.x, y: anchor.y })
167
+ const targetTitle = sideChatBridge.current?.peekTargetTitle(snapshot.sessionId)
168
+ setSideDraft({ snapshot, x: anchor.x, y: anchor.y, ...(targetTitle !== undefined ? { targetTitle } : {}) })
166
169
  controller.clear()
167
170
  window.getSelection()?.removeAllRanges()
168
171
  }
@@ -231,6 +234,7 @@ function AnnotateOverlayInner({ ctx, store, controller }: OverlayProps): ReactNo
231
234
  <SideChatNoteEditor
232
235
  x={sideDraft.x}
233
236
  y={sideDraft.y}
237
+ targetTitle={sideDraft.targetTitle}
234
238
  onSave={commitSideDraft}
235
239
  onCancel={() => { setSideDraft(null) }}
236
240
  />
@@ -244,6 +248,7 @@ function AnnotateOverlayInner({ ctx, store, controller }: OverlayProps): ReactNo
244
248
  function SideChatNoteEditor(props: {
245
249
  x: number
246
250
  y: number
251
+ targetTitle?: string
247
252
  onSave: (note: string) => boolean
248
253
  onCancel: () => void
249
254
  }): ReactNode {
@@ -284,30 +289,36 @@ function SideChatNoteEditor(props: {
284
289
  }
285
290
 
286
291
  return (
287
- <div ref={rootRef} className={css.editorNew} style={{ left, top, width }}>
288
- <input
289
- className={css.editorInput}
290
- value={note}
291
- placeholder={t('sideNotePlaceholder')}
292
- aria-label={t('sideNoteAria')}
293
- autoFocus
294
- onChange={(event) => { setNote(event.target.value); setFailed(false) }}
295
- onKeyDown={(event) => {
296
- if (event.key === 'Enter') {
292
+ <div ref={rootRef} className={css.editorAsk} style={{ left, top, width }}>
293
+ {props.targetTitle !== undefined && props.targetTitle !== '' && (
294
+ <div className={css.editorTarget}>{t('sendToTarget', { title: props.targetTitle })}</div>
295
+ )}
296
+ <div className={css.editorAskRow}>
297
+ <input
298
+ className={css.editorInput}
299
+ value={note}
300
+ placeholder={t('sideNotePlaceholder')}
301
+ aria-label={t('sideNoteAria')}
302
+ autoFocus
303
+ onChange={(event) => { setNote(event.target.value); setFailed(false) }}
304
+ onKeyDown={(event) => {
305
+ if (event.key !== 'Enter') return
306
+ // IME 保护:组合中(候选窗未提交)的 Enter 属于输入法,不能算确认。
307
+ if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return
297
308
  event.preventDefault()
298
309
  save()
299
- }
300
- }}
301
- />
302
- <button
303
- type="button"
304
- className={css.confirmButton}
305
- title={t('confirmTitle')}
306
- aria-label={t('confirmAskAria')}
307
- onClick={save}
308
- >
309
- <IconCheckOutline16 size={14} />
310
- </button>
310
+ }}
311
+ />
312
+ <button
313
+ type="button"
314
+ className={css.confirmButton}
315
+ title={t('confirmTitle')}
316
+ aria-label={t('confirmAskAria')}
317
+ onClick={save}
318
+ >
319
+ <IconCheckOutline16 size={14} />
320
+ </button>
321
+ </div>
311
322
  {failed && <div className={css.editorError}>{t('openSideFailed')}</div>}
312
323
  </div>
313
324
  )
@@ -323,10 +334,16 @@ function SelectionToolbar(props: {
323
334
  useLocaleTick()
324
335
  const { rect } = props.snapshot
325
336
  const left = Math.max(8, Math.min(window.innerWidth - 8, rect.left + rect.width / 2))
326
- const top = Math.max(4, rect.top - 10)
337
+ // 上方空间不足(选区贴视口顶部)时翻转到选区下方——工具条绝不能整体出屏。
338
+ // snapshot.rect 只有 left/top/width(首行矩形),翻转落点取整选区包围盒的底缘。
339
+ const flipBelow = rect.top < 64
340
+ const selectionBottom = props.snapshot.range.getBoundingClientRect().bottom
341
+ const top = flipBelow
342
+ ? Math.max(4, Math.min(window.innerHeight - 48, selectionBottom + 10))
343
+ : Math.max(4, rect.top - 10)
327
344
  return (
328
345
  <div
329
- className={css.toolbar}
346
+ className={flipBelow ? `${css.toolbar} ${css.toolbarBelow}` : css.toolbar}
330
347
  style={{ left, top }}
331
348
  role="toolbar"
332
349
  aria-label={t('toolbarAria')}
@@ -373,6 +390,12 @@ function BadgeLayer(props: {
373
390
  const badges: ReactNode[] = []
374
391
  const placed: { x: number; y: number }[] = []
375
392
  let highlight: ReactNode = null
393
+ // composer 区域(宿主 [data-composer-seat],非公开契约,查不到则只按视口
394
+ // 裁剪):锚点滚进 composer 遮挡区时角标必须隐藏——否则角标会画在 composer
395
+ // 卡片上,看起来像引用块自带的编号(极具迷惑性)。
396
+ const composerRect = typeof document === 'undefined'
397
+ ? null
398
+ : document.querySelector('[data-composer-seat]')?.getBoundingClientRect() ?? null
376
399
  for (const annotation of annotations) {
377
400
  const range = resolveRange(annotation, props.cache)
378
401
  if (range === null) continue
@@ -391,6 +414,10 @@ function BadgeLayer(props: {
391
414
  if (anchor === null) continue
392
415
  // 锚点滚出视口的角标不渲染(fixed 定位否则会漂浮在无关内容上方)。
393
416
  if (anchor.centerY < 0 || anchor.centerY > window.innerHeight || anchor.right < 0 || anchor.right > window.innerWidth) continue
417
+ // 锚点落入 composer 遮挡区的角标同样不渲染。
418
+ if (composerRect !== null
419
+ && anchor.centerY >= composerRect.top && anchor.centerY <= composerRect.bottom
420
+ && anchor.right >= composerRect.left && anchor.right <= composerRect.right) continue
394
421
  // 同一选区/相邻行的多个角标会落在同一点位——错开保证每个都可点击。
395
422
  const point = spreadBadgePoint({ x: anchor.right, y: anchor.centerY }, placed)
396
423
  placed.push(point)
@@ -477,10 +504,11 @@ function AnnotationEditor(props: {
477
504
  autoFocus
478
505
  onChange={(event) => { setNote(event.target.value) }}
479
506
  onKeyDown={(event) => {
480
- if (event.key === 'Enter') {
481
- event.preventDefault()
482
- save()
483
- }
507
+ if (event.key !== 'Enter') return
508
+ // IME 保护:组合中(候选窗未提交)的 Enter 属于输入法,不能算确认。
509
+ if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return
510
+ event.preventDefault()
511
+ save()
484
512
  }}
485
513
  />
486
514
  <button
@@ -506,10 +534,11 @@ function AnnotationEditor(props: {
506
534
  autoFocus
507
535
  onChange={(event) => { setNote(event.target.value) }}
508
536
  onKeyDown={(event) => {
509
- if (event.key === 'Enter' && !event.shiftKey) {
510
- event.preventDefault()
511
- save()
512
- }
537
+ if (event.key !== 'Enter' || event.shiftKey) return
538
+ // IME 保护:组合中(候选窗未提交)的 Enter 属于输入法,不能算保存。
539
+ if (event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return
540
+ event.preventDefault()
541
+ save()
513
542
  }}
514
543
  />
515
544
  <div className={css.editorFooter}>
@@ -14,6 +14,11 @@ export interface SideChatBridge {
14
14
  * when no side chat could be opened (caller keeps its own flow).
15
15
  */
16
16
  askInSideChat(sessionId: SessionId, draftText: string): boolean
17
+ /**
18
+ * 预览 askInSideChat 此刻会落到哪个侧边聊天 Tab 的标题(编辑器上标明
19
+ * 「发送至:侧边 N」);目标不可预知时返回 undefined。
20
+ */
21
+ peekTargetTitle(sessionId: SessionId): string | undefined
17
22
  }
18
23
 
19
24
  export const sideChatBridge: { current: SideChatBridge | null } = { current: null }
@@ -39,6 +39,17 @@ export const en = {
39
39
  stopReplyTitle: 'Stop the current reply',
40
40
  send: 'Send',
41
41
 
42
+ // transcript fallbacks (node → message folding)
43
+ imagePlaceholder: '[Image]',
44
+ toolFallback: 'Tool',
45
+ unknownError: 'Unknown error',
46
+ modelRetryStarted: 'Model request failed, retrying…',
47
+ modelRetryWaiting: 'Model request failed, waiting to retry…',
48
+ maxTokens: 'Output hit the length limit; this turn was truncated.',
49
+ runCommand: 'Run command {cmd}',
50
+ commandNameFallback: 'command',
51
+ compacted: 'Earlier conversation context was compacted.',
52
+
42
53
  // /side command
43
54
  cmdDesc: 'Open a side chat (forked from the current session)',
44
55
  cmdNew: 'New side chat',
@@ -57,6 +68,7 @@ export const en = {
57
68
  sideNoteAria: 'Side chat note',
58
69
  confirmAskAria: 'Confirm and ask',
59
70
  openSideFailed: "Couldn't open the side chat. Try again.",
71
+ sendToTarget: 'Send to: {title}',
60
72
  deleteNote: 'Delete annotation',
61
73
  cancel: 'Cancel',
62
74
  save: 'Save',
@@ -103,6 +115,16 @@ export const zh: Record<CopyKey, string> = {
103
115
  stopReplyTitle: '停止当前回复',
104
116
  send: '发送',
105
117
 
118
+ imagePlaceholder: '[图片]',
119
+ toolFallback: '工具',
120
+ unknownError: '未知错误',
121
+ modelRetryStarted: '模型请求失败,正在自动重试…',
122
+ modelRetryWaiting: '模型请求失败,等待自动重试…',
123
+ maxTokens: '输出达到长度上限,本轮回复已截断。',
124
+ runCommand: '执行命令 {cmd}',
125
+ commandNameFallback: '命令',
126
+ compacted: '已压缩更早的对话上下文。',
127
+
106
128
  cmdDesc: '打开侧边聊天(从当前会话 fork)',
107
129
  cmdNew: '新建侧边聊天',
108
130
  cmdNewDetail: '从当前会话 fork,独立演进',
@@ -119,6 +141,7 @@ export const zh: Record<CopyKey, string> = {
119
141
  sideNoteAria: '侧边聊天注解',
120
142
  confirmAskAria: '确认并提问',
121
143
  openSideFailed: '打开侧边聊天失败,请重试',
144
+ sendToTarget: '发送至:{title}',
122
145
  deleteNote: '删除注释',
123
146
  cancel: '取消',
124
147
  save: '保存',
@@ -26,7 +26,6 @@ import { readTab } from './open.ts'
26
26
  import { t, useLocaleTick } from '../locales.ts'
27
27
  import css from './sidechat.module.css'
28
28
 
29
- /** MarkdownText 代码块复制按钮文案(契约要求中文;引用稳定,变了会清流式缓存)。 */
30
29
  /** MarkdownText 代码块复制按钮文案(跟随 DSH 语言;函数内读取保持引用稳定)。 */
31
30
  function codeLabels() {
32
31
  return { copyLabel: t('codeCopy'), copiedLabel: t('codeCopied') }
@@ -169,11 +168,15 @@ export function SideChatPanel(props: TabComponentProps) {
169
168
 
170
169
  // ── 桥接草稿移交:meta.pendingDraft → composer 草稿,应用后清除 ──
171
170
  const pendingDraft = meta.pendingDraft
171
+ const rootRef = useRef<HTMLDivElement>(null)
172
172
  useEffect(() => {
173
173
  if (pendingDraft === undefined || pendingDraft === '' || phase !== 'chat') return
174
174
  composer.appendDraft(pendingDraft)
175
175
  const current = parseSideChatMeta(readTab(ctx, tab.id)?.meta)
176
176
  ctx.betterSidebar.updateTab(tab.id, { meta: clearPendingDraft(current) })
177
+ // 划选提问的落点体验:草稿注入后焦点直达输入框,用户接着打字即可。
178
+ // visible 预聚焦 effect 只在可见性跳变时跑,已可见的 tab 覆盖不到。
179
+ requestAnimationFrame(() => { rootRef.current?.querySelector('textarea')?.focus() })
177
180
  // appendDraft 随草稿逐键换身份;不列入依赖 —— effect 只在
178
181
  // pendingDraft/相位变化时真正动作(清除后 pendingDraft 为 undefined,幂等)。
179
182
  }, [pendingDraft, phase, ctx, tab.id])
@@ -213,7 +216,7 @@ export function SideChatPanel(props: TabComponentProps) {
213
216
  const openFailed = snapshot?.openState === 'error'
214
217
 
215
218
  return (
216
- <div className={css.root}>
219
+ <div ref={rootRef} className={css.root}>
217
220
  <div ref={bodyRef} className={css.body}>
218
221
  {messages.length === 0 && !running
219
222
  ? <EmptyState />
@@ -20,7 +20,7 @@ import type { Context } from '../../context-types.ts'
20
20
  import { sideChatBridge } from '../bridge.ts'
21
21
  import { SideChatPanel } from './SideChatPanel.tsx'
22
22
  import { SIDE_TAB_TYPE, canForkFrom, collectSideTabs, mintSideTabId, sideTabTitle } from './model.ts'
23
- import { openOrFocusSideChat } from './open.ts'
23
+ import { openOrFocusSideChat, sideChatTargetTitle } from './open.ts'
24
24
  import { t } from '../locales.ts'
25
25
  import { registerSideCommand } from './slash.ts'
26
26
 
@@ -45,6 +45,8 @@ export function registerSideChat(ctx: Context): void {
45
45
  const impl = {
46
46
  askInSideChat: (sessionId: string, draftText: string): boolean =>
47
47
  openOrFocusSideChat(ctx, sessionId, draftText),
48
+ peekTargetTitle: (sessionId: string): string | undefined =>
49
+ sideChatTargetTitle(ctx, sessionId),
48
50
  }
49
51
  sideChatBridge.current = impl
50
52
  return () => {
@@ -90,13 +90,23 @@ function walkTree(node: unknown, into: SidebarTab[]): void {
90
90
  if (Array.isArray(n.children)) for (const child of n.children) walkTree(child, into)
91
91
  }
92
92
 
93
- /** 枚举侧栏状态(splits + bottomSplits 两棵树)里的全部 Tab。 */
93
+ /**
94
+ * 枚举侧栏状态里的全部 Tab:splits + bottomSplits 两棵树,外加 0.16 引入的
95
+ * 浮动窗 floats(元素形态 { id, tab, … },tab 为窗口持有的 SidebarTab)。
96
+ * floats 缺失(0.16 之前)时数组检查天然跳过,前后版本通吃。
97
+ */
94
98
  export function collectTabs(state: unknown): SidebarTab[] {
95
99
  if (typeof state !== 'object' || state === null) return []
96
- const s = state as { splits?: unknown; bottomSplits?: unknown }
100
+ const s = state as { splits?: unknown; bottomSplits?: unknown; floats?: unknown }
97
101
  const into: SidebarTab[] = []
98
102
  walkTree(s.splits, into)
99
103
  walkTree(s.bottomSplits, into)
104
+ if (Array.isArray(s.floats)) {
105
+ for (const float of s.floats) {
106
+ const tab = (float as { tab?: unknown } | null)?.tab
107
+ if (isTab(tab)) into.push(tab)
108
+ }
109
+ }
100
110
  return into
101
111
  }
102
112
 
@@ -105,11 +115,6 @@ export function collectSideTabs(state: unknown): SidebarTab[] {
105
115
  return collectTabs(state).filter(tab => tab.type === SIDE_TAB_TYPE)
106
116
  }
107
117
 
108
- /** 并存数(createTab 编号标题的输入)。 */
109
- export function countSideTabs(state: unknown): number {
110
- return collectSideTabs(state).length
111
- }
112
-
113
118
  // ── fork 准入 ────────────────────────────────────────────────────────────────
114
119
 
115
120
  /**
@@ -201,7 +206,7 @@ export function contentTextOf(content: unknown): string {
201
206
  if (typeof block !== 'object' || block === null) continue
202
207
  const b = block as LooseBlock
203
208
  if (b.type === 'text' && typeof b.text === 'string') parts.push(b.text)
204
- else if (b.type === 'image') parts.push('[图片]')
209
+ else if (b.type === 'image') parts.push(t('imagePlaceholder'))
205
210
  }
206
211
  return parts.join('\n')
207
212
  }
@@ -254,7 +259,7 @@ export function nodeToMessage(node: unknown): ChatMessage | null {
254
259
  const call = n.call as { name?: unknown } | null
255
260
  const toolName = typeof call?.name === 'string'
256
261
  ? call.name
257
- : typeof n.callId === 'string' ? n.callId : '工具'
262
+ : typeof n.callId === 'string' ? n.callId : t('toolFallback')
258
263
  return {
259
264
  key: seqKey('t', n),
260
265
  role: 'tool',
@@ -264,24 +269,26 @@ export function nodeToMessage(node: unknown): ChatMessage | null {
264
269
  }
265
270
  }
266
271
  case 'turn-error':
267
- return { key: seqKey('e', n), role: 'error', text: typeof n.message === 'string' ? n.message : '未知错误' }
272
+ return { key: seqKey('e', n), role: 'error', text: typeof n.message === 'string' ? n.message : t('unknownError') }
268
273
  case 'model-retry': {
269
274
  if (n.retryState === 'cancelled') return null
270
275
  return {
271
276
  key: seqKey('r', n),
272
277
  role: 'notice',
273
- text: n.retryState === 'started' ? '模型请求失败,正在自动重试…' : '模型请求失败,等待自动重试…',
278
+ text: t(n.retryState === 'started' ? 'modelRetryStarted' : 'modelRetryWaiting'),
274
279
  }
275
280
  }
276
281
  case 'turn-max-tokens':
277
- return { key: seqKey('m', n), role: 'notice', text: '输出达到长度上限,本轮回复已截断。' }
282
+ return { key: seqKey('m', n), role: 'notice', text: t('maxTokens') }
278
283
  case 'command': {
279
- const name = typeof n.name === 'string' && n.name !== '' ? `/${n.name}` : '/命令'
280
- const args = typeof n.args === 'string' ? n.args : ''
281
- return { key: seqKey('c', n), role: 'notice', text: `执行命令 ${name}${args}` }
284
+ const name = typeof n.name === 'string' && n.name !== '' ? n.name : t('commandNameFallback')
285
+ // args 数据源自带前导空格(「/goal x」形态),trimStart 后统一补一个空格,
286
+ // 防止「/sidefoo」(缺分隔)或「/goal x」(双空格)。
287
+ const args = typeof n.args === 'string' ? n.args.trimStart() : ''
288
+ return { key: seqKey('c', n), role: 'notice', text: t('runCommand', { cmd: `/${name}${args === '' ? '' : ` ${args}`}` }) }
282
289
  }
283
290
  case 'compaction':
284
- return { key: seqKey('k', n), role: 'notice', text: '已压缩更早的对话上下文。' }
291
+ return { key: seqKey('k', n), role: 'notice', text: t('compacted') }
285
292
  default:
286
293
  // context(注入)/ unknown(未识面事件):MVP 不渲染。
287
294
  return null
@@ -307,7 +314,7 @@ export function transcriptOf(snapshot: ConversationSnapshot | undefined | null):
307
314
  out.push({
308
315
  key: `rc:${typeof c.callId === 'string' ? c.callId : '?'}`,
309
316
  role: 'tool',
310
- toolName: typeof c.name === 'string' ? c.name : '工具',
317
+ toolName: typeof c.name === 'string' ? c.name : t('toolFallback'),
311
318
  text: '',
312
319
  streaming: true,
313
320
  })
@@ -13,6 +13,7 @@ import {
13
13
  collectSideTabs,
14
14
  collectTabs,
15
15
  parseSideChatMeta,
16
+ sideTabTitle,
16
17
  } from './model.ts'
17
18
  import { readInputDraft, resolveSessionInput } from './composer.ts'
18
19
 
@@ -49,6 +50,22 @@ export function openOrFocusSideChat(ctx: Context, sessionId: string, draftText?:
49
50
  return true
50
51
  }
51
52
 
53
+ return createSideChat(ctx, sessionId, draftText)
54
+ } catch (error) {
55
+ console.warn('[dsh-sidenote] 打开侧边聊天失败:', error)
56
+ return false
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 无条件新建一个侧边聊天 Tab(/side「新建侧边聊天」选项的语义——既有实例
62
+ * 在弹层里另有聚焦项,「新建」必须真的新建,否则标签与行为不符)。
63
+ * @returns false = 未能创建(调用方保持自己的流程)。
64
+ */
65
+ export function createSideChat(ctx: Context, sessionId: string, draftText?: string): boolean {
66
+ try {
67
+ const snapshot = ctx.betterSidebar.getSnapshot()
68
+ if (snapshot.sessionId !== sessionId || snapshot.state === undefined) return false
52
69
  // 新建:openTab 走 createTab 铸造(seed.meta 会被忽略),所以先记下既有
53
70
  // id 集,openTab 同步落状态后找出新 Tab,再把 pendingDraft 写进它的 meta。
54
71
  const before = new Set(collectTabs(snapshot.state).map(tab => tab.id))
@@ -60,7 +77,24 @@ export function openOrFocusSideChat(ctx: Context, sessionId: string, draftText?:
60
77
  }
61
78
  return true
62
79
  } catch (error) {
63
- console.warn('[dsh-sidenote] 打开侧边聊天失败:', error)
80
+ console.warn('[dsh-sidenote] 新建侧边聊天失败:', error)
64
81
  return false
65
82
  }
66
83
  }
84
+
85
+ /**
86
+ * 桥接/命令的目标预览:askInSideChat 此时若执行会落在哪个 Tab 的标题
87
+ * (既有并存 = 最后一个;无 = 即将新建的首个标题)。用于编辑器上标明
88
+ * 「发送至:侧边 N」。会话不在屏上/状态缺失时返回 undefined。
89
+ */
90
+ export function sideChatTargetTitle(ctx: Context, sessionId: string): string | undefined {
91
+ try {
92
+ const snapshot = ctx.betterSidebar.getSnapshot()
93
+ if (snapshot.sessionId !== sessionId || snapshot.state === undefined) return undefined
94
+ const existing = collectSideTabs(snapshot.state)
95
+ if (existing.length > 0) return existing[existing.length - 1]!.title
96
+ return sideTabTitle([])
97
+ } catch {
98
+ return undefined
99
+ }
100
+ }
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { Context } from '../../context-types.ts'
11
11
  import { canForkFrom, collectSideTabs } from './model.ts'
12
- import { openOrFocusSideChat } from './open.ts'
12
+ import { createSideChat } from './open.ts'
13
13
  import { t } from '../locales.ts'
14
14
 
15
15
  /** dsh-client-ui-commands ClientSessionContext 的最小镜像(只有 sessionId)。 */
@@ -69,7 +69,9 @@ export function registerSideCommand(ctx: Context): void {
69
69
  },
70
70
  onSelect: (option, session) => {
71
71
  if (option.id === 'new') {
72
- openOrFocusSideChat(ctx, session.sessionId)
72
+ // 「新建侧边聊天」必须真新建——既有实例的聚焦项在弹层里另列,
73
+ // 走 openOrFocusSideChat 会在已有侧边聊天时静默聚焦(标签说谎)。
74
+ createSideChat(ctx, session.sessionId)
73
75
  return
74
76
  }
75
77
  if (option.id.startsWith('focus:')) {