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