dsh-sidenote 0.3.2 → 0.3.4
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 +71 -40
- package/lib/client.js +960 -634
- package/lib/types/client/host/contracts.d.ts +45 -0
- package/lib/types/client/sidechat/lifecycle.d.ts +36 -2
- package/lib/types/client/sidechat/native.d.ts +87 -0
- package/lib/types/client/sidechat/open.d.ts +5 -1
- package/lib/types/client/sidechat/slash.d.ts +3 -1
- package/package.json +1 -1
- package/src/client/host/contracts.ts +29 -0
- package/src/client/host/probes.ts +27 -0
- package/src/client/index.tsx +8 -0
- package/src/client/sidechat/ModelMenu.tsx +4 -4
- package/src/client/sidechat/SideChatPanel.tsx +57 -1
- package/src/client/sidechat/index.tsx +13 -3
- package/src/client/sidechat/lifecycle.ts +128 -22
- package/src/client/sidechat/native.ts +192 -0
- package/src/client/sidechat/open.ts +63 -7
- package/src/client/sidechat/slash.ts +46 -13
package/lib/client.js
CHANGED
|
@@ -4,57 +4,136 @@ 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 _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
8
7
|
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
10
|
let react_dom_client = require("react-dom/client");
|
|
11
|
-
//#region src/client/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
name: "ctx.modules.import(动态组件复用)",
|
|
36
|
-
check: (ctx) => {
|
|
37
|
-
return typeof ctx.modules?.import === "function";
|
|
38
|
-
},
|
|
39
|
-
fallback: "ProducedFiles 等动态复用件降级为自绘"
|
|
11
|
+
//#region src/client/sidechat/native.ts
|
|
12
|
+
/** better-sidebar version that moved the right column to DSH's native sidebar. */
|
|
13
|
+
const NATIVE_SIDEBAR_MINOR = 19;
|
|
14
|
+
let root;
|
|
15
|
+
/** Remember the plugin root context at activation (see module docstring). */
|
|
16
|
+
function setRootContext(ctx) {
|
|
17
|
+
root = ctx;
|
|
18
|
+
}
|
|
19
|
+
/** Plugin root context; falls back to the caller's context (legacy behaviour). */
|
|
20
|
+
function rootContext(fallback) {
|
|
21
|
+
return root ?? fallback;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* True when the host keeps sidebar tabs outside the legacy layout store
|
|
25
|
+
* (better-sidebar >= 0.19). Version parse is best-effort: an unparsable
|
|
26
|
+
* version reports false, which keeps the pre-0.19 behaviour.
|
|
27
|
+
*/
|
|
28
|
+
function nativeSidebarHost(ctx) {
|
|
29
|
+
try {
|
|
30
|
+
const [major = 0, minor = 0] = ctx.betterSidebar.version.split(".").map((part) => Number.parseInt(part, 10));
|
|
31
|
+
return major > 0 || minor >= NATIVE_SIDEBAR_MINOR;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
40
34
|
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
}
|
|
36
|
+
/** Native tab id → live panel. Native layouts are memory-only, so is this. */
|
|
37
|
+
const liveSideChats = /* @__PURE__ */ new Map();
|
|
38
|
+
/**
|
|
39
|
+
* tabId → 首次注册序号(打开序的近似)。tab.id 跨重挂载稳定,重注册不改写
|
|
40
|
+
* ——遍历 Map 的插入序会随重挂漂移,「最近打开」只能锚在首次序号上。
|
|
41
|
+
*/
|
|
42
|
+
const birthOrder = /* @__PURE__ */ new Map();
|
|
43
|
+
let birthSeq = 0;
|
|
44
|
+
/**
|
|
45
|
+
* Publish a mounted panel so programmatic entry points can focus it and seed
|
|
46
|
+
* its draft. Returns the disposer for the panel's effect cleanup.
|
|
47
|
+
*/
|
|
48
|
+
function registerLiveSideChat(sessionId, tabId, seedDraft, readMeta, readTitle) {
|
|
49
|
+
const entry = {
|
|
50
|
+
tabId,
|
|
51
|
+
sessionId,
|
|
52
|
+
seedDraft,
|
|
53
|
+
readMeta,
|
|
54
|
+
readTitle
|
|
55
|
+
};
|
|
56
|
+
liveSideChats.set(tabId, entry);
|
|
57
|
+
if (!birthOrder.has(tabId)) birthOrder.set(tabId, ++birthSeq);
|
|
58
|
+
const opening = openings.get(sessionId);
|
|
59
|
+
if (opening !== void 0) {
|
|
60
|
+
openings.delete(sessionId);
|
|
61
|
+
for (const draft of opening.drafts) seedDraft(draft);
|
|
62
|
+
}
|
|
63
|
+
return () => {
|
|
64
|
+
if (liveSideChats.get(tabId) === entry) liveSideChats.delete(tabId);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** 标记失效应力:openTab 被静默拒绝/面板未挂载时,超时后允许重试。 */
|
|
68
|
+
const OPENING_TTL_MS = 1e4;
|
|
69
|
+
const openings = /* @__PURE__ */ new Map();
|
|
70
|
+
/** 标记一次已发起、面板尚未注册的 native 打开(create/reopen 铸造后调用)。 */
|
|
71
|
+
function markSideChatOpening(sessionId) {
|
|
72
|
+
openings.set(sessionId, {
|
|
73
|
+
at: Date.now(),
|
|
74
|
+
drafts: []
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** 进行中的打开;TTL 外视为 openTab 被拒/面板未挂载,清除并返回 undefined。 */
|
|
78
|
+
function sideChatOpening(sessionId) {
|
|
79
|
+
const mark = openings.get(sessionId);
|
|
80
|
+
if (mark === void 0) return void 0;
|
|
81
|
+
if (Date.now() - mark.at > OPENING_TTL_MS) {
|
|
82
|
+
openings.delete(sessionId);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
return mark;
|
|
86
|
+
}
|
|
87
|
+
/** 一个会话的全部存活面板(按打开序升序;native 下枚举 side tab 的唯一数据源)。 */
|
|
88
|
+
function liveSideChatsOf(sessionId) {
|
|
89
|
+
return [...liveSideChats.values()].filter((entry) => entry.sessionId === sessionId).sort((a, b) => (birthOrder.get(a.tabId) ?? 0) - (birthOrder.get(b.tabId) ?? 0));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Most recently OPENED live panel of a session — the open-or-focus target
|
|
93
|
+
* when the legacy snapshot cannot see the tab (mirrors "last opened wins").
|
|
94
|
+
* 排序锚在首次注册序号上:面板重挂载不翻序(见 birthOrder)。
|
|
95
|
+
*/
|
|
96
|
+
function lastLiveSideChat(sessionId) {
|
|
97
|
+
const all = liveSideChatsOf(sessionId);
|
|
98
|
+
return all[all.length - 1];
|
|
99
|
+
}
|
|
100
|
+
/** 插件卸载/重载时清场(apply 的 ctx.effect disposer 调用)。 */
|
|
101
|
+
function clearNativeRuntime() {
|
|
102
|
+
root = void 0;
|
|
103
|
+
liveSideChats.clear();
|
|
104
|
+
openings.clear();
|
|
105
|
+
birthOrder.clear();
|
|
106
|
+
}
|
|
107
|
+
/** Native tab shell for reads that go through the layout snapshot (`readTab`). */
|
|
108
|
+
function nativeTabShell(tabId, type) {
|
|
109
|
+
const entry = liveSideChats.get(tabId);
|
|
110
|
+
if (entry === void 0) return void 0;
|
|
111
|
+
return {
|
|
112
|
+
id: entry.tabId,
|
|
113
|
+
type,
|
|
114
|
+
title: "",
|
|
115
|
+
meta: entry.readMeta()
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 聚焦一个 native tab:better-sidebar >= 0.19 的 `surface.activate` 是空操作
|
|
120
|
+
* (只返回记录存在性——authority: dsh-better-sidebar src/client/native/surface.ts),
|
|
121
|
+
* 真实聚焦面是 dsh 0.1.5 的 `ISidebarRight.focus`(authority:
|
|
122
|
+
* dsh-client-ui-sidebar-right lib/types/client/service.d.ts `focus(tabId)`)。
|
|
123
|
+
* focus 只标记活动 tab/pane,不展开折叠的栏——聚焦的语义包含「让用户看见」,
|
|
124
|
+
* 故折叠时补 toggleExpanded(authority: 同文件 `isExpanded/toggleExpanded`)。
|
|
125
|
+
* @returns true = 已通过 focus 聚焦;false = 面缺席(调用方回退 activateTab)。
|
|
126
|
+
*/
|
|
127
|
+
function focusNativeTab(ctx, tabId) {
|
|
128
|
+
try {
|
|
129
|
+
const face = ctx.get("sidebarRight");
|
|
130
|
+
if (typeof face?.focus !== "function") return false;
|
|
131
|
+
face.focus(tabId);
|
|
132
|
+
if (face.isExpanded?.() === false) face.toggleExpanded?.();
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
56
136
|
}
|
|
57
|
-
return missing;
|
|
58
137
|
}
|
|
59
138
|
//#endregion
|
|
60
139
|
//#region src/client/locales.ts
|
|
@@ -350,135 +429,6 @@ window.__ModuleLoader__.load({
|
|
|
350
429
|
return text;
|
|
351
430
|
}
|
|
352
431
|
//#endregion
|
|
353
|
-
//#region src/client/reflow.ts
|
|
354
|
-
/**
|
|
355
|
-
* 回流通道的受控对象层(Delivery_02 Workitem_04 v2):侧边聊天的结论以
|
|
356
|
-
* 「待回流对象」进入主会话——主 composer 上方的 chip(可预览/可移除),
|
|
357
|
-
* 发送瞬间由 annotate/send.ts 拦截器与注释协议块一起序列化进消息。
|
|
358
|
-
* 不经草稿文本流(用户裁定:直接进草稿「很脏」)。
|
|
359
|
-
*
|
|
360
|
-
* 气泡留痕:序列化形态是 XML 协议块(见 buildReflowBlock),随注释协议块
|
|
361
|
-
* 一起作为消息连续前缀注入,bubble.ts 反解析后折叠为「含侧边回流上下文」标签。
|
|
362
|
-
*
|
|
363
|
-
* 持久化:localStorage 按会话键(dsh-sidenote:reflow:v1:<sessionId>),
|
|
364
|
-
* 与注释 store 同款纪律(刷新不丢、容错 revive、空删键)。
|
|
365
|
-
*/
|
|
366
|
-
const STORAGE_PREFIX$1 = "dsh-sidenote:reflow:v1:";
|
|
367
|
-
/**
|
|
368
|
-
* 回流块序列化(XML 形态,与注释协议同族):
|
|
369
|
-
*
|
|
370
|
-
* <reflow source="侧边 2" reason="用户选择从侧边聊天带回主线">
|
|
371
|
-
* <问>用户在侧边聊天里的提问</问>
|
|
372
|
-
* <答>结论全文(不截断)</答>
|
|
373
|
-
* </reflow>
|
|
374
|
-
*
|
|
375
|
-
* 问答成对:只有答没有问,模型理解打折;question 缺省时只有 <答>。
|
|
376
|
-
* 来源与意图放在属性里,内容行保持纯净;模型对 XML 包裹的上下文理解最好
|
|
377
|
-
* (Claude 官方推荐形态 / Codex additionalContext 同款思路)。
|
|
378
|
-
*/
|
|
379
|
-
function buildReflowBlock(item) {
|
|
380
|
-
const parts = [];
|
|
381
|
-
if (item.question !== void 0 && item.question.trim() !== "") parts.push(`<问>${item.question}</问>`);
|
|
382
|
-
parts.push(`<答>${item.text}</答>`);
|
|
383
|
-
return `<reflow source="${item.sideTitle}" reason="${t("reflowReason")}">\n${parts.join("\n")}\n</reflow>`;
|
|
384
|
-
}
|
|
385
|
-
function revive(value) {
|
|
386
|
-
if (typeof value !== "object" || value === null) return null;
|
|
387
|
-
const r = value;
|
|
388
|
-
if (typeof r.id !== "number" || typeof r.sessionId !== "string" || r.sessionId === "") return null;
|
|
389
|
-
if (typeof r.sideTitle !== "string" || typeof r.text !== "string") return null;
|
|
390
|
-
return {
|
|
391
|
-
id: r.id,
|
|
392
|
-
sessionId: r.sessionId,
|
|
393
|
-
sideTitle: r.sideTitle,
|
|
394
|
-
text: r.text,
|
|
395
|
-
...typeof r.question === "string" && r.question.trim() !== "" ? { question: r.question } : {},
|
|
396
|
-
createdAt: typeof r.createdAt === "number" ? r.createdAt : 0
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
function createReflowStore(now = () => Date.now(), storage) {
|
|
400
|
-
let items = [];
|
|
401
|
-
let nextId = 1;
|
|
402
|
-
let version = 0;
|
|
403
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
404
|
-
const store = storage === void 0 ? typeof localStorage !== "undefined" ? localStorage : null : storage;
|
|
405
|
-
if (store != null) try {
|
|
406
|
-
const keys = [];
|
|
407
|
-
for (let i = 0; i < store.length; i += 1) {
|
|
408
|
-
const key = store.key(i);
|
|
409
|
-
if (typeof key === "string" && key.startsWith(STORAGE_PREFIX$1)) keys.push(key);
|
|
410
|
-
}
|
|
411
|
-
for (const key of keys) {
|
|
412
|
-
const parsed = JSON.parse(store.getItem(key) ?? "null");
|
|
413
|
-
if (!Array.isArray(parsed)) continue;
|
|
414
|
-
for (const raw of parsed) {
|
|
415
|
-
const item = revive(raw);
|
|
416
|
-
if (item === null) continue;
|
|
417
|
-
items.push(item);
|
|
418
|
-
nextId = Math.max(nextId, item.id + 1);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
} catch (error) {
|
|
422
|
-
console.warn("[dsh-sidenote] 回流持久化读取失败(按空起步):", error);
|
|
423
|
-
}
|
|
424
|
-
const persist = (sessionId) => {
|
|
425
|
-
if (store == null) return;
|
|
426
|
-
try {
|
|
427
|
-
const mine = items.filter((i) => i.sessionId === sessionId);
|
|
428
|
-
const key = STORAGE_PREFIX$1 + sessionId;
|
|
429
|
-
if (mine.length === 0) store.removeItem(key);
|
|
430
|
-
else store.setItem(key, JSON.stringify(mine));
|
|
431
|
-
} catch (error) {
|
|
432
|
-
console.warn("[dsh-sidenote] 回流持久化写入失败:", error);
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
const notify = () => {
|
|
436
|
-
version += 1;
|
|
437
|
-
for (const fn of [...listeners]) fn();
|
|
438
|
-
};
|
|
439
|
-
return {
|
|
440
|
-
getSnapshot: () => version,
|
|
441
|
-
subscribe(fn) {
|
|
442
|
-
listeners.add(fn);
|
|
443
|
-
return () => {
|
|
444
|
-
listeners.delete(fn);
|
|
445
|
-
};
|
|
446
|
-
},
|
|
447
|
-
add(sessionId, sideTitle, text, question) {
|
|
448
|
-
const item = {
|
|
449
|
-
id: nextId,
|
|
450
|
-
sessionId,
|
|
451
|
-
sideTitle,
|
|
452
|
-
text,
|
|
453
|
-
...typeof question === "string" && question.trim() !== "" ? { question } : {},
|
|
454
|
-
createdAt: now()
|
|
455
|
-
};
|
|
456
|
-
nextId += 1;
|
|
457
|
-
items = [...items, item];
|
|
458
|
-
persist(sessionId);
|
|
459
|
-
notify();
|
|
460
|
-
return item;
|
|
461
|
-
},
|
|
462
|
-
remove(id) {
|
|
463
|
-
const target = items.find((i) => i.id === id);
|
|
464
|
-
if (target === void 0) return;
|
|
465
|
-
items = items.filter((i) => i.id !== id);
|
|
466
|
-
persist(target.sessionId);
|
|
467
|
-
notify();
|
|
468
|
-
},
|
|
469
|
-
clearSession(sessionId) {
|
|
470
|
-
if (!items.some((i) => i.sessionId === sessionId)) return;
|
|
471
|
-
items = items.filter((i) => i.sessionId !== sessionId);
|
|
472
|
-
persist(sessionId);
|
|
473
|
-
notify();
|
|
474
|
-
},
|
|
475
|
-
list: (sessionId) => items.filter((i) => i.sessionId === sessionId)
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
//#endregion
|
|
479
|
-
//#region src/client/bridge.ts
|
|
480
|
-
const sideChatBridge = { current: null };
|
|
481
|
-
//#endregion
|
|
482
432
|
//#region src/client/annotate/format.ts
|
|
483
433
|
/** 协议头(双语):zh「我批注了以下 2 处内容:」/ en 见 locales。 */
|
|
484
434
|
function protocolHeader(n) {
|
|
@@ -831,54 +781,591 @@ window.__ModuleLoader__.load({
|
|
|
831
781
|
};
|
|
832
782
|
}
|
|
833
783
|
//#endregion
|
|
834
|
-
//#region src/client/
|
|
835
|
-
/** ContentBlock[] → 纯文本(transcript.ts 同款的极简版,避免互依赖)。 */
|
|
836
|
-
function blocksText(content) {
|
|
837
|
-
if (!Array.isArray(content)) return void 0;
|
|
838
|
-
const parts = [];
|
|
839
|
-
for (const block of content) {
|
|
840
|
-
const b = block;
|
|
841
|
-
if (b?.type === "text" && typeof b.text === "string") parts.push(b.text);
|
|
842
|
-
}
|
|
843
|
-
return parts.length === 0 ? void 0 : parts.join("\n");
|
|
844
|
-
}
|
|
845
|
-
/** 未知 card 值的 warn-once 登记(T3 纪律:降级不静默)。 */
|
|
846
|
-
const warnedCards = /* @__PURE__ */ new Set();
|
|
847
|
-
function warnUnknownCard(value) {
|
|
848
|
-
if (warnedCards.has(value)) return;
|
|
849
|
-
warnedCards.add(value);
|
|
850
|
-
console.warn(`[dsh-sidenote] 未知工具卡种类 "${value}"——降级为 generic 卡(宿主可能扩展了 card union)`);
|
|
851
|
-
}
|
|
852
|
-
/** 相对 cwd 按会话工作区折成绝对路径(UI bridge 责任——presentation 注释明文)。 */
|
|
853
|
-
function resolveCwd(cwd, cwdBase) {
|
|
854
|
-
if (cwd === void 0) return cwdBase;
|
|
855
|
-
if (cwd.startsWith("/") || /^[A-Za-z]:[\\/]/.test(cwd)) return cwd;
|
|
856
|
-
if (cwdBase === void 0) return cwd;
|
|
857
|
-
return `${cwdBase.replace(/\/+$/, "")}/${cwd}`;
|
|
858
|
-
}
|
|
784
|
+
//#region src/client/sidechat/open.ts
|
|
859
785
|
/**
|
|
860
|
-
*
|
|
861
|
-
*
|
|
786
|
+
* 从最新快照读一个 Tab(meta 合并写入前的读取面;布局即注册表)。
|
|
787
|
+
* better-sidebar >= 0.19: the open tab lives in the native right sidebar, not
|
|
788
|
+
* in the layout snapshot — fall back to the live-panel registry.
|
|
862
789
|
*/
|
|
863
|
-
function
|
|
864
|
-
|
|
865
|
-
const segs = p.split("/").filter((s) => s !== "");
|
|
866
|
-
return segs.length > 3 ? segs.slice(-3).join("/") : p;
|
|
790
|
+
function readTab(ctx, tabId) {
|
|
791
|
+
return collectTabs(ctx.betterSidebar.getSnapshot().state).find((candidate) => candidate.id === tabId) ?? nativeTabShell(tabId, "dsh-sidenote:side");
|
|
867
792
|
}
|
|
868
793
|
/**
|
|
869
|
-
*
|
|
870
|
-
*
|
|
794
|
+
* 为 sessionId 新建或聚焦一个侧边聊天 Tab;draftText 给出时写入其草稿。
|
|
795
|
+
* @returns false = 未能打开(调用方保持自己的流程)。
|
|
871
796
|
*/
|
|
872
|
-
function
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
797
|
+
function openOrFocusSideChat(ctx, sessionId, draftText) {
|
|
798
|
+
try {
|
|
799
|
+
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
800
|
+
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
|
|
801
|
+
const existing = collectSideTabs(snapshot.state);
|
|
802
|
+
const live = lastLiveSideChat(sessionId);
|
|
803
|
+
if (live !== void 0) {
|
|
804
|
+
if (draftText !== void 0 && draftText !== "") live.seedDraft(draftText);
|
|
805
|
+
if (!focusNativeTab(ctx, live.tabId)) ctx.betterSidebar.activateTab(live.tabId, { sessionId });
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
if (existing.length > 0) {
|
|
809
|
+
const target = existing[existing.length - 1];
|
|
810
|
+
if (draftText !== void 0 && draftText !== "") {
|
|
811
|
+
const meta = parseSideChatMeta(target.meta);
|
|
812
|
+
const input = meta.childId === void 0 ? null : resolveSessionInput(ctx, meta.childId);
|
|
813
|
+
if (input !== null) input.setDraft(appendDraftText(readInputDraft(input), draftText));
|
|
814
|
+
else ctx.betterSidebar.updateTab(target.id, { meta: {
|
|
815
|
+
...meta,
|
|
816
|
+
pendingDraft: appendDraftText(meta.pendingDraft ?? "", draftText)
|
|
817
|
+
} });
|
|
818
|
+
}
|
|
819
|
+
ctx.betterSidebar.activateTab(target.id, { sessionId });
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
const opening = sideChatOpening(sessionId);
|
|
823
|
+
if (opening !== void 0) {
|
|
824
|
+
if (draftText !== void 0 && draftText !== "") opening.drafts.push(draftText);
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
return createSideChat(ctx, sessionId, draftText);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
console.warn("[dsh-sidenote] 打开侧边聊天失败:", error);
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* 无条件新建一个侧边聊天 Tab(/side「新建侧边聊天」选项的语义——既有实例
|
|
835
|
+
* 在弹层里另有聚焦项,「新建」必须真的新建,否则标签与行为不符)。
|
|
836
|
+
* @returns false = 未能创建(调用方保持自己的流程)。
|
|
837
|
+
*/
|
|
838
|
+
function createSideChat(ctx, sessionId, draftText) {
|
|
839
|
+
try {
|
|
840
|
+
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
841
|
+
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
|
|
842
|
+
if (!ctx.betterSidebar.isTabEnabled("dsh-sidenote:side")) return false;
|
|
843
|
+
const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
|
|
844
|
+
const seed = draftText !== void 0 && draftText !== "" ? {
|
|
845
|
+
type: SIDE_TAB_TYPE,
|
|
846
|
+
meta: { pendingDraft: draftText }
|
|
847
|
+
} : { type: SIDE_TAB_TYPE };
|
|
848
|
+
ctx.betterSidebar.openTab(seed, { sessionId });
|
|
849
|
+
const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
|
|
850
|
+
if (created === void 0) {
|
|
851
|
+
if (!nativeSidebarHost(ctx)) return false;
|
|
852
|
+
markSideChatOpening(sessionId);
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
if (draftText !== void 0 && draftText !== "") ctx.betterSidebar.updateTab(created.id, { meta: { pendingDraft: draftText } });
|
|
856
|
+
return true;
|
|
857
|
+
} catch (error) {
|
|
858
|
+
console.warn("[dsh-sidenote] 新建侧边聊天失败:", error);
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* 桥接/命令的目标预览:askInSideChat 此时若执行会落在哪个 Tab 的标题
|
|
864
|
+
* (既有并存 = 最后一个;无 = 即将新建的首个标题)。用于编辑器上标明
|
|
865
|
+
* 「发送至:侧边 N」。会话不在屏上/状态缺失时返回 undefined。
|
|
866
|
+
*/
|
|
867
|
+
function sideChatTargetTitle(ctx, sessionId) {
|
|
868
|
+
try {
|
|
869
|
+
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
870
|
+
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return void 0;
|
|
871
|
+
const existing = collectSideTabs(snapshot.state);
|
|
872
|
+
if (existing.length > 0) return existing[existing.length - 1].title;
|
|
873
|
+
const live = lastLiveSideChat(sessionId);
|
|
874
|
+
if (live !== void 0) return live.readTitle();
|
|
875
|
+
return sideTabTitle([]);
|
|
876
|
+
} catch {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* 重开最近关闭的侧边聊天(D3 后悔药):开 Tab + meta 预置 childId/
|
|
882
|
+
* parentSessionId——面板挂载后走绑定恢复路径(不重新 fork)。
|
|
883
|
+
* 会话本体必须仍 archived 在列(若被清理,面板落「会话已不存在」态——
|
|
884
|
+
* 有现成错误态兜着)。
|
|
885
|
+
*/
|
|
886
|
+
function reopenSideChat(ctx, sessionId, childId, title) {
|
|
887
|
+
try {
|
|
888
|
+
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
889
|
+
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
|
|
890
|
+
if (!ctx.betterSidebar.isTabEnabled("dsh-sidenote:side")) return false;
|
|
891
|
+
if (nativeSidebarHost(ctx) && liveSideChatsOf(sessionId).length > 0) return false;
|
|
892
|
+
const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
|
|
893
|
+
const meta = {
|
|
894
|
+
childId,
|
|
895
|
+
parentSessionId: sessionId
|
|
896
|
+
};
|
|
897
|
+
ctx.betterSidebar.openTab({
|
|
898
|
+
type: SIDE_TAB_TYPE,
|
|
899
|
+
...title !== void 0 ? { title } : {},
|
|
900
|
+
meta
|
|
901
|
+
}, { sessionId });
|
|
902
|
+
const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
|
|
903
|
+
if (created === void 0) {
|
|
904
|
+
if (!nativeSidebarHost(ctx)) return false;
|
|
905
|
+
markSideChatOpening(sessionId);
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
ctx.betterSidebar.updateTab(created.id, {
|
|
909
|
+
...title !== void 0 ? { title } : {},
|
|
910
|
+
meta
|
|
911
|
+
});
|
|
912
|
+
return true;
|
|
913
|
+
} catch (error) {
|
|
914
|
+
console.warn("[dsh-sidenote] 重开侧边聊天失败:", error);
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/client/sidechat/lifecycle.ts
|
|
920
|
+
/**
|
|
921
|
+
* 首开编排:fork(全量历史快照)→ 登记 meta(先行,绝不丢登记)→
|
|
922
|
+
* 归档隐藏出会话列表(best-effort)→ 模型跟随主会话(best-effort)。
|
|
923
|
+
* fork 失败抛错(面板组件转错误态);后两步失败只告警不阻断。
|
|
924
|
+
* @returns fork 出的子会话 id。
|
|
925
|
+
*/
|
|
926
|
+
async function forkAndRegister(ctx, parentSessionId, tabId) {
|
|
927
|
+
let boundarySeq;
|
|
928
|
+
try {
|
|
929
|
+
const snap = chatSourceOf(ctx, ctx.sessions.binding(parentSessionId))?.getLegacy();
|
|
930
|
+
let max = -1;
|
|
931
|
+
for (const node of snap?.nodes ?? []) {
|
|
932
|
+
const seq = node.seq;
|
|
933
|
+
if (typeof seq === "number" && seq > max) max = seq;
|
|
934
|
+
}
|
|
935
|
+
if (max >= 0) boundarySeq = max;
|
|
936
|
+
} catch {}
|
|
937
|
+
const forked = await ctx.sessions.fork({ sessionId: parentSessionId });
|
|
938
|
+
const owner = rootContext(ctx);
|
|
939
|
+
updateTabMeta(ctx, tabId, (current) => ({
|
|
940
|
+
...current,
|
|
941
|
+
childId: forked,
|
|
942
|
+
parentSessionId,
|
|
943
|
+
...boundarySeq !== void 0 ? { boundarySeq } : {}
|
|
944
|
+
}));
|
|
945
|
+
try {
|
|
946
|
+
await owner.workspaces.archiveSession(forked);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
console.warn("[dsh-sidenote] 归档侧边会话失败(会话列表可能短暂可见):", error);
|
|
949
|
+
}
|
|
950
|
+
try {
|
|
951
|
+
const selection = await readModelSelection(owner, parentSessionId);
|
|
952
|
+
if (selection === void 0) console.warn("[dsh-sidenote] 同步主会话模型跳过:主机模型 API 不可用");
|
|
953
|
+
else if (!await writeModelSelection(owner, forked, selection)) console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型)");
|
|
954
|
+
} catch (error) {
|
|
955
|
+
console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型):", error);
|
|
956
|
+
}
|
|
957
|
+
return forked;
|
|
958
|
+
}
|
|
959
|
+
/** Tab meta 读-改-写(布局持久化寄存处);tab 已消失时 no-op。 */
|
|
960
|
+
function updateTabMeta(ctx, tabId, mutate) {
|
|
961
|
+
const current = parseSideChatMeta(readTab(ctx, tabId)?.meta);
|
|
962
|
+
ctx.betterSidebar.updateTab(tabId, { meta: mutate(current) });
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* 程序化入口(/side、bridge 划选提问)打开 Tab 时面板可能处于折叠态——
|
|
966
|
+
* 类型型 openTab 不自动展开(better-sidebar 只对 path/url 内容型打开展开)。
|
|
967
|
+
* 幂等展开(off-face:store.update 是 SidebarStore 公开 mutator 面;
|
|
968
|
+
* feature-check + 吞错,兜底失败不影响功能)。
|
|
969
|
+
*/
|
|
970
|
+
function ensurePanelOpen(store) {
|
|
971
|
+
const mutable = store;
|
|
972
|
+
try {
|
|
973
|
+
mutable?.update?.((state) => {
|
|
974
|
+
if (state.panelOpen === false) state.panelOpen = true;
|
|
975
|
+
});
|
|
976
|
+
} catch {}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* 打开非 staged 会话的事件窗口(off-face:open() 在 concrete Session 上是
|
|
980
|
+
* public 且幂等,但不在 SessionFace 契约上——契约面只有 staged 会话会被
|
|
981
|
+
* 运行时自动 open)。feature-check + 吞错,运行时若移除则降级为只发不收。
|
|
982
|
+
*/
|
|
983
|
+
function openSessionWindow(session) {
|
|
984
|
+
const openable = session;
|
|
985
|
+
if (typeof openable?.open !== "function") return;
|
|
986
|
+
openable.open().catch((error) => {
|
|
987
|
+
console.warn("[dsh-sidenote] 会话窗口打开失败:", error);
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Current model selection of a session, read from its `modelSelection`
|
|
992
|
+
* projection (dsh >= 0.1.2): the durable selection is projected state, so no RPC
|
|
993
|
+
* is needed and the read is safe for any session id. Snapshot shape authority:
|
|
994
|
+
* `@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts`
|
|
995
|
+
* (`ModelSelectionProjection = { lastUsed, next }`,两者均可为 null——
|
|
996
|
+
* `next` 缺省时按投影文档回落 `lastUsed`)。
|
|
997
|
+
*/
|
|
998
|
+
function projectedModelSelection(ctx, sessionId) {
|
|
999
|
+
try {
|
|
1000
|
+
const snapshot = (ctx.sessions.binding(sessionId)?.session)?.projections?.faceOf?.("modelSelection")?.getSnapshot?.();
|
|
1001
|
+
return snapshot?.next ?? snapshot?.lastUsed ?? void 0;
|
|
1002
|
+
} catch {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Remote session face carrying `selectModel`/`modelCatalog`(dsh 0.1.5 删除
|
|
1008
|
+
* `connection.api.sessions.*` client 面后的模型 RPC 面;事实上 0.1.2-rc.1
|
|
1009
|
+
* 起存在——authority: dsh-api-session-controller lib/typert.remote-client.d.ts)。
|
|
1010
|
+
* 探测走 `ctx.get`(cordis 的 get 不受 inject 门禁限制,本插件为兼容旧宿主
|
|
1011
|
+
* 不声明 inject 'remote.session';机制见 contracts.ts RemoteSessionModelFace)。
|
|
1012
|
+
* `ctx.get('remote')` 返回命名空间容器时下钻其 `.session` 成员。
|
|
1013
|
+
*/
|
|
1014
|
+
function remoteSessionFace(ctx) {
|
|
1015
|
+
for (const name of ["remote.session", "remote"]) {
|
|
1016
|
+
let candidate;
|
|
1017
|
+
try {
|
|
1018
|
+
candidate = ctx.get(name);
|
|
1019
|
+
} catch {
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
const face = candidate;
|
|
1023
|
+
if (typeof face?.selectModel === "function") return face;
|
|
1024
|
+
if (typeof face?.session?.selectModel === "function") return face.session;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* 读会话当前模型选择:投影面(0.1.2+,同步无 RPC)优先;旧
|
|
1029
|
+
* `connection.api.sessions.models` RPC(<= 0.1.2 的 client 面,0.1.5 已删)
|
|
1030
|
+
* 回退(双版本链,同 chatSourceOf 先例)。永不抛错。
|
|
1031
|
+
*/
|
|
1032
|
+
async function readModelSelection(ctx, sessionId) {
|
|
1033
|
+
const projected = projectedModelSelection(ctx, sessionId);
|
|
1034
|
+
if (projected !== void 0) return projected;
|
|
1035
|
+
try {
|
|
1036
|
+
const res = await ctx.connection.api.sessions.models({ sessionId });
|
|
1037
|
+
if (res.result.ok) return res.result.value.current;
|
|
1038
|
+
} catch {}
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* 写会话模型选择:`remote.session.selectModel`(新面)优先,旧
|
|
1042
|
+
* `connection.api.sessions.selectModel` 回退。面在而调用失败(模型不可路由等)
|
|
1043
|
+
* 属真实失败,不再落旧面重试。
|
|
1044
|
+
*/
|
|
1045
|
+
async function writeModelSelection(ctx, sessionId, selection) {
|
|
1046
|
+
const request = {
|
|
1047
|
+
sessionId,
|
|
1048
|
+
provider: selection.provider,
|
|
1049
|
+
model: selection.model,
|
|
1050
|
+
...selection.reasoningEffort !== void 0 ? { reasoningEffort: selection.reasoningEffort } : {}
|
|
1051
|
+
};
|
|
1052
|
+
const face = remoteSessionFace(ctx);
|
|
1053
|
+
if (face !== void 0) try {
|
|
1054
|
+
return (await face.selectModel(request)).ok;
|
|
1055
|
+
} catch {
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
return (await ctx.connection.api.sessions.selectModel(request)).result.ok;
|
|
1060
|
+
} catch {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
/** 读子会话当前模型名(composer 模型标签用);任何失败回退 null(默认文案)。 */
|
|
1065
|
+
async function readModelName(ctx, sessionId) {
|
|
1066
|
+
return (await readModelSelection(ctx, sessionId))?.model ?? null;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* 0.1.2 优先:`uiConversation` 服务 → binding(会话 binding) → target('chat')
|
|
1070
|
+
* → 快照 .legacy 切片(0.1.2 把内容面从 Session 快照顶层拆到此处——
|
|
1071
|
+
* 根因与证据见 reports/ux-review/W00-fork-replay-012.md);0.1.1 回退
|
|
1072
|
+
* Session 快照顶层(nodes 直接在)。均 miss 返回 undefined(面板维持
|
|
1073
|
+
* 既有降级,不崩)。
|
|
1074
|
+
*/
|
|
1075
|
+
function chatSourceOf(ctx, binding) {
|
|
1076
|
+
const session = binding?.session;
|
|
1077
|
+
if (session === void 0) return void 0;
|
|
1078
|
+
try {
|
|
1079
|
+
const target = ctx.get("uiConversation")?.binding?.(binding)?.target?.("chat");
|
|
1080
|
+
if (target !== void 0 && typeof target.getSnapshot === "function" && typeof target.subscribe === "function") return {
|
|
1081
|
+
subscribe: (fn) => target.subscribe(fn),
|
|
1082
|
+
getLegacy: () => {
|
|
1083
|
+
return target.getSnapshot()?.legacy ?? null;
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
} catch {}
|
|
1087
|
+
const snap = session.getSnapshot();
|
|
1088
|
+
if (snap !== null && Array.isArray(snap.nodes)) return {
|
|
1089
|
+
subscribe: (fn) => session.subscribe(fn),
|
|
1090
|
+
getLegacy: () => session.getSnapshot()
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* 拉会话模型目录:0.1.5 走 `remote.session.modelCatalog()`(目录是宿主级,
|
|
1095
|
+
* 菜单选中项所需的 current 由投影/旧面补上);旧面回退
|
|
1096
|
+
* `connection.api.sessions.models`。失败 null → 菜单保持只读标签态。
|
|
1097
|
+
*/
|
|
1098
|
+
async function listModels(ctx, sessionId) {
|
|
1099
|
+
const face = remoteSessionFace(ctx);
|
|
1100
|
+
if (typeof face?.modelCatalog === "function") try {
|
|
1101
|
+
const result = await face.modelCatalog();
|
|
1102
|
+
if (result.ok) {
|
|
1103
|
+
const catalog = result.value;
|
|
1104
|
+
return {
|
|
1105
|
+
current: await readModelSelection(ctx, sessionId) ?? catalog.default,
|
|
1106
|
+
routable: catalog.routableProviders.length > 0,
|
|
1107
|
+
groups: catalog.groups,
|
|
1108
|
+
...catalog.failures !== void 0 ? { failures: catalog.failures } : {}
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
} catch {}
|
|
1112
|
+
try {
|
|
1113
|
+
const res = await ctx.connection.api.sessions.models({ sessionId });
|
|
1114
|
+
return res.result.ok ? res.result.value : null;
|
|
1115
|
+
} catch {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/** 切换子会话模型;成功返回新模型展示名,失败 null(best-effort)。 */
|
|
1120
|
+
async function switchModel(ctx, sessionId, provider, model) {
|
|
1121
|
+
return await writeModelSelection(ctx, sessionId, {
|
|
1122
|
+
provider,
|
|
1123
|
+
model
|
|
1124
|
+
}) ? model : null;
|
|
1125
|
+
}
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/client/host/probes.ts
|
|
1128
|
+
const PROBES = [
|
|
1129
|
+
{
|
|
1130
|
+
name: "Session.open()(非 staged 会话开窗)",
|
|
1131
|
+
check: (ctx) => {
|
|
1132
|
+
return typeof ctx.sessions?.binding === "function";
|
|
1133
|
+
},
|
|
1134
|
+
fallback: "侧边聊天消息流降级为只发不收"
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
name: "conversation.input 输入机器",
|
|
1138
|
+
check: (ctx) => {
|
|
1139
|
+
return typeof (ctx.get?.("conversation"))?.input?.for === "function";
|
|
1140
|
+
},
|
|
1141
|
+
fallback: "侧边 composer 降级为本地草稿 + session.prompt"
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
name: "commandUi(斜杠命令注册)",
|
|
1145
|
+
check: (ctx) => {
|
|
1146
|
+
return typeof (ctx.get?.("commandUi"))?.register === "function";
|
|
1147
|
+
},
|
|
1148
|
+
fallback: "/side 与「/侧边」命令不可用(菜单/顶栏入口仍在)"
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
name: "ctx.modules.import(动态组件复用)",
|
|
1152
|
+
check: (ctx) => {
|
|
1153
|
+
return typeof ctx.modules?.import === "function";
|
|
1154
|
+
},
|
|
1155
|
+
fallback: "ProducedFiles 等动态复用件降级为自绘"
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
name: "sidebarRight.focus(native tab 程序化聚焦)",
|
|
1159
|
+
check: (ctx) => {
|
|
1160
|
+
if (!nativeSidebarHost(ctx)) return true;
|
|
1161
|
+
return typeof (ctx.get?.("sidebarRight"))?.focus === "function";
|
|
1162
|
+
},
|
|
1163
|
+
fallback: "聚焦既有侧边聊天 tab 不可用(activateTab 在 native 面是空操作)"
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
name: "会话模型面(remote.session / connection.api.sessions 双版本链)",
|
|
1167
|
+
check: (ctx) => {
|
|
1168
|
+
if (remoteSessionFace(ctx) !== void 0) return true;
|
|
1169
|
+
return typeof ctx.connection?.api?.sessions?.models === "function";
|
|
1170
|
+
},
|
|
1171
|
+
fallback: "fork 模型同步、模型标签与模型切换不可用(子会话用宿主默认模型)"
|
|
1172
|
+
}
|
|
1173
|
+
];
|
|
1174
|
+
/** 激活时跑一遍探测清单;缺失项逐项 warn-once。返回缺失项数(测试可断言)。 */
|
|
1175
|
+
function probeHost(ctx) {
|
|
1176
|
+
let missing = 0;
|
|
1177
|
+
for (const probe of PROBES) {
|
|
1178
|
+
let ok = false;
|
|
1179
|
+
try {
|
|
1180
|
+
ok = probe.check(ctx);
|
|
1181
|
+
} catch {
|
|
1182
|
+
ok = false;
|
|
1183
|
+
}
|
|
1184
|
+
if (!ok) {
|
|
1185
|
+
missing += 1;
|
|
1186
|
+
console.warn(`[dsh-sidenote] 宿主能力缺失:${probe.name} → ${probe.fallback}`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return missing;
|
|
1190
|
+
}
|
|
1191
|
+
//#endregion
|
|
1192
|
+
//#region src/client/reflow.ts
|
|
1193
|
+
/**
|
|
1194
|
+
* 回流通道的受控对象层(Delivery_02 Workitem_04 v2):侧边聊天的结论以
|
|
1195
|
+
* 「待回流对象」进入主会话——主 composer 上方的 chip(可预览/可移除),
|
|
1196
|
+
* 发送瞬间由 annotate/send.ts 拦截器与注释协议块一起序列化进消息。
|
|
1197
|
+
* 不经草稿文本流(用户裁定:直接进草稿「很脏」)。
|
|
1198
|
+
*
|
|
1199
|
+
* 气泡留痕:序列化形态是 XML 协议块(见 buildReflowBlock),随注释协议块
|
|
1200
|
+
* 一起作为消息连续前缀注入,bubble.ts 反解析后折叠为「含侧边回流上下文」标签。
|
|
1201
|
+
*
|
|
1202
|
+
* 持久化:localStorage 按会话键(dsh-sidenote:reflow:v1:<sessionId>),
|
|
1203
|
+
* 与注释 store 同款纪律(刷新不丢、容错 revive、空删键)。
|
|
1204
|
+
*/
|
|
1205
|
+
const STORAGE_PREFIX$1 = "dsh-sidenote:reflow:v1:";
|
|
1206
|
+
/**
|
|
1207
|
+
* 回流块序列化(XML 形态,与注释协议同族):
|
|
1208
|
+
*
|
|
1209
|
+
* <reflow source="侧边 2" reason="用户选择从侧边聊天带回主线">
|
|
1210
|
+
* <问>用户在侧边聊天里的提问</问>
|
|
1211
|
+
* <答>结论全文(不截断)</答>
|
|
1212
|
+
* </reflow>
|
|
1213
|
+
*
|
|
1214
|
+
* 问答成对:只有答没有问,模型理解打折;question 缺省时只有 <答>。
|
|
1215
|
+
* 来源与意图放在属性里,内容行保持纯净;模型对 XML 包裹的上下文理解最好
|
|
1216
|
+
* (Claude 官方推荐形态 / Codex additionalContext 同款思路)。
|
|
1217
|
+
*/
|
|
1218
|
+
function buildReflowBlock(item) {
|
|
1219
|
+
const parts = [];
|
|
1220
|
+
if (item.question !== void 0 && item.question.trim() !== "") parts.push(`<问>${item.question}</问>`);
|
|
1221
|
+
parts.push(`<答>${item.text}</答>`);
|
|
1222
|
+
return `<reflow source="${item.sideTitle}" reason="${t("reflowReason")}">\n${parts.join("\n")}\n</reflow>`;
|
|
1223
|
+
}
|
|
1224
|
+
function revive(value) {
|
|
1225
|
+
if (typeof value !== "object" || value === null) return null;
|
|
1226
|
+
const r = value;
|
|
1227
|
+
if (typeof r.id !== "number" || typeof r.sessionId !== "string" || r.sessionId === "") return null;
|
|
1228
|
+
if (typeof r.sideTitle !== "string" || typeof r.text !== "string") return null;
|
|
1229
|
+
return {
|
|
1230
|
+
id: r.id,
|
|
1231
|
+
sessionId: r.sessionId,
|
|
1232
|
+
sideTitle: r.sideTitle,
|
|
1233
|
+
text: r.text,
|
|
1234
|
+
...typeof r.question === "string" && r.question.trim() !== "" ? { question: r.question } : {},
|
|
1235
|
+
createdAt: typeof r.createdAt === "number" ? r.createdAt : 0
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function createReflowStore(now = () => Date.now(), storage) {
|
|
1239
|
+
let items = [];
|
|
1240
|
+
let nextId = 1;
|
|
1241
|
+
let version = 0;
|
|
1242
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1243
|
+
const store = storage === void 0 ? typeof localStorage !== "undefined" ? localStorage : null : storage;
|
|
1244
|
+
if (store != null) try {
|
|
1245
|
+
const keys = [];
|
|
1246
|
+
for (let i = 0; i < store.length; i += 1) {
|
|
1247
|
+
const key = store.key(i);
|
|
1248
|
+
if (typeof key === "string" && key.startsWith(STORAGE_PREFIX$1)) keys.push(key);
|
|
1249
|
+
}
|
|
1250
|
+
for (const key of keys) {
|
|
1251
|
+
const parsed = JSON.parse(store.getItem(key) ?? "null");
|
|
1252
|
+
if (!Array.isArray(parsed)) continue;
|
|
1253
|
+
for (const raw of parsed) {
|
|
1254
|
+
const item = revive(raw);
|
|
1255
|
+
if (item === null) continue;
|
|
1256
|
+
items.push(item);
|
|
1257
|
+
nextId = Math.max(nextId, item.id + 1);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
console.warn("[dsh-sidenote] 回流持久化读取失败(按空起步):", error);
|
|
1262
|
+
}
|
|
1263
|
+
const persist = (sessionId) => {
|
|
1264
|
+
if (store == null) return;
|
|
1265
|
+
try {
|
|
1266
|
+
const mine = items.filter((i) => i.sessionId === sessionId);
|
|
1267
|
+
const key = STORAGE_PREFIX$1 + sessionId;
|
|
1268
|
+
if (mine.length === 0) store.removeItem(key);
|
|
1269
|
+
else store.setItem(key, JSON.stringify(mine));
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
console.warn("[dsh-sidenote] 回流持久化写入失败:", error);
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
const notify = () => {
|
|
1275
|
+
version += 1;
|
|
1276
|
+
for (const fn of [...listeners]) fn();
|
|
1277
|
+
};
|
|
1278
|
+
return {
|
|
1279
|
+
getSnapshot: () => version,
|
|
1280
|
+
subscribe(fn) {
|
|
1281
|
+
listeners.add(fn);
|
|
1282
|
+
return () => {
|
|
1283
|
+
listeners.delete(fn);
|
|
1284
|
+
};
|
|
1285
|
+
},
|
|
1286
|
+
add(sessionId, sideTitle, text, question) {
|
|
1287
|
+
const item = {
|
|
1288
|
+
id: nextId,
|
|
1289
|
+
sessionId,
|
|
1290
|
+
sideTitle,
|
|
1291
|
+
text,
|
|
1292
|
+
...typeof question === "string" && question.trim() !== "" ? { question } : {},
|
|
1293
|
+
createdAt: now()
|
|
1294
|
+
};
|
|
1295
|
+
nextId += 1;
|
|
1296
|
+
items = [...items, item];
|
|
1297
|
+
persist(sessionId);
|
|
1298
|
+
notify();
|
|
1299
|
+
return item;
|
|
1300
|
+
},
|
|
1301
|
+
remove(id) {
|
|
1302
|
+
const target = items.find((i) => i.id === id);
|
|
1303
|
+
if (target === void 0) return;
|
|
1304
|
+
items = items.filter((i) => i.id !== id);
|
|
1305
|
+
persist(target.sessionId);
|
|
1306
|
+
notify();
|
|
1307
|
+
},
|
|
1308
|
+
clearSession(sessionId) {
|
|
1309
|
+
if (!items.some((i) => i.sessionId === sessionId)) return;
|
|
1310
|
+
items = items.filter((i) => i.sessionId !== sessionId);
|
|
1311
|
+
persist(sessionId);
|
|
1312
|
+
notify();
|
|
1313
|
+
},
|
|
1314
|
+
list: (sessionId) => items.filter((i) => i.sessionId === sessionId)
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
//#endregion
|
|
1318
|
+
//#region src/client/bridge.ts
|
|
1319
|
+
const sideChatBridge = { current: null };
|
|
1320
|
+
//#endregion
|
|
1321
|
+
//#region src/client/chat/cards.ts
|
|
1322
|
+
/** ContentBlock[] → 纯文本(transcript.ts 同款的极简版,避免互依赖)。 */
|
|
1323
|
+
function blocksText(content) {
|
|
1324
|
+
if (!Array.isArray(content)) return void 0;
|
|
1325
|
+
const parts = [];
|
|
1326
|
+
for (const block of content) {
|
|
1327
|
+
const b = block;
|
|
1328
|
+
if (b?.type === "text" && typeof b.text === "string") parts.push(b.text);
|
|
1329
|
+
}
|
|
1330
|
+
return parts.length === 0 ? void 0 : parts.join("\n");
|
|
1331
|
+
}
|
|
1332
|
+
/** 未知 card 值的 warn-once 登记(T3 纪律:降级不静默)。 */
|
|
1333
|
+
const warnedCards = /* @__PURE__ */ new Set();
|
|
1334
|
+
function warnUnknownCard(value) {
|
|
1335
|
+
if (warnedCards.has(value)) return;
|
|
1336
|
+
warnedCards.add(value);
|
|
1337
|
+
console.warn(`[dsh-sidenote] 未知工具卡种类 "${value}"——降级为 generic 卡(宿主可能扩展了 card union)`);
|
|
1338
|
+
}
|
|
1339
|
+
/** 相对 cwd 按会话工作区折成绝对路径(UI bridge 责任——presentation 注释明文)。 */
|
|
1340
|
+
function resolveCwd(cwd, cwdBase) {
|
|
1341
|
+
if (cwd === void 0) return cwdBase;
|
|
1342
|
+
if (cwd.startsWith("/") || /^[A-Za-z]:[\\/]/.test(cwd)) return cwd;
|
|
1343
|
+
if (cwdBase === void 0) return cwd;
|
|
1344
|
+
return `${cwdBase.replace(/\/+$/, "")}/${cwd}`;
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* 展示态路径缩短:>3 段的绝对路径只保末 3 段(≈仓库相对形态,与主区
|
|
1348
|
+
* 「Read · dsh-sidenote/scripts/x.sh」同款)。窄栏标题位只够放语义尾部。
|
|
1349
|
+
*/
|
|
1350
|
+
function shortPath(p) {
|
|
1351
|
+
if (!p.startsWith("/")) return p;
|
|
1352
|
+
const segs = p.split("/").filter((s) => s !== "");
|
|
1353
|
+
return segs.length > 3 ? segs.slice(-3).join("/") : p;
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* 标题整形:「Tool /abs/path」→「Tool · 末3段」(主区同名同款);
|
|
1357
|
+
* 裸绝对路径标题 → 末 3 段;不含路径的标题原样。
|
|
1358
|
+
*/
|
|
1359
|
+
function displayTitle(title) {
|
|
1360
|
+
const m = /^([A-Za-z][\w-]*) (\/.+)$/.exec(title);
|
|
1361
|
+
const name = m?.[1];
|
|
1362
|
+
const path = m?.[2];
|
|
1363
|
+
if (name !== void 0 && path !== void 0) return `${name} · ${shortPath(path)}`;
|
|
1364
|
+
return title.startsWith("/") ? shortPath(title) : title;
|
|
1365
|
+
}
|
|
1366
|
+
/** todo_write 入参形状校验(字段不齐即放弃 todo 卡)。两种数据源形状:
|
|
1367
|
+
* 0.1.1 wire:callView.rawInput = todos 数组本身(dsh-tool-todo presentCall 实证);
|
|
1368
|
+
* 0.1.2 argsRaw:{ todos: [...] }。 */
|
|
882
1369
|
function todoItemsOf(raw) {
|
|
883
1370
|
const list = Array.isArray(raw) ? raw : typeof raw === "object" && raw !== null ? raw.todos : void 0;
|
|
884
1371
|
if (!Array.isArray(list) || list.length === 0) return void 0;
|
|
@@ -1694,92 +2181,92 @@ window.__ModuleLoader__.load({
|
|
|
1694
2181
|
document.head.appendChild(tag);
|
|
1695
2182
|
}
|
|
1696
2183
|
var sidechat_module_css_default = {
|
|
2184
|
+
"pendingBar": "_5KEEsG_pendingBar",
|
|
1697
2185
|
"composerCard": "_5KEEsG_composerCard",
|
|
1698
|
-
"
|
|
1699
|
-
"
|
|
1700
|
-
"transcript": "_5KEEsG_transcript",
|
|
1701
|
-
"rowPreview": "_5KEEsG_rowPreview",
|
|
1702
|
-
"toolGenericBody": "_5KEEsG_toolGenericBody",
|
|
1703
|
-
"densityRow": "_5KEEsG_densityRow",
|
|
1704
|
-
"pendingReason": "_5KEEsG_pendingReason",
|
|
1705
|
-
"toolError": "_5KEEsG_toolError",
|
|
1706
|
-
"pendingOptionDesc": "_5KEEsG_pendingOptionDesc",
|
|
2186
|
+
"attachRail": "_5KEEsG_attachRail",
|
|
2187
|
+
"reasoningBody": "_5KEEsG_reasoningBody",
|
|
1707
2188
|
"empty": "_5KEEsG_empty",
|
|
1708
|
-
"
|
|
1709
|
-
"
|
|
2189
|
+
"dotRunning": "_5KEEsG_dotRunning",
|
|
2190
|
+
"toolBody": "_5KEEsG_toolBody",
|
|
2191
|
+
"attachImg": "_5KEEsG_attachImg",
|
|
2192
|
+
"slashItem": "_5KEEsG_slashItem",
|
|
2193
|
+
"stateDetail": "_5KEEsG_stateDetail",
|
|
2194
|
+
"sentChipRow": "_5KEEsG_sentChipRow",
|
|
1710
2195
|
"userBubble": "_5KEEsG_userBubble",
|
|
1711
|
-
"
|
|
1712
|
-
"pendingOptionActive": "_5KEEsG_pendingOptionActive",
|
|
1713
|
-
"pendingDesc": "_5KEEsG_pendingDesc",
|
|
1714
|
-
"pendingActions": "_5KEEsG_pendingActions",
|
|
1715
|
-
"todoDot": "_5KEEsG_todoDot",
|
|
1716
|
-
"todoText": "_5KEEsG_todoText",
|
|
1717
|
-
"emptyIcon": "_5KEEsG_emptyIcon",
|
|
2196
|
+
"rowPreview": "_5KEEsG_rowPreview",
|
|
1718
2197
|
"reflowButton": "_5KEEsG_reflowButton",
|
|
1719
|
-
"
|
|
1720
|
-
"
|
|
1721
|
-
"
|
|
1722
|
-
"
|
|
1723
|
-
"
|
|
1724
|
-
"
|
|
1725
|
-
"
|
|
1726
|
-
"sendButton": "_5KEEsG_sendButton",
|
|
1727
|
-
"slashItemActive": "_5KEEsG_slashItemActive",
|
|
1728
|
-
"stateDetail": "_5KEEsG_stateDetail",
|
|
1729
|
-
"emptyTitle": "_5KEEsG_emptyTitle",
|
|
1730
|
-
"pendingCustom": "_5KEEsG_pendingCustom",
|
|
1731
|
-
"dotRunning": "_5KEEsG_dotRunning",
|
|
1732
|
-
"densityToggle": "_5KEEsG_densityToggle",
|
|
2198
|
+
"toolLocations": "_5KEEsG_toolLocations",
|
|
2199
|
+
"toolGenericBody": "_5KEEsG_toolGenericBody",
|
|
2200
|
+
"jumpBottom": "_5KEEsG_jumpBottom",
|
|
2201
|
+
"streamingHint": "_5KEEsG_streamingHint",
|
|
2202
|
+
"pendingTitle": "_5KEEsG_pendingTitle",
|
|
2203
|
+
"inheritedBody": "_5KEEsG_inheritedBody",
|
|
2204
|
+
"pendingQHeader": "_5KEEsG_pendingQHeader",
|
|
1733
2205
|
"pendingOption": "_5KEEsG_pendingOption",
|
|
1734
|
-
"
|
|
1735
|
-
"
|
|
2206
|
+
"pendingOptionActive": "_5KEEsG_pendingOptionActive",
|
|
2207
|
+
"rowActions": "_5KEEsG_rowActions",
|
|
2208
|
+
"stateScreen": "_5KEEsG_stateScreen",
|
|
2209
|
+
"pendingActions": "_5KEEsG_pendingActions",
|
|
2210
|
+
"pendingBtnGhost": "_5KEEsG_pendingBtnGhost",
|
|
2211
|
+
"actionGroup": "_5KEEsG_actionGroup",
|
|
1736
2212
|
"toolHead": "_5KEEsG_toolHead",
|
|
2213
|
+
"toolBodyText": "_5KEEsG_toolBodyText",
|
|
2214
|
+
"sidechat-pulse": "_5KEEsG_sidechat-pulse",
|
|
2215
|
+
"assistantBody": "_5KEEsG_assistantBody",
|
|
2216
|
+
"todoDot": "_5KEEsG_todoDot",
|
|
2217
|
+
"todoBody": "_5KEEsG_todoBody",
|
|
2218
|
+
"toolError": "_5KEEsG_toolError",
|
|
1737
2219
|
"reasoning": "_5KEEsG_reasoning",
|
|
1738
|
-
"
|
|
2220
|
+
"attachThumb": "_5KEEsG_attachThumb",
|
|
2221
|
+
"attachButton": "_5KEEsG_attachButton",
|
|
2222
|
+
"densityToggle": "_5KEEsG_densityToggle",
|
|
2223
|
+
"composerFoot": "_5KEEsG_composerFoot",
|
|
2224
|
+
"stopButton": "_5KEEsG_stopButton",
|
|
2225
|
+
"slashItemActive": "_5KEEsG_slashItemActive",
|
|
2226
|
+
"slashMenu": "_5KEEsG_slashMenu",
|
|
1739
2227
|
"writeNotice": "_5KEEsG_writeNotice",
|
|
1740
2228
|
"input": "_5KEEsG_input",
|
|
1741
|
-
"
|
|
1742
|
-
"todoItem": "_5KEEsG_todoItem",
|
|
1743
|
-
"composer": "_5KEEsG_composer",
|
|
1744
|
-
"slashItem": "_5KEEsG_slashItem",
|
|
1745
|
-
"flowRow": "_5KEEsG_flowRow",
|
|
1746
|
-
"pendingHead": "_5KEEsG_pendingHead",
|
|
2229
|
+
"densityRow": "_5KEEsG_densityRow",
|
|
1747
2230
|
"composerChips": "_5KEEsG_composerChips",
|
|
2231
|
+
"sentChip": "_5KEEsG_sentChip",
|
|
2232
|
+
"pendingQuestion": "_5KEEsG_pendingQuestion",
|
|
2233
|
+
"todoText": "_5KEEsG_todoText",
|
|
2234
|
+
"pendingReason": "_5KEEsG_pendingReason",
|
|
2235
|
+
"pendingBtnPrimary": "_5KEEsG_pendingBtnPrimary",
|
|
2236
|
+
"dotIdle": "_5KEEsG_dotIdle",
|
|
2237
|
+
"assistantRow": "_5KEEsG_assistantRow",
|
|
2238
|
+
"flowRow": "_5KEEsG_flowRow",
|
|
2239
|
+
"userRow": "_5KEEsG_userRow",
|
|
2240
|
+
"toolCard": "_5KEEsG_toolCard",
|
|
1748
2241
|
"errorRow": "_5KEEsG_errorRow",
|
|
2242
|
+
"noticeRow": "_5KEEsG_noticeRow",
|
|
1749
2243
|
"slashItemDetail": "_5KEEsG_slashItemDetail",
|
|
2244
|
+
"transcript": "_5KEEsG_transcript",
|
|
1750
2245
|
"toolRunning": "_5KEEsG_toolRunning",
|
|
1751
|
-
"
|
|
1752
|
-
"
|
|
1753
|
-
"
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
2246
|
+
"emptyIcon": "_5KEEsG_emptyIcon",
|
|
2247
|
+
"pendingCustom": "_5KEEsG_pendingCustom",
|
|
2248
|
+
"headerSide": "_5KEEsG_headerSide",
|
|
2249
|
+
"parentStrip": "_5KEEsG_parentStrip",
|
|
2250
|
+
"modelSelect": "_5KEEsG_modelSelect",
|
|
2251
|
+
"pendingHead": "_5KEEsG_pendingHead",
|
|
1756
2252
|
"root": "_5KEEsG_root",
|
|
1757
|
-
"
|
|
2253
|
+
"flowRowError": "_5KEEsG_flowRowError",
|
|
2254
|
+
"writeNoticeClose": "_5KEEsG_writeNoticeClose",
|
|
1758
2255
|
"slashGroupTitle": "_5KEEsG_slashGroupTitle",
|
|
1759
|
-
"
|
|
1760
|
-
"
|
|
1761
|
-
"
|
|
1762
|
-
"
|
|
1763
|
-
"
|
|
1764
|
-
"
|
|
1765
|
-
"toolBodyText": "_5KEEsG_toolBodyText",
|
|
1766
|
-
"pendingCard": "_5KEEsG_pendingCard",
|
|
1767
|
-
"parentStrip": "_5KEEsG_parentStrip",
|
|
1768
|
-
"toolLocations": "_5KEEsG_toolLocations",
|
|
1769
|
-
"actionGroup": "_5KEEsG_actionGroup",
|
|
1770
|
-
"headerSide": "_5KEEsG_headerSide",
|
|
1771
|
-
"attachImg": "_5KEEsG_attachImg",
|
|
2256
|
+
"sendButton": "_5KEEsG_sendButton",
|
|
2257
|
+
"slashItemName": "_5KEEsG_slashItemName",
|
|
2258
|
+
"body": "_5KEEsG_body",
|
|
2259
|
+
"emptyText": "_5KEEsG_emptyText",
|
|
2260
|
+
"composer": "_5KEEsG_composer",
|
|
2261
|
+
"emptyTitle": "_5KEEsG_emptyTitle",
|
|
1772
2262
|
"attachRemove": "_5KEEsG_attachRemove",
|
|
1773
|
-
"
|
|
1774
|
-
"
|
|
1775
|
-
"
|
|
1776
|
-
"
|
|
1777
|
-
"
|
|
1778
|
-
"
|
|
1779
|
-
"
|
|
1780
|
-
"modelSelect": "_5KEEsG_modelSelect",
|
|
1781
|
-
"attachThumb": "_5KEEsG_attachThumb",
|
|
1782
|
-
"pendingQText": "_5KEEsG_pendingQText"
|
|
2263
|
+
"dotPending": "_5KEEsG_dotPending",
|
|
2264
|
+
"todoItem": "_5KEEsG_todoItem",
|
|
2265
|
+
"pendingQText": "_5KEEsG_pendingQText",
|
|
2266
|
+
"pendingOptionDesc": "_5KEEsG_pendingOptionDesc",
|
|
2267
|
+
"pendingDesc": "_5KEEsG_pendingDesc",
|
|
2268
|
+
"modelLabel": "_5KEEsG_modelLabel",
|
|
2269
|
+
"pendingCard": "_5KEEsG_pendingCard"
|
|
1783
2270
|
};
|
|
1784
2271
|
//#endregion
|
|
1785
2272
|
//#region src/client/chat/ToolCard.tsx
|
|
@@ -2233,291 +2720,96 @@ window.__ModuleLoader__.load({
|
|
|
2233
2720
|
if (message.card !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, {
|
|
2234
2721
|
model: message.card,
|
|
2235
2722
|
rowKey: message.key,
|
|
2236
|
-
fold,
|
|
2237
|
-
...message.streaming === true ? { streaming: true } : {},
|
|
2238
|
-
...message.isError === true ? { error: true } : {}
|
|
2239
|
-
});
|
|
2240
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2241
|
-
className: sidechat_module_css_default.toolCard,
|
|
2242
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2243
|
-
className: sidechat_module_css_default.toolHead,
|
|
2244
|
-
children: [
|
|
2245
|
-
t("toolLabel"),
|
|
2246
|
-
" · ",
|
|
2247
|
-
message.toolName,
|
|
2248
|
-
message.isError === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2249
|
-
className: sidechat_module_css_default.toolError,
|
|
2250
|
-
children: t("failed")
|
|
2251
|
-
}),
|
|
2252
|
-
message.streaming === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2253
|
-
className: sidechat_module_css_default.toolRunning,
|
|
2254
|
-
children: t("running")
|
|
2255
|
-
})
|
|
2256
|
-
]
|
|
2257
|
-
}), message.text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2258
|
-
className: sidechat_module_css_default.toolBody,
|
|
2259
|
-
children: message.text
|
|
2260
|
-
})]
|
|
2261
|
-
});
|
|
2262
|
-
case "error": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2263
|
-
className: sidechat_module_css_default.errorRow,
|
|
2264
|
-
children: message.text
|
|
2265
|
-
});
|
|
2266
|
-
case "notice": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2267
|
-
className: sidechat_module_css_default.noticeRow,
|
|
2268
|
-
children: message.text
|
|
2269
|
-
});
|
|
2270
|
-
}
|
|
2271
|
-
}, rowPropsEqual);
|
|
2272
|
-
/** memo 比较器:transcriptOf 每次快照重建消息对象(引用必变),按字段值比。 */
|
|
2273
|
-
function rowPropsEqual(prev, next) {
|
|
2274
|
-
if (prev.fold !== next.fold || prev.reflow !== next.reflow) return false;
|
|
2275
|
-
if (prev.parentSessionId !== next.parentSessionId || prev.sideTitle !== next.sideTitle) return false;
|
|
2276
|
-
if (prev.question !== next.question) return false;
|
|
2277
|
-
const a = prev.message;
|
|
2278
|
-
const b = next.message;
|
|
2279
|
-
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;
|
|
2280
|
-
}
|
|
2281
|
-
//#endregion
|
|
2282
|
-
//#region src/client/sidechat/open.ts
|
|
2283
|
-
/** 从最新快照读一个 Tab(meta 合并写入前的读取面;布局即注册表)。 */
|
|
2284
|
-
function readTab(ctx, tabId) {
|
|
2285
|
-
return collectTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => tab.id === tabId);
|
|
2286
|
-
}
|
|
2287
|
-
/**
|
|
2288
|
-
* 为 sessionId 新建或聚焦一个侧边聊天 Tab;draftText 给出时写入其草稿。
|
|
2289
|
-
* @returns false = 未能打开(调用方保持自己的流程)。
|
|
2290
|
-
*/
|
|
2291
|
-
function openOrFocusSideChat(ctx, sessionId, draftText) {
|
|
2292
|
-
try {
|
|
2293
|
-
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
2294
|
-
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
|
|
2295
|
-
const existing = collectSideTabs(snapshot.state);
|
|
2296
|
-
if (existing.length > 0) {
|
|
2297
|
-
const target = existing[existing.length - 1];
|
|
2298
|
-
if (draftText !== void 0 && draftText !== "") {
|
|
2299
|
-
const meta = parseSideChatMeta(target.meta);
|
|
2300
|
-
const input = meta.childId === void 0 ? null : resolveSessionInput(ctx, meta.childId);
|
|
2301
|
-
if (input !== null) input.setDraft(appendDraftText(readInputDraft(input), draftText));
|
|
2302
|
-
else ctx.betterSidebar.updateTab(target.id, { meta: {
|
|
2303
|
-
...meta,
|
|
2304
|
-
pendingDraft: appendDraftText(meta.pendingDraft ?? "", draftText)
|
|
2305
|
-
} });
|
|
2306
|
-
}
|
|
2307
|
-
ctx.betterSidebar.activateTab(target.id, { sessionId });
|
|
2308
|
-
return true;
|
|
2309
|
-
}
|
|
2310
|
-
return createSideChat(ctx, sessionId, draftText);
|
|
2311
|
-
} catch (error) {
|
|
2312
|
-
console.warn("[dsh-sidenote] 打开侧边聊天失败:", error);
|
|
2313
|
-
return false;
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2316
|
-
/**
|
|
2317
|
-
* 无条件新建一个侧边聊天 Tab(/side「新建侧边聊天」选项的语义——既有实例
|
|
2318
|
-
* 在弹层里另有聚焦项,「新建」必须真的新建,否则标签与行为不符)。
|
|
2319
|
-
* @returns false = 未能创建(调用方保持自己的流程)。
|
|
2320
|
-
*/
|
|
2321
|
-
function createSideChat(ctx, sessionId, draftText) {
|
|
2322
|
-
try {
|
|
2323
|
-
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
2324
|
-
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
|
|
2325
|
-
const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
|
|
2326
|
-
ctx.betterSidebar.openTab({ type: SIDE_TAB_TYPE }, { sessionId });
|
|
2327
|
-
const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
|
|
2328
|
-
if (created === void 0) return false;
|
|
2329
|
-
if (draftText !== void 0 && draftText !== "") ctx.betterSidebar.updateTab(created.id, { meta: { pendingDraft: draftText } });
|
|
2330
|
-
return true;
|
|
2331
|
-
} catch (error) {
|
|
2332
|
-
console.warn("[dsh-sidenote] 新建侧边聊天失败:", error);
|
|
2333
|
-
return false;
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
/**
|
|
2337
|
-
* 桥接/命令的目标预览:askInSideChat 此时若执行会落在哪个 Tab 的标题
|
|
2338
|
-
* (既有并存 = 最后一个;无 = 即将新建的首个标题)。用于编辑器上标明
|
|
2339
|
-
* 「发送至:侧边 N」。会话不在屏上/状态缺失时返回 undefined。
|
|
2340
|
-
*/
|
|
2341
|
-
function sideChatTargetTitle(ctx, sessionId) {
|
|
2342
|
-
try {
|
|
2343
|
-
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
2344
|
-
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return void 0;
|
|
2345
|
-
const existing = collectSideTabs(snapshot.state);
|
|
2346
|
-
if (existing.length > 0) return existing[existing.length - 1].title;
|
|
2347
|
-
return sideTabTitle([]);
|
|
2348
|
-
} catch {
|
|
2349
|
-
return;
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
/**
|
|
2353
|
-
* 重开最近关闭的侧边聊天(D3 后悔药):开 Tab + meta 预置 childId/
|
|
2354
|
-
* parentSessionId——面板挂载后走绑定恢复路径(不重新 fork)。
|
|
2355
|
-
* 会话本体必须仍 archived 在列(若被清理,面板落「会话已不存在」态——
|
|
2356
|
-
* 有现成错误态兜着)。
|
|
2357
|
-
*/
|
|
2358
|
-
function reopenSideChat(ctx, sessionId, childId, title) {
|
|
2359
|
-
try {
|
|
2360
|
-
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
2361
|
-
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
|
|
2362
|
-
const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
|
|
2363
|
-
ctx.betterSidebar.openTab({ type: SIDE_TAB_TYPE }, { sessionId });
|
|
2364
|
-
const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
|
|
2365
|
-
if (created === void 0) return false;
|
|
2366
|
-
ctx.betterSidebar.updateTab(created.id, {
|
|
2367
|
-
...title !== void 0 ? { title } : {},
|
|
2368
|
-
meta: {
|
|
2369
|
-
childId,
|
|
2370
|
-
parentSessionId: sessionId
|
|
2371
|
-
}
|
|
2372
|
-
});
|
|
2373
|
-
return true;
|
|
2374
|
-
} catch (error) {
|
|
2375
|
-
console.warn("[dsh-sidenote] 重开侧边聊天失败:", error);
|
|
2376
|
-
return false;
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
//#endregion
|
|
2380
|
-
//#region src/client/sidechat/lifecycle.ts
|
|
2381
|
-
/**
|
|
2382
|
-
* 首开编排:fork(全量历史快照)→ 登记 meta(先行,绝不丢登记)→
|
|
2383
|
-
* 归档隐藏出会话列表(best-effort)→ 模型跟随主会话(best-effort)。
|
|
2384
|
-
* fork 失败抛错(面板组件转错误态);后两步失败只告警不阻断。
|
|
2385
|
-
* @returns fork 出的子会话 id。
|
|
2386
|
-
*/
|
|
2387
|
-
async function forkAndRegister(ctx, parentSessionId, tabId) {
|
|
2388
|
-
let boundarySeq;
|
|
2389
|
-
try {
|
|
2390
|
-
const snap = chatSourceOf(ctx, ctx.sessions.binding(parentSessionId))?.getLegacy();
|
|
2391
|
-
let max = -1;
|
|
2392
|
-
for (const node of snap?.nodes ?? []) {
|
|
2393
|
-
const seq = node.seq;
|
|
2394
|
-
if (typeof seq === "number" && seq > max) max = seq;
|
|
2395
|
-
}
|
|
2396
|
-
if (max >= 0) boundarySeq = max;
|
|
2397
|
-
} catch {}
|
|
2398
|
-
const forked = await ctx.sessions.fork({ sessionId: parentSessionId });
|
|
2399
|
-
updateTabMeta(ctx, tabId, (current) => ({
|
|
2400
|
-
...current,
|
|
2401
|
-
childId: forked,
|
|
2402
|
-
parentSessionId,
|
|
2403
|
-
...boundarySeq !== void 0 ? { boundarySeq } : {}
|
|
2404
|
-
}));
|
|
2405
|
-
try {
|
|
2406
|
-
await ctx.workspaces.archiveSession(forked);
|
|
2407
|
-
} catch (error) {
|
|
2408
|
-
console.warn("[dsh-sidenote] 归档侧边会话失败(会话列表可能短暂可见):", error);
|
|
2409
|
-
}
|
|
2410
|
-
try {
|
|
2411
|
-
const parentModels = await ctx.connection.api.sessions.models({ sessionId: parentSessionId });
|
|
2412
|
-
if (parentModels.result.ok) {
|
|
2413
|
-
const current = parentModels.result.value.current;
|
|
2414
|
-
await ctx.connection.api.sessions.selectModel({
|
|
2415
|
-
sessionId: forked,
|
|
2416
|
-
provider: current.provider,
|
|
2417
|
-
model: current.model,
|
|
2418
|
-
...current.reasoningEffort !== void 0 ? { reasoningEffort: current.reasoningEffort } : {}
|
|
2723
|
+
fold,
|
|
2724
|
+
...message.streaming === true ? { streaming: true } : {},
|
|
2725
|
+
...message.isError === true ? { error: true } : {}
|
|
2419
2726
|
});
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2727
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2728
|
+
className: sidechat_module_css_default.toolCard,
|
|
2729
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2730
|
+
className: sidechat_module_css_default.toolHead,
|
|
2731
|
+
children: [
|
|
2732
|
+
t("toolLabel"),
|
|
2733
|
+
" · ",
|
|
2734
|
+
message.toolName,
|
|
2735
|
+
message.isError === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2736
|
+
className: sidechat_module_css_default.toolError,
|
|
2737
|
+
children: t("failed")
|
|
2738
|
+
}),
|
|
2739
|
+
message.streaming === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2740
|
+
className: sidechat_module_css_default.toolRunning,
|
|
2741
|
+
children: t("running")
|
|
2742
|
+
})
|
|
2743
|
+
]
|
|
2744
|
+
}), message.text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2745
|
+
className: sidechat_module_css_default.toolBody,
|
|
2746
|
+
children: message.text
|
|
2747
|
+
})]
|
|
2748
|
+
});
|
|
2749
|
+
case "error": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2750
|
+
className: sidechat_module_css_default.errorRow,
|
|
2751
|
+
children: message.text
|
|
2442
2752
|
});
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
*/
|
|
2450
|
-
function
|
|
2451
|
-
|
|
2452
|
-
if (
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2753
|
+
case "notice": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2754
|
+
className: sidechat_module_css_default.noticeRow,
|
|
2755
|
+
children: message.text
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
}, rowPropsEqual);
|
|
2759
|
+
/** memo 比较器:transcriptOf 每次快照重建消息对象(引用必变),按字段值比。 */
|
|
2760
|
+
function rowPropsEqual(prev, next) {
|
|
2761
|
+
if (prev.fold !== next.fold || prev.reflow !== next.reflow) return false;
|
|
2762
|
+
if (prev.parentSessionId !== next.parentSessionId || prev.sideTitle !== next.sideTitle) return false;
|
|
2763
|
+
if (prev.question !== next.question) return false;
|
|
2764
|
+
const a = prev.message;
|
|
2765
|
+
const b = next.message;
|
|
2766
|
+
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;
|
|
2456
2767
|
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2768
|
+
//#endregion
|
|
2769
|
+
//#region src/client/sidechat/recentClosed.ts
|
|
2770
|
+
const KEY_PREFIX = "dsh-sidenote:closed-side:v1:";
|
|
2771
|
+
const CAP = 5;
|
|
2772
|
+
function readAll(storageKey) {
|
|
2459
2773
|
try {
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2774
|
+
const raw = localStorage.getItem(storageKey);
|
|
2775
|
+
if (raw === null) return [];
|
|
2776
|
+
const parsed = JSON.parse(raw);
|
|
2777
|
+
if (!Array.isArray(parsed)) return [];
|
|
2778
|
+
return parsed.filter((e) => typeof e === "object" && e !== null && typeof e.childId === "string" && typeof e.parentSessionId === "string" && typeof e.title === "string");
|
|
2462
2779
|
} catch {
|
|
2463
|
-
return
|
|
2780
|
+
return [];
|
|
2464
2781
|
}
|
|
2465
2782
|
}
|
|
2466
|
-
|
|
2467
|
-
* 0.1.2 优先:`uiConversation` 服务 → binding(会话 binding) → target('chat')
|
|
2468
|
-
* → 快照 .legacy 切片(0.1.2 把内容面从 Session 快照顶层拆到此处——
|
|
2469
|
-
* 根因与证据见 reports/ux-review/W00-fork-replay-012.md);0.1.1 回退
|
|
2470
|
-
* Session 快照顶层(nodes 直接在)。均 miss 返回 undefined(面板维持
|
|
2471
|
-
* 既有降级,不崩)。
|
|
2472
|
-
*/
|
|
2473
|
-
function chatSourceOf(ctx, binding) {
|
|
2474
|
-
const session = binding?.session;
|
|
2475
|
-
if (session === void 0) return void 0;
|
|
2783
|
+
function recordClosedSideChat(parentSessionId, entry) {
|
|
2476
2784
|
try {
|
|
2477
|
-
const
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
getLegacy: () => {
|
|
2481
|
-
return target.getSnapshot()?.legacy ?? null;
|
|
2482
|
-
}
|
|
2483
|
-
};
|
|
2785
|
+
const key = KEY_PREFIX + parentSessionId;
|
|
2786
|
+
const list = [entry, ...readAll(key).filter((e) => e.childId !== entry.childId)].slice(0, CAP);
|
|
2787
|
+
localStorage.setItem(key, JSON.stringify(list));
|
|
2484
2788
|
} catch {}
|
|
2485
|
-
const snap = session.getSnapshot();
|
|
2486
|
-
if (snap !== null && Array.isArray(snap.nodes)) return {
|
|
2487
|
-
subscribe: (fn) => session.subscribe(fn),
|
|
2488
|
-
getLegacy: () => session.getSnapshot()
|
|
2489
|
-
};
|
|
2490
2789
|
}
|
|
2491
|
-
/**
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
return res.result.ok ? res.result.value : null;
|
|
2496
|
-
} catch {
|
|
2497
|
-
return null;
|
|
2498
|
-
}
|
|
2790
|
+
/** 读某主会话的最近关闭列表(新者在前)。 */
|
|
2791
|
+
function listClosedSideChats(parentSessionId) {
|
|
2792
|
+
if (typeof localStorage === "undefined") return [];
|
|
2793
|
+
return readAll(KEY_PREFIX + parentSessionId);
|
|
2499
2794
|
}
|
|
2500
|
-
/**
|
|
2501
|
-
|
|
2795
|
+
/** 重开消费:从列表移除(重开后若再关会重新入列)。 */
|
|
2796
|
+
function dropClosedSideChat(parentSessionId, childId) {
|
|
2502
2797
|
try {
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
} catch {
|
|
2509
|
-
return null;
|
|
2510
|
-
}
|
|
2798
|
+
const key = KEY_PREFIX + parentSessionId;
|
|
2799
|
+
const list = readAll(key).filter((e) => e.childId !== childId);
|
|
2800
|
+
if (list.length === 0) localStorage.removeItem(key);
|
|
2801
|
+
else localStorage.setItem(key, JSON.stringify(list));
|
|
2802
|
+
} catch {}
|
|
2511
2803
|
}
|
|
2512
2804
|
//#endregion
|
|
2513
2805
|
//#region src/client/sidechat/ModelMenu.tsx
|
|
2514
2806
|
/**
|
|
2515
2807
|
* 模型选择器(WI-02):composer 脚部的模型 chip → 两级 Menu(provider 分组
|
|
2516
|
-
* 标签 +
|
|
2517
|
-
*
|
|
2808
|
+
* 标签 + 型号行)。数据面走 lifecycle.ts 的双版本链(0.1.5 的
|
|
2809
|
+
* remote.session.modelCatalog/selectModel 优先,<= 0.1.2 的
|
|
2810
|
+
* connection.api.sessions 回退);切的是子会话自己的选择,不回写主会话。
|
|
2518
2811
|
*
|
|
2519
|
-
* 懒加载:首次打开才拉目录(
|
|
2520
|
-
* (selectModel 返回的 selected 为准)。
|
|
2812
|
+
* 懒加载:首次打开才拉目录(catalog RPC 有成本);切换后本地标签立即更新。
|
|
2521
2813
|
*/
|
|
2522
2814
|
function ModelMenu(props) {
|
|
2523
2815
|
useLocaleTick();
|
|
@@ -3437,6 +3729,47 @@ window.__ModuleLoader__.load({
|
|
|
3437
3729
|
ctx,
|
|
3438
3730
|
tab.id
|
|
3439
3731
|
]);
|
|
3732
|
+
const composerRef = (0, react.useRef)(composer);
|
|
3733
|
+
composerRef.current = composer;
|
|
3734
|
+
const metaRef = (0, react.useRef)(tab.meta);
|
|
3735
|
+
metaRef.current = tab.meta;
|
|
3736
|
+
const phaseRef = (0, react.useRef)(phase);
|
|
3737
|
+
phaseRef.current = phase;
|
|
3738
|
+
const titleRef = (0, react.useRef)(tab.title);
|
|
3739
|
+
titleRef.current = tab.title;
|
|
3740
|
+
(0, react.useEffect)(() => {
|
|
3741
|
+
const metaNow = parseSideChatMeta(metaRef.current);
|
|
3742
|
+
if (nativeSidebarHost(ctx) && metaNow.childId !== void 0) dropClosedSideChat(scope.sessionId, metaNow.childId);
|
|
3743
|
+
const dispose = registerLiveSideChat(scope.sessionId, tab.id, (text) => {
|
|
3744
|
+
if (phaseRef.current !== "chat") {
|
|
3745
|
+
updateTabMeta(ctx, tab.id, (m) => ({
|
|
3746
|
+
...m,
|
|
3747
|
+
pendingDraft: appendDraftText(m.pendingDraft ?? "", text)
|
|
3748
|
+
}));
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
composerRef.current.appendDraft(text);
|
|
3752
|
+
requestAnimationFrame(() => {
|
|
3753
|
+
rootRef.current?.querySelector("textarea")?.focus();
|
|
3754
|
+
});
|
|
3755
|
+
}, () => metaRef.current, () => titleRef.current);
|
|
3756
|
+
return () => {
|
|
3757
|
+
dispose();
|
|
3758
|
+
if (nativeSidebarHost(ctx)) {
|
|
3759
|
+
const m = parseSideChatMeta(metaRef.current);
|
|
3760
|
+
if (m.childId !== void 0 && m.parentSessionId !== void 0) recordClosedSideChat(m.parentSessionId, {
|
|
3761
|
+
childId: m.childId,
|
|
3762
|
+
parentSessionId: m.parentSessionId,
|
|
3763
|
+
title: titleRef.current,
|
|
3764
|
+
closedAt: Date.now()
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
3767
|
+
};
|
|
3768
|
+
}, [
|
|
3769
|
+
scope.sessionId,
|
|
3770
|
+
tab.id,
|
|
3771
|
+
ctx
|
|
3772
|
+
]);
|
|
3440
3773
|
const promote = (0, react.useCallback)(() => {
|
|
3441
3774
|
if (childId === void 0) return;
|
|
3442
3775
|
(async () => {
|
|
@@ -3607,42 +3940,6 @@ window.__ModuleLoader__.load({
|
|
|
3607
3940
|
});
|
|
3608
3941
|
}
|
|
3609
3942
|
//#endregion
|
|
3610
|
-
//#region src/client/sidechat/recentClosed.ts
|
|
3611
|
-
const KEY_PREFIX = "dsh-sidenote:closed-side:v1:";
|
|
3612
|
-
const CAP = 5;
|
|
3613
|
-
function readAll(storageKey) {
|
|
3614
|
-
try {
|
|
3615
|
-
const raw = localStorage.getItem(storageKey);
|
|
3616
|
-
if (raw === null) return [];
|
|
3617
|
-
const parsed = JSON.parse(raw);
|
|
3618
|
-
if (!Array.isArray(parsed)) return [];
|
|
3619
|
-
return parsed.filter((e) => typeof e === "object" && e !== null && typeof e.childId === "string" && typeof e.parentSessionId === "string" && typeof e.title === "string");
|
|
3620
|
-
} catch {
|
|
3621
|
-
return [];
|
|
3622
|
-
}
|
|
3623
|
-
}
|
|
3624
|
-
function recordClosedSideChat(parentSessionId, entry) {
|
|
3625
|
-
try {
|
|
3626
|
-
const key = KEY_PREFIX + parentSessionId;
|
|
3627
|
-
const list = [entry, ...readAll(key).filter((e) => e.childId !== entry.childId)].slice(0, CAP);
|
|
3628
|
-
localStorage.setItem(key, JSON.stringify(list));
|
|
3629
|
-
} catch {}
|
|
3630
|
-
}
|
|
3631
|
-
/** 读某主会话的最近关闭列表(新者在前)。 */
|
|
3632
|
-
function listClosedSideChats(parentSessionId) {
|
|
3633
|
-
if (typeof localStorage === "undefined") return [];
|
|
3634
|
-
return readAll(KEY_PREFIX + parentSessionId);
|
|
3635
|
-
}
|
|
3636
|
-
/** 重开消费:从列表移除(重开后若再关会重新入列)。 */
|
|
3637
|
-
function dropClosedSideChat(parentSessionId, childId) {
|
|
3638
|
-
try {
|
|
3639
|
-
const key = KEY_PREFIX + parentSessionId;
|
|
3640
|
-
const list = readAll(key).filter((e) => e.childId !== childId);
|
|
3641
|
-
if (list.length === 0) localStorage.removeItem(key);
|
|
3642
|
-
else localStorage.setItem(key, JSON.stringify(list));
|
|
3643
|
-
} catch {}
|
|
3644
|
-
}
|
|
3645
|
-
//#endregion
|
|
3646
3943
|
//#region src/client/sidechat/header.tsx
|
|
3647
3944
|
/**
|
|
3648
3945
|
* 顶栏「侧边」入口(Delivery_02 Workitem_02 发现性):注册进宿主的
|
|
@@ -3684,27 +3981,42 @@ window.__ModuleLoader__.load({
|
|
|
3684
3981
|
}
|
|
3685
3982
|
//#endregion
|
|
3686
3983
|
//#region src/client/sidechat/slash.ts
|
|
3984
|
+
/** 0.1.5 信号:`connection.api.sessions` 面在 0.1.5 被删(与模型面迁移同判据)。 */
|
|
3985
|
+
function isDsh015Plus(ctx) {
|
|
3986
|
+
const legacy = ctx.connection?.api?.sessions;
|
|
3987
|
+
return legacy === void 0 || legacy === null;
|
|
3988
|
+
}
|
|
3687
3989
|
/** 单个命令名对应的完整贡献(side / 侧边 共用)。 */
|
|
3688
3990
|
function makeContribution(ctx, name) {
|
|
3689
3991
|
return {
|
|
3690
3992
|
name,
|
|
3691
|
-
description: t("cmdDesc"),
|
|
3993
|
+
description: isDsh015Plus(ctx) ? () => t("cmdDesc") : t("cmdDesc"),
|
|
3692
3994
|
available: (session) => canForkFrom(ctx, session.sessionId),
|
|
3693
3995
|
ui: {
|
|
3694
3996
|
kind: "popupSelect",
|
|
3695
3997
|
options: (session) => {
|
|
3696
|
-
const options = [
|
|
3998
|
+
const options = [];
|
|
3999
|
+
const native = nativeSidebarHost(ctx);
|
|
4000
|
+
const lives = native ? liveSideChatsOf(session.sessionId) : [];
|
|
4001
|
+
if (!native || lives.length === 0) options.push({
|
|
3697
4002
|
id: "new",
|
|
3698
4003
|
label: t("cmdNew"),
|
|
3699
4004
|
detail: t("cmdNewDetail")
|
|
3700
|
-
}
|
|
3701
|
-
const
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
label: t("cmdFocus", { title: tab.title }),
|
|
4005
|
+
});
|
|
4006
|
+
if (native) for (const live of lives) options.push({
|
|
4007
|
+
id: `focus:${live.tabId}`,
|
|
4008
|
+
label: t("cmdFocus", { title: live.readTitle() }),
|
|
3705
4009
|
detail: t("cmdFocusDetail")
|
|
3706
4010
|
});
|
|
3707
|
-
|
|
4011
|
+
else {
|
|
4012
|
+
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
4013
|
+
if (snapshot.sessionId === session.sessionId && snapshot.state !== void 0) for (const tab of collectSideTabs(snapshot.state)) options.push({
|
|
4014
|
+
id: `focus:${tab.id}`,
|
|
4015
|
+
label: t("cmdFocus", { title: tab.title }),
|
|
4016
|
+
detail: t("cmdFocusDetail")
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
4019
|
+
if (!(native && lives.length > 0)) for (const entry of listClosedSideChats(session.sessionId)) options.push({
|
|
3708
4020
|
id: `reopen:${entry.childId}`,
|
|
3709
4021
|
label: t("cmdReopen", { title: entry.title }),
|
|
3710
4022
|
detail: t("cmdReopenDetail")
|
|
@@ -3717,7 +4029,8 @@ window.__ModuleLoader__.load({
|
|
|
3717
4029
|
return;
|
|
3718
4030
|
}
|
|
3719
4031
|
if (option.id.startsWith("focus:")) {
|
|
3720
|
-
|
|
4032
|
+
const tabId = option.id.slice(6);
|
|
4033
|
+
if (!focusNativeTab(ctx, tabId)) ctx.betterSidebar.activateTab(tabId, { sessionId: session.sessionId });
|
|
3721
4034
|
return;
|
|
3722
4035
|
}
|
|
3723
4036
|
if (option.id.startsWith("reopen:")) {
|
|
@@ -3903,11 +4216,18 @@ window.__ModuleLoader__.load({
|
|
|
3903
4216
|
icon: (size) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutline16, { size }),
|
|
3904
4217
|
order: 60,
|
|
3905
4218
|
available: (availableCtx, scope) => canForkFrom(availableCtx, scope.sessionId),
|
|
3906
|
-
createTab: (state) =>
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
4219
|
+
createTab: (state) => {
|
|
4220
|
+
const titles = collectSideTabs(state).map((tab) => tab.title);
|
|
4221
|
+
if (nativeSidebarHost(ctx)) {
|
|
4222
|
+
const current = ctx.sessions.list.getSnapshot().current;
|
|
4223
|
+
if (current !== void 0) for (const live of liveSideChatsOf(current)) titles.push(live.readTitle());
|
|
4224
|
+
}
|
|
4225
|
+
return { tab: {
|
|
4226
|
+
id: mintSideTabId(),
|
|
4227
|
+
type: SIDE_TAB_TYPE,
|
|
4228
|
+
title: sideTabTitle(titles)
|
|
4229
|
+
} };
|
|
4230
|
+
},
|
|
3911
4231
|
onClose: (closedTab) => {
|
|
3912
4232
|
const meta = parseSideChatMeta(closedTab.meta);
|
|
3913
4233
|
if (meta.childId !== void 0 && meta.parentSessionId !== void 0) recordClosedSideChat(meta.parentSessionId, {
|
|
@@ -4389,43 +4709,43 @@ window.__ModuleLoader__.load({
|
|
|
4389
4709
|
document.head.appendChild(tag);
|
|
4390
4710
|
}
|
|
4391
4711
|
var annotate_module_css_default = {
|
|
4392
|
-
"toolbarButton": "_SVViG_toolbarButton",
|
|
4393
|
-
"editorSpacer": "_SVViG_editorSpacer",
|
|
4394
|
-
"sentCardTitle": "_SVViG_sentCardTitle",
|
|
4395
|
-
"chipRemove": "_SVViG_chipRemove",
|
|
4396
|
-
"toolbarBelow": "_SVViG_toolbarBelow",
|
|
4397
|
-
"editorTextarea": "_SVViG_editorTextarea",
|
|
4398
|
-
"chipWrap": "_SVViG_chipWrap",
|
|
4399
|
-
"chipClearAll": "_SVViG_chipClearAll",
|
|
4400
|
-
"editorNew": "_SVViG_editorNew",
|
|
4401
|
-
"badge": "_SVViG_badge",
|
|
4402
|
-
"editorAskRow": "_SVViG_editorAskRow",
|
|
4403
|
-
"chipText": "_SVViG_chipText",
|
|
4404
4712
|
"badgeSent": "_SVViG_badgeSent",
|
|
4405
|
-
"
|
|
4406
|
-
"cancelButton": "_SVViG_cancelButton",
|
|
4407
|
-
"deleteButton": "_SVViG_deleteButton",
|
|
4408
|
-
"chipPanel": "_SVViG_chipPanel",
|
|
4409
|
-
"chipNote": "_SVViG_chipNote",
|
|
4713
|
+
"chipClearAll": "_SVViG_chipClearAll",
|
|
4410
4714
|
"highlight": "_SVViG_highlight",
|
|
4715
|
+
"chipRemove": "_SVViG_chipRemove",
|
|
4411
4716
|
"dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
|
|
4412
|
-
"
|
|
4413
|
-
"hintClose": "_SVViG_hintClose",
|
|
4414
|
-
"chipNumber": "_SVViG_chipNumber",
|
|
4415
|
-
"saveButton": "_SVViG_saveButton",
|
|
4416
|
-
"chip": "_SVViG_chip",
|
|
4717
|
+
"editorNew": "_SVViG_editorNew",
|
|
4417
4718
|
"chipRow": "_SVViG_chipRow",
|
|
4418
|
-
"
|
|
4719
|
+
"editorTextarea": "_SVViG_editorTextarea",
|
|
4720
|
+
"editorAsk": "_SVViG_editorAsk",
|
|
4721
|
+
"chipNote": "_SVViG_chipNote",
|
|
4722
|
+
"chip": "_SVViG_chip",
|
|
4723
|
+
"chipText": "_SVViG_chipText",
|
|
4724
|
+
"chipPanel": "_SVViG_chipPanel",
|
|
4725
|
+
"editorError": "_SVViG_editorError",
|
|
4726
|
+
"toolbarBelow": "_SVViG_toolbarBelow",
|
|
4419
4727
|
"sentCardQuote": "_SVViG_sentCardQuote",
|
|
4420
4728
|
"hint": "_SVViG_hint",
|
|
4421
|
-
"
|
|
4422
|
-
"
|
|
4729
|
+
"sentChipRow": "_SVViG_sentChipRow",
|
|
4730
|
+
"deleteButton": "_SVViG_deleteButton",
|
|
4731
|
+
"sentCardNote": "_SVViG_sentCardNote",
|
|
4423
4732
|
"confirmButton": "_SVViG_confirmButton",
|
|
4424
|
-
"
|
|
4425
|
-
"
|
|
4733
|
+
"cancelButton": "_SVViG_cancelButton",
|
|
4734
|
+
"editorFooter": "_SVViG_editorFooter",
|
|
4735
|
+
"sentCardTitle": "_SVViG_sentCardTitle",
|
|
4736
|
+
"badge": "_SVViG_badge",
|
|
4426
4737
|
"editorTarget": "_SVViG_editorTarget",
|
|
4427
|
-
"
|
|
4428
|
-
"
|
|
4738
|
+
"chipNumber": "_SVViG_chipNumber",
|
|
4739
|
+
"editorAskRow": "_SVViG_editorAskRow",
|
|
4740
|
+
"editorSpacer": "_SVViG_editorSpacer",
|
|
4741
|
+
"hintClose": "_SVViG_hintClose",
|
|
4742
|
+
"editorInput": "_SVViG_editorInput",
|
|
4743
|
+
"saveButton": "_SVViG_saveButton",
|
|
4744
|
+
"toolbarButton": "_SVViG_toolbarButton",
|
|
4745
|
+
"editorEdit": "_SVViG_editorEdit",
|
|
4746
|
+
"chipWrap": "_SVViG_chipWrap",
|
|
4747
|
+
"sentChip": "_SVViG_sentChip",
|
|
4748
|
+
"toolbar": "_SVViG_toolbar"
|
|
4429
4749
|
};
|
|
4430
4750
|
//#endregion
|
|
4431
4751
|
//#region src/client/annotate/hint.tsx
|
|
@@ -5537,6 +5857,12 @@ window.__ModuleLoader__.load({
|
|
|
5537
5857
|
"locale"
|
|
5538
5858
|
];
|
|
5539
5859
|
function apply(ctx) {
|
|
5860
|
+
ctx.effect(() => {
|
|
5861
|
+
setRootContext(ctx);
|
|
5862
|
+
return () => {
|
|
5863
|
+
clearNativeRuntime();
|
|
5864
|
+
};
|
|
5865
|
+
}, "dsh-sidenote: native bridge");
|
|
5540
5866
|
attachLocale(ctx.locale);
|
|
5541
5867
|
probeHost(ctx);
|
|
5542
5868
|
const reflow = createReflowStore();
|