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 +188 -83
- package/lib/types/client/bridge.d.ts +5 -0
- package/lib/types/client/locales.d.ts +10 -0
- package/lib/types/client/sidechat/model.d.ts +5 -3
- package/lib/types/client/sidechat/open.d.ts +12 -0
- package/package.json +2 -2
- package/src/client/annotate/annotate.module.css +47 -0
- package/src/client/annotate/chip.tsx +24 -1
- package/src/client/annotate/overlay.tsx +62 -33
- package/src/client/bridge.ts +5 -0
- package/src/client/locales.ts +23 -0
- package/src/client/sidechat/SideChatPanel.tsx +5 -2
- package/src/client/sidechat/index.tsx +3 -1
- package/src/client/sidechat/model.ts +24 -17
- package/src/client/sidechat/open.ts +35 -1
- package/src/client/sidechat/slash.ts +4 -2
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
|
-
/**
|
|
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 !== "" ?
|
|
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:
|
|
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]
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
542
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
561
|
-
"
|
|
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
|
-
|
|
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 = {
|
|
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
|
-
"
|
|
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
|
-
"
|
|
1461
|
-
"
|
|
1462
|
-
"
|
|
1463
|
-
"
|
|
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.
|
|
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)("
|
|
1698
|
-
className: annotate_module_css_default.
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
|
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
|
|
1867
|
-
|
|
1868
|
-
|
|
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
|
|
1899
|
-
|
|
1900
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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.
|
|
288
|
-
|
|
289
|
-
className={css.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
</
|
|
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
|
-
|
|
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
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
510
|
-
|
|
511
|
-
|
|
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}>
|
package/src/client/bridge.ts
CHANGED
|
@@ -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 }
|
package/src/client/locales.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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 !== '' ?
|
|
280
|
-
|
|
281
|
-
|
|
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]
|
|
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 {
|
|
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
|
-
|
|
72
|
+
// 「新建侧边聊天」必须真新建——既有实例的聚焦项在弹层里另列,
|
|
73
|
+
// 走 openOrFocusSideChat 会在已有侧边聊天时静默聚焦(标签说谎)。
|
|
74
|
+
createSideChat(ctx, session.sessionId)
|
|
73
75
|
return
|
|
74
76
|
}
|
|
75
77
|
if (option.id.startsWith('focus:')) {
|