dsh-sidenote 0.3.2 → 0.3.3
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 +68 -40
- package/lib/client.js +834 -553
- 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 +80 -0
- package/lib/types/client/sidechat/open.d.ts +5 -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 +4 -0
- package/src/client/sidechat/ModelMenu.tsx +4 -4
- package/src/client/sidechat/SideChatPanel.tsx +56 -1
- package/src/client/sidechat/lifecycle.ts +128 -22
- package/src/client/sidechat/native.ts +168 -0
- package/src/client/sidechat/open.ts +56 -7
- package/src/client/sidechat/slash.ts +12 -2
package/lib/client.js
CHANGED
|
@@ -4,57 +4,117 @@ 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
|
+
* Publish a mounted panel so programmatic entry points can focus it and seed
|
|
40
|
+
* its draft. Returns the disposer for the panel's effect cleanup.
|
|
41
|
+
*/
|
|
42
|
+
function registerLiveSideChat(sessionId, tabId, seedDraft, readMeta) {
|
|
43
|
+
const entry = {
|
|
44
|
+
tabId,
|
|
45
|
+
sessionId,
|
|
46
|
+
seedDraft,
|
|
47
|
+
readMeta
|
|
48
|
+
};
|
|
49
|
+
liveSideChats.set(tabId, entry);
|
|
50
|
+
const opening = openings.get(sessionId);
|
|
51
|
+
if (opening !== void 0) {
|
|
52
|
+
openings.delete(sessionId);
|
|
53
|
+
for (const draft of opening.drafts) seedDraft(draft);
|
|
54
|
+
}
|
|
55
|
+
return () => {
|
|
56
|
+
if (liveSideChats.get(tabId) === entry) liveSideChats.delete(tabId);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** 标记失效应力:openTab 被静默拒绝/面板未挂载时,超时后允许重试。 */
|
|
60
|
+
const OPENING_TTL_MS = 1e4;
|
|
61
|
+
const openings = /* @__PURE__ */ new Map();
|
|
62
|
+
/** 标记一次已发起、面板尚未注册的 native 打开(create/reopen 铸造后调用)。 */
|
|
63
|
+
function markSideChatOpening(sessionId) {
|
|
64
|
+
openings.set(sessionId, {
|
|
65
|
+
at: Date.now(),
|
|
66
|
+
drafts: []
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** 进行中的打开;TTL 外视为 openTab 被拒/面板未挂载,清除并返回 undefined。 */
|
|
70
|
+
function sideChatOpening(sessionId) {
|
|
71
|
+
const mark = openings.get(sessionId);
|
|
72
|
+
if (mark === void 0) return void 0;
|
|
73
|
+
if (Date.now() - mark.at > OPENING_TTL_MS) {
|
|
74
|
+
openings.delete(sessionId);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
return mark;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Most recently registered live panel of a session — the open-or-focus target
|
|
81
|
+
* when the legacy snapshot cannot see the tab (mirrors "last opened wins").
|
|
82
|
+
*/
|
|
83
|
+
function lastLiveSideChat(sessionId) {
|
|
84
|
+
let found;
|
|
85
|
+
for (const entry of liveSideChats.values()) if (entry.sessionId === sessionId) found = entry;
|
|
86
|
+
return found;
|
|
87
|
+
}
|
|
88
|
+
/** Native tab shell for reads that go through the layout snapshot (`readTab`). */
|
|
89
|
+
function nativeTabShell(tabId, type) {
|
|
90
|
+
const entry = liveSideChats.get(tabId);
|
|
91
|
+
if (entry === void 0) return void 0;
|
|
92
|
+
return {
|
|
93
|
+
id: entry.tabId,
|
|
94
|
+
type,
|
|
95
|
+
title: "",
|
|
96
|
+
meta: entry.readMeta()
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 聚焦一个 native tab:better-sidebar >= 0.19 的 `surface.activate` 是空操作
|
|
101
|
+
* (只返回记录存在性——authority: dsh-better-sidebar src/client/native/surface.ts),
|
|
102
|
+
* 真实聚焦面是 dsh 0.1.5 的 `ISidebarRight.focus`(authority:
|
|
103
|
+
* dsh-client-ui-sidebar-right lib/types/client/service.d.ts `focus(tabId)`)。
|
|
104
|
+
* focus 只标记活动 tab/pane,不展开折叠的栏——聚焦的语义包含「让用户看见」,
|
|
105
|
+
* 故折叠时补 toggleExpanded(authority: 同文件 `isExpanded/toggleExpanded`)。
|
|
106
|
+
* @returns true = 已通过 focus 聚焦;false = 面缺席(调用方回退 activateTab)。
|
|
107
|
+
*/
|
|
108
|
+
function focusNativeTab(ctx, tabId) {
|
|
109
|
+
try {
|
|
110
|
+
const face = ctx.get("sidebarRight");
|
|
111
|
+
if (typeof face?.focus !== "function") return false;
|
|
112
|
+
face.focus(tabId);
|
|
113
|
+
if (face.isExpanded?.() === false) face.toggleExpanded?.();
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
56
117
|
}
|
|
57
|
-
return missing;
|
|
58
118
|
}
|
|
59
119
|
//#endregion
|
|
60
120
|
//#region src/client/locales.ts
|
|
@@ -350,135 +410,6 @@ window.__ModuleLoader__.load({
|
|
|
350
410
|
return text;
|
|
351
411
|
}
|
|
352
412
|
//#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
413
|
//#region src/client/annotate/format.ts
|
|
483
414
|
/** 协议头(双语):zh「我批注了以下 2 处内容:」/ en 见 locales。 */
|
|
484
415
|
function protocolHeader(n) {
|
|
@@ -831,6 +762,540 @@ window.__ModuleLoader__.load({
|
|
|
831
762
|
};
|
|
832
763
|
}
|
|
833
764
|
//#endregion
|
|
765
|
+
//#region src/client/sidechat/open.ts
|
|
766
|
+
/**
|
|
767
|
+
* 从最新快照读一个 Tab(meta 合并写入前的读取面;布局即注册表)。
|
|
768
|
+
* better-sidebar >= 0.19: the open tab lives in the native right sidebar, not
|
|
769
|
+
* in the layout snapshot — fall back to the live-panel registry.
|
|
770
|
+
*/
|
|
771
|
+
function readTab(ctx, tabId) {
|
|
772
|
+
return collectTabs(ctx.betterSidebar.getSnapshot().state).find((candidate) => candidate.id === tabId) ?? nativeTabShell(tabId, "dsh-sidenote:side");
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* 为 sessionId 新建或聚焦一个侧边聊天 Tab;draftText 给出时写入其草稿。
|
|
776
|
+
* @returns false = 未能打开(调用方保持自己的流程)。
|
|
777
|
+
*/
|
|
778
|
+
function openOrFocusSideChat(ctx, sessionId, draftText) {
|
|
779
|
+
try {
|
|
780
|
+
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
781
|
+
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
|
|
782
|
+
const existing = collectSideTabs(snapshot.state);
|
|
783
|
+
const live = lastLiveSideChat(sessionId);
|
|
784
|
+
if (live !== void 0) {
|
|
785
|
+
if (draftText !== void 0 && draftText !== "") live.seedDraft(draftText);
|
|
786
|
+
if (!focusNativeTab(ctx, live.tabId)) ctx.betterSidebar.activateTab(live.tabId, { sessionId });
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
if (existing.length > 0) {
|
|
790
|
+
const target = existing[existing.length - 1];
|
|
791
|
+
if (draftText !== void 0 && draftText !== "") {
|
|
792
|
+
const meta = parseSideChatMeta(target.meta);
|
|
793
|
+
const input = meta.childId === void 0 ? null : resolveSessionInput(ctx, meta.childId);
|
|
794
|
+
if (input !== null) input.setDraft(appendDraftText(readInputDraft(input), draftText));
|
|
795
|
+
else ctx.betterSidebar.updateTab(target.id, { meta: {
|
|
796
|
+
...meta,
|
|
797
|
+
pendingDraft: appendDraftText(meta.pendingDraft ?? "", draftText)
|
|
798
|
+
} });
|
|
799
|
+
}
|
|
800
|
+
ctx.betterSidebar.activateTab(target.id, { sessionId });
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
const opening = sideChatOpening(sessionId);
|
|
804
|
+
if (opening !== void 0) {
|
|
805
|
+
if (draftText !== void 0 && draftText !== "") opening.drafts.push(draftText);
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
return createSideChat(ctx, sessionId, draftText);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
console.warn("[dsh-sidenote] 打开侧边聊天失败:", error);
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* 无条件新建一个侧边聊天 Tab(/side「新建侧边聊天」选项的语义——既有实例
|
|
816
|
+
* 在弹层里另有聚焦项,「新建」必须真的新建,否则标签与行为不符)。
|
|
817
|
+
* @returns false = 未能创建(调用方保持自己的流程)。
|
|
818
|
+
*/
|
|
819
|
+
function createSideChat(ctx, sessionId, draftText) {
|
|
820
|
+
try {
|
|
821
|
+
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
822
|
+
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
|
|
823
|
+
if (!ctx.betterSidebar.isTabEnabled("dsh-sidenote:side")) return false;
|
|
824
|
+
const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
|
|
825
|
+
const seed = draftText !== void 0 && draftText !== "" ? {
|
|
826
|
+
type: SIDE_TAB_TYPE,
|
|
827
|
+
meta: { pendingDraft: draftText }
|
|
828
|
+
} : { type: SIDE_TAB_TYPE };
|
|
829
|
+
ctx.betterSidebar.openTab(seed, { sessionId });
|
|
830
|
+
const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
|
|
831
|
+
if (created === void 0) {
|
|
832
|
+
if (!nativeSidebarHost(ctx)) return false;
|
|
833
|
+
markSideChatOpening(sessionId);
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
if (draftText !== void 0 && draftText !== "") ctx.betterSidebar.updateTab(created.id, { meta: { pendingDraft: draftText } });
|
|
837
|
+
return true;
|
|
838
|
+
} catch (error) {
|
|
839
|
+
console.warn("[dsh-sidenote] 新建侧边聊天失败:", error);
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* 桥接/命令的目标预览:askInSideChat 此时若执行会落在哪个 Tab 的标题
|
|
845
|
+
* (既有并存 = 最后一个;无 = 即将新建的首个标题)。用于编辑器上标明
|
|
846
|
+
* 「发送至:侧边 N」。会话不在屏上/状态缺失时返回 undefined。
|
|
847
|
+
*/
|
|
848
|
+
function sideChatTargetTitle(ctx, sessionId) {
|
|
849
|
+
try {
|
|
850
|
+
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
851
|
+
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return void 0;
|
|
852
|
+
const existing = collectSideTabs(snapshot.state);
|
|
853
|
+
if (existing.length > 0) return existing[existing.length - 1].title;
|
|
854
|
+
return sideTabTitle([]);
|
|
855
|
+
} catch {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* 重开最近关闭的侧边聊天(D3 后悔药):开 Tab + meta 预置 childId/
|
|
861
|
+
* parentSessionId——面板挂载后走绑定恢复路径(不重新 fork)。
|
|
862
|
+
* 会话本体必须仍 archived 在列(若被清理,面板落「会话已不存在」态——
|
|
863
|
+
* 有现成错误态兜着)。
|
|
864
|
+
*/
|
|
865
|
+
function reopenSideChat(ctx, sessionId, childId, title) {
|
|
866
|
+
try {
|
|
867
|
+
const snapshot = ctx.betterSidebar.getSnapshot();
|
|
868
|
+
if (snapshot.sessionId !== sessionId || snapshot.state === void 0) return false;
|
|
869
|
+
if (!ctx.betterSidebar.isTabEnabled("dsh-sidenote:side")) return false;
|
|
870
|
+
const before = new Set(collectTabs(snapshot.state).map((tab) => tab.id));
|
|
871
|
+
const meta = {
|
|
872
|
+
childId,
|
|
873
|
+
parentSessionId: sessionId
|
|
874
|
+
};
|
|
875
|
+
ctx.betterSidebar.openTab({
|
|
876
|
+
type: SIDE_TAB_TYPE,
|
|
877
|
+
...title !== void 0 ? { title } : {},
|
|
878
|
+
meta
|
|
879
|
+
}, { sessionId });
|
|
880
|
+
const created = collectSideTabs(ctx.betterSidebar.getSnapshot().state).find((tab) => !before.has(tab.id));
|
|
881
|
+
if (created === void 0) {
|
|
882
|
+
if (!nativeSidebarHost(ctx)) return false;
|
|
883
|
+
markSideChatOpening(sessionId);
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
ctx.betterSidebar.updateTab(created.id, {
|
|
887
|
+
...title !== void 0 ? { title } : {},
|
|
888
|
+
meta
|
|
889
|
+
});
|
|
890
|
+
return true;
|
|
891
|
+
} catch (error) {
|
|
892
|
+
console.warn("[dsh-sidenote] 重开侧边聊天失败:", error);
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/client/sidechat/lifecycle.ts
|
|
898
|
+
/**
|
|
899
|
+
* 首开编排:fork(全量历史快照)→ 登记 meta(先行,绝不丢登记)→
|
|
900
|
+
* 归档隐藏出会话列表(best-effort)→ 模型跟随主会话(best-effort)。
|
|
901
|
+
* fork 失败抛错(面板组件转错误态);后两步失败只告警不阻断。
|
|
902
|
+
* @returns fork 出的子会话 id。
|
|
903
|
+
*/
|
|
904
|
+
async function forkAndRegister(ctx, parentSessionId, tabId) {
|
|
905
|
+
let boundarySeq;
|
|
906
|
+
try {
|
|
907
|
+
const snap = chatSourceOf(ctx, ctx.sessions.binding(parentSessionId))?.getLegacy();
|
|
908
|
+
let max = -1;
|
|
909
|
+
for (const node of snap?.nodes ?? []) {
|
|
910
|
+
const seq = node.seq;
|
|
911
|
+
if (typeof seq === "number" && seq > max) max = seq;
|
|
912
|
+
}
|
|
913
|
+
if (max >= 0) boundarySeq = max;
|
|
914
|
+
} catch {}
|
|
915
|
+
const forked = await ctx.sessions.fork({ sessionId: parentSessionId });
|
|
916
|
+
const owner = rootContext(ctx);
|
|
917
|
+
updateTabMeta(ctx, tabId, (current) => ({
|
|
918
|
+
...current,
|
|
919
|
+
childId: forked,
|
|
920
|
+
parentSessionId,
|
|
921
|
+
...boundarySeq !== void 0 ? { boundarySeq } : {}
|
|
922
|
+
}));
|
|
923
|
+
try {
|
|
924
|
+
await owner.workspaces.archiveSession(forked);
|
|
925
|
+
} catch (error) {
|
|
926
|
+
console.warn("[dsh-sidenote] 归档侧边会话失败(会话列表可能短暂可见):", error);
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
const selection = await readModelSelection(owner, parentSessionId);
|
|
930
|
+
if (selection === void 0) console.warn("[dsh-sidenote] 同步主会话模型跳过:主机模型 API 不可用");
|
|
931
|
+
else if (!await writeModelSelection(owner, forked, selection)) console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型)");
|
|
932
|
+
} catch (error) {
|
|
933
|
+
console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型):", error);
|
|
934
|
+
}
|
|
935
|
+
return forked;
|
|
936
|
+
}
|
|
937
|
+
/** Tab meta 读-改-写(布局持久化寄存处);tab 已消失时 no-op。 */
|
|
938
|
+
function updateTabMeta(ctx, tabId, mutate) {
|
|
939
|
+
const current = parseSideChatMeta(readTab(ctx, tabId)?.meta);
|
|
940
|
+
ctx.betterSidebar.updateTab(tabId, { meta: mutate(current) });
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* 程序化入口(/side、bridge 划选提问)打开 Tab 时面板可能处于折叠态——
|
|
944
|
+
* 类型型 openTab 不自动展开(better-sidebar 只对 path/url 内容型打开展开)。
|
|
945
|
+
* 幂等展开(off-face:store.update 是 SidebarStore 公开 mutator 面;
|
|
946
|
+
* feature-check + 吞错,兜底失败不影响功能)。
|
|
947
|
+
*/
|
|
948
|
+
function ensurePanelOpen(store) {
|
|
949
|
+
const mutable = store;
|
|
950
|
+
try {
|
|
951
|
+
mutable?.update?.((state) => {
|
|
952
|
+
if (state.panelOpen === false) state.panelOpen = true;
|
|
953
|
+
});
|
|
954
|
+
} catch {}
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* 打开非 staged 会话的事件窗口(off-face:open() 在 concrete Session 上是
|
|
958
|
+
* public 且幂等,但不在 SessionFace 契约上——契约面只有 staged 会话会被
|
|
959
|
+
* 运行时自动 open)。feature-check + 吞错,运行时若移除则降级为只发不收。
|
|
960
|
+
*/
|
|
961
|
+
function openSessionWindow(session) {
|
|
962
|
+
const openable = session;
|
|
963
|
+
if (typeof openable?.open !== "function") return;
|
|
964
|
+
openable.open().catch((error) => {
|
|
965
|
+
console.warn("[dsh-sidenote] 会话窗口打开失败:", error);
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Current model selection of a session, read from its `modelSelection`
|
|
970
|
+
* projection (dsh >= 0.1.2): the durable selection is projected state, so no RPC
|
|
971
|
+
* is needed and the read is safe for any session id. Snapshot shape authority:
|
|
972
|
+
* `@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts`
|
|
973
|
+
* (`ModelSelectionProjection = { lastUsed, next }`,两者均可为 null——
|
|
974
|
+
* `next` 缺省时按投影文档回落 `lastUsed`)。
|
|
975
|
+
*/
|
|
976
|
+
function projectedModelSelection(ctx, sessionId) {
|
|
977
|
+
try {
|
|
978
|
+
const snapshot = (ctx.sessions.binding(sessionId)?.session)?.projections?.faceOf?.("modelSelection")?.getSnapshot?.();
|
|
979
|
+
return snapshot?.next ?? snapshot?.lastUsed ?? void 0;
|
|
980
|
+
} catch {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Remote session face carrying `selectModel`/`modelCatalog`(dsh 0.1.5 删除
|
|
986
|
+
* `connection.api.sessions.*` client 面后的模型 RPC 面;事实上 0.1.2-rc.1
|
|
987
|
+
* 起存在——authority: dsh-api-session-controller lib/typert.remote-client.d.ts)。
|
|
988
|
+
* 探测走 `ctx.get`(cordis 的 get 不受 inject 门禁限制,本插件为兼容旧宿主
|
|
989
|
+
* 不声明 inject 'remote.session';机制见 contracts.ts RemoteSessionModelFace)。
|
|
990
|
+
* `ctx.get('remote')` 返回命名空间容器时下钻其 `.session` 成员。
|
|
991
|
+
*/
|
|
992
|
+
function remoteSessionFace(ctx) {
|
|
993
|
+
for (const name of ["remote.session", "remote"]) {
|
|
994
|
+
let candidate;
|
|
995
|
+
try {
|
|
996
|
+
candidate = ctx.get(name);
|
|
997
|
+
} catch {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
const face = candidate;
|
|
1001
|
+
if (typeof face?.selectModel === "function") return face;
|
|
1002
|
+
if (typeof face?.session?.selectModel === "function") return face.session;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* 读会话当前模型选择:投影面(0.1.2+,同步无 RPC)优先;旧
|
|
1007
|
+
* `connection.api.sessions.models` RPC(<= 0.1.2 的 client 面,0.1.5 已删)
|
|
1008
|
+
* 回退(双版本链,同 chatSourceOf 先例)。永不抛错。
|
|
1009
|
+
*/
|
|
1010
|
+
async function readModelSelection(ctx, sessionId) {
|
|
1011
|
+
const projected = projectedModelSelection(ctx, sessionId);
|
|
1012
|
+
if (projected !== void 0) return projected;
|
|
1013
|
+
try {
|
|
1014
|
+
const res = await ctx.connection.api.sessions.models({ sessionId });
|
|
1015
|
+
if (res.result.ok) return res.result.value.current;
|
|
1016
|
+
} catch {}
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* 写会话模型选择:`remote.session.selectModel`(新面)优先,旧
|
|
1020
|
+
* `connection.api.sessions.selectModel` 回退。面在而调用失败(模型不可路由等)
|
|
1021
|
+
* 属真实失败,不再落旧面重试。
|
|
1022
|
+
*/
|
|
1023
|
+
async function writeModelSelection(ctx, sessionId, selection) {
|
|
1024
|
+
const request = {
|
|
1025
|
+
sessionId,
|
|
1026
|
+
provider: selection.provider,
|
|
1027
|
+
model: selection.model,
|
|
1028
|
+
...selection.reasoningEffort !== void 0 ? { reasoningEffort: selection.reasoningEffort } : {}
|
|
1029
|
+
};
|
|
1030
|
+
const face = remoteSessionFace(ctx);
|
|
1031
|
+
if (face !== void 0) try {
|
|
1032
|
+
return (await face.selectModel(request)).ok;
|
|
1033
|
+
} catch {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
return (await ctx.connection.api.sessions.selectModel(request)).result.ok;
|
|
1038
|
+
} catch {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
/** 读子会话当前模型名(composer 模型标签用);任何失败回退 null(默认文案)。 */
|
|
1043
|
+
async function readModelName(ctx, sessionId) {
|
|
1044
|
+
return (await readModelSelection(ctx, sessionId))?.model ?? null;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* 0.1.2 优先:`uiConversation` 服务 → binding(会话 binding) → target('chat')
|
|
1048
|
+
* → 快照 .legacy 切片(0.1.2 把内容面从 Session 快照顶层拆到此处——
|
|
1049
|
+
* 根因与证据见 reports/ux-review/W00-fork-replay-012.md);0.1.1 回退
|
|
1050
|
+
* Session 快照顶层(nodes 直接在)。均 miss 返回 undefined(面板维持
|
|
1051
|
+
* 既有降级,不崩)。
|
|
1052
|
+
*/
|
|
1053
|
+
function chatSourceOf(ctx, binding) {
|
|
1054
|
+
const session = binding?.session;
|
|
1055
|
+
if (session === void 0) return void 0;
|
|
1056
|
+
try {
|
|
1057
|
+
const target = ctx.get("uiConversation")?.binding?.(binding)?.target?.("chat");
|
|
1058
|
+
if (target !== void 0 && typeof target.getSnapshot === "function" && typeof target.subscribe === "function") return {
|
|
1059
|
+
subscribe: (fn) => target.subscribe(fn),
|
|
1060
|
+
getLegacy: () => {
|
|
1061
|
+
return target.getSnapshot()?.legacy ?? null;
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
} catch {}
|
|
1065
|
+
const snap = session.getSnapshot();
|
|
1066
|
+
if (snap !== null && Array.isArray(snap.nodes)) return {
|
|
1067
|
+
subscribe: (fn) => session.subscribe(fn),
|
|
1068
|
+
getLegacy: () => session.getSnapshot()
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* 拉会话模型目录:0.1.5 走 `remote.session.modelCatalog()`(目录是宿主级,
|
|
1073
|
+
* 菜单选中项所需的 current 由投影/旧面补上);旧面回退
|
|
1074
|
+
* `connection.api.sessions.models`。失败 null → 菜单保持只读标签态。
|
|
1075
|
+
*/
|
|
1076
|
+
async function listModels(ctx, sessionId) {
|
|
1077
|
+
const face = remoteSessionFace(ctx);
|
|
1078
|
+
if (typeof face?.modelCatalog === "function") try {
|
|
1079
|
+
const result = await face.modelCatalog();
|
|
1080
|
+
if (result.ok) {
|
|
1081
|
+
const catalog = result.value;
|
|
1082
|
+
return {
|
|
1083
|
+
current: await readModelSelection(ctx, sessionId) ?? catalog.default,
|
|
1084
|
+
routable: catalog.routableProviders.length > 0,
|
|
1085
|
+
groups: catalog.groups,
|
|
1086
|
+
...catalog.failures !== void 0 ? { failures: catalog.failures } : {}
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
} catch {}
|
|
1090
|
+
try {
|
|
1091
|
+
const res = await ctx.connection.api.sessions.models({ sessionId });
|
|
1092
|
+
return res.result.ok ? res.result.value : null;
|
|
1093
|
+
} catch {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
/** 切换子会话模型;成功返回新模型展示名,失败 null(best-effort)。 */
|
|
1098
|
+
async function switchModel(ctx, sessionId, provider, model) {
|
|
1099
|
+
return await writeModelSelection(ctx, sessionId, {
|
|
1100
|
+
provider,
|
|
1101
|
+
model
|
|
1102
|
+
}) ? model : null;
|
|
1103
|
+
}
|
|
1104
|
+
//#endregion
|
|
1105
|
+
//#region src/client/host/probes.ts
|
|
1106
|
+
const PROBES = [
|
|
1107
|
+
{
|
|
1108
|
+
name: "Session.open()(非 staged 会话开窗)",
|
|
1109
|
+
check: (ctx) => {
|
|
1110
|
+
return typeof ctx.sessions?.binding === "function";
|
|
1111
|
+
},
|
|
1112
|
+
fallback: "侧边聊天消息流降级为只发不收"
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
name: "conversation.input 输入机器",
|
|
1116
|
+
check: (ctx) => {
|
|
1117
|
+
return typeof (ctx.get?.("conversation"))?.input?.for === "function";
|
|
1118
|
+
},
|
|
1119
|
+
fallback: "侧边 composer 降级为本地草稿 + session.prompt"
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
name: "commandUi(斜杠命令注册)",
|
|
1123
|
+
check: (ctx) => {
|
|
1124
|
+
return typeof (ctx.get?.("commandUi"))?.register === "function";
|
|
1125
|
+
},
|
|
1126
|
+
fallback: "/side 与「/侧边」命令不可用(菜单/顶栏入口仍在)"
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
name: "ctx.modules.import(动态组件复用)",
|
|
1130
|
+
check: (ctx) => {
|
|
1131
|
+
return typeof ctx.modules?.import === "function";
|
|
1132
|
+
},
|
|
1133
|
+
fallback: "ProducedFiles 等动态复用件降级为自绘"
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
name: "sidebarRight.focus(native tab 程序化聚焦)",
|
|
1137
|
+
check: (ctx) => {
|
|
1138
|
+
if (!nativeSidebarHost(ctx)) return true;
|
|
1139
|
+
return typeof (ctx.get?.("sidebarRight"))?.focus === "function";
|
|
1140
|
+
},
|
|
1141
|
+
fallback: "聚焦既有侧边聊天 tab 不可用(activateTab 在 native 面是空操作)"
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
name: "会话模型面(remote.session / connection.api.sessions 双版本链)",
|
|
1145
|
+
check: (ctx) => {
|
|
1146
|
+
if (remoteSessionFace(ctx) !== void 0) return true;
|
|
1147
|
+
return typeof ctx.connection?.api?.sessions?.models === "function";
|
|
1148
|
+
},
|
|
1149
|
+
fallback: "fork 模型同步、模型标签与模型切换不可用(子会话用宿主默认模型)"
|
|
1150
|
+
}
|
|
1151
|
+
];
|
|
1152
|
+
/** 激活时跑一遍探测清单;缺失项逐项 warn-once。返回缺失项数(测试可断言)。 */
|
|
1153
|
+
function probeHost(ctx) {
|
|
1154
|
+
let missing = 0;
|
|
1155
|
+
for (const probe of PROBES) {
|
|
1156
|
+
let ok = false;
|
|
1157
|
+
try {
|
|
1158
|
+
ok = probe.check(ctx);
|
|
1159
|
+
} catch {
|
|
1160
|
+
ok = false;
|
|
1161
|
+
}
|
|
1162
|
+
if (!ok) {
|
|
1163
|
+
missing += 1;
|
|
1164
|
+
console.warn(`[dsh-sidenote] 宿主能力缺失:${probe.name} → ${probe.fallback}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return missing;
|
|
1168
|
+
}
|
|
1169
|
+
//#endregion
|
|
1170
|
+
//#region src/client/reflow.ts
|
|
1171
|
+
/**
|
|
1172
|
+
* 回流通道的受控对象层(Delivery_02 Workitem_04 v2):侧边聊天的结论以
|
|
1173
|
+
* 「待回流对象」进入主会话——主 composer 上方的 chip(可预览/可移除),
|
|
1174
|
+
* 发送瞬间由 annotate/send.ts 拦截器与注释协议块一起序列化进消息。
|
|
1175
|
+
* 不经草稿文本流(用户裁定:直接进草稿「很脏」)。
|
|
1176
|
+
*
|
|
1177
|
+
* 气泡留痕:序列化形态是 XML 协议块(见 buildReflowBlock),随注释协议块
|
|
1178
|
+
* 一起作为消息连续前缀注入,bubble.ts 反解析后折叠为「含侧边回流上下文」标签。
|
|
1179
|
+
*
|
|
1180
|
+
* 持久化:localStorage 按会话键(dsh-sidenote:reflow:v1:<sessionId>),
|
|
1181
|
+
* 与注释 store 同款纪律(刷新不丢、容错 revive、空删键)。
|
|
1182
|
+
*/
|
|
1183
|
+
const STORAGE_PREFIX$1 = "dsh-sidenote:reflow:v1:";
|
|
1184
|
+
/**
|
|
1185
|
+
* 回流块序列化(XML 形态,与注释协议同族):
|
|
1186
|
+
*
|
|
1187
|
+
* <reflow source="侧边 2" reason="用户选择从侧边聊天带回主线">
|
|
1188
|
+
* <问>用户在侧边聊天里的提问</问>
|
|
1189
|
+
* <答>结论全文(不截断)</答>
|
|
1190
|
+
* </reflow>
|
|
1191
|
+
*
|
|
1192
|
+
* 问答成对:只有答没有问,模型理解打折;question 缺省时只有 <答>。
|
|
1193
|
+
* 来源与意图放在属性里,内容行保持纯净;模型对 XML 包裹的上下文理解最好
|
|
1194
|
+
* (Claude 官方推荐形态 / Codex additionalContext 同款思路)。
|
|
1195
|
+
*/
|
|
1196
|
+
function buildReflowBlock(item) {
|
|
1197
|
+
const parts = [];
|
|
1198
|
+
if (item.question !== void 0 && item.question.trim() !== "") parts.push(`<问>${item.question}</问>`);
|
|
1199
|
+
parts.push(`<答>${item.text}</答>`);
|
|
1200
|
+
return `<reflow source="${item.sideTitle}" reason="${t("reflowReason")}">\n${parts.join("\n")}\n</reflow>`;
|
|
1201
|
+
}
|
|
1202
|
+
function revive(value) {
|
|
1203
|
+
if (typeof value !== "object" || value === null) return null;
|
|
1204
|
+
const r = value;
|
|
1205
|
+
if (typeof r.id !== "number" || typeof r.sessionId !== "string" || r.sessionId === "") return null;
|
|
1206
|
+
if (typeof r.sideTitle !== "string" || typeof r.text !== "string") return null;
|
|
1207
|
+
return {
|
|
1208
|
+
id: r.id,
|
|
1209
|
+
sessionId: r.sessionId,
|
|
1210
|
+
sideTitle: r.sideTitle,
|
|
1211
|
+
text: r.text,
|
|
1212
|
+
...typeof r.question === "string" && r.question.trim() !== "" ? { question: r.question } : {},
|
|
1213
|
+
createdAt: typeof r.createdAt === "number" ? r.createdAt : 0
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function createReflowStore(now = () => Date.now(), storage) {
|
|
1217
|
+
let items = [];
|
|
1218
|
+
let nextId = 1;
|
|
1219
|
+
let version = 0;
|
|
1220
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1221
|
+
const store = storage === void 0 ? typeof localStorage !== "undefined" ? localStorage : null : storage;
|
|
1222
|
+
if (store != null) try {
|
|
1223
|
+
const keys = [];
|
|
1224
|
+
for (let i = 0; i < store.length; i += 1) {
|
|
1225
|
+
const key = store.key(i);
|
|
1226
|
+
if (typeof key === "string" && key.startsWith(STORAGE_PREFIX$1)) keys.push(key);
|
|
1227
|
+
}
|
|
1228
|
+
for (const key of keys) {
|
|
1229
|
+
const parsed = JSON.parse(store.getItem(key) ?? "null");
|
|
1230
|
+
if (!Array.isArray(parsed)) continue;
|
|
1231
|
+
for (const raw of parsed) {
|
|
1232
|
+
const item = revive(raw);
|
|
1233
|
+
if (item === null) continue;
|
|
1234
|
+
items.push(item);
|
|
1235
|
+
nextId = Math.max(nextId, item.id + 1);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
console.warn("[dsh-sidenote] 回流持久化读取失败(按空起步):", error);
|
|
1240
|
+
}
|
|
1241
|
+
const persist = (sessionId) => {
|
|
1242
|
+
if (store == null) return;
|
|
1243
|
+
try {
|
|
1244
|
+
const mine = items.filter((i) => i.sessionId === sessionId);
|
|
1245
|
+
const key = STORAGE_PREFIX$1 + sessionId;
|
|
1246
|
+
if (mine.length === 0) store.removeItem(key);
|
|
1247
|
+
else store.setItem(key, JSON.stringify(mine));
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
console.warn("[dsh-sidenote] 回流持久化写入失败:", error);
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
const notify = () => {
|
|
1253
|
+
version += 1;
|
|
1254
|
+
for (const fn of [...listeners]) fn();
|
|
1255
|
+
};
|
|
1256
|
+
return {
|
|
1257
|
+
getSnapshot: () => version,
|
|
1258
|
+
subscribe(fn) {
|
|
1259
|
+
listeners.add(fn);
|
|
1260
|
+
return () => {
|
|
1261
|
+
listeners.delete(fn);
|
|
1262
|
+
};
|
|
1263
|
+
},
|
|
1264
|
+
add(sessionId, sideTitle, text, question) {
|
|
1265
|
+
const item = {
|
|
1266
|
+
id: nextId,
|
|
1267
|
+
sessionId,
|
|
1268
|
+
sideTitle,
|
|
1269
|
+
text,
|
|
1270
|
+
...typeof question === "string" && question.trim() !== "" ? { question } : {},
|
|
1271
|
+
createdAt: now()
|
|
1272
|
+
};
|
|
1273
|
+
nextId += 1;
|
|
1274
|
+
items = [...items, item];
|
|
1275
|
+
persist(sessionId);
|
|
1276
|
+
notify();
|
|
1277
|
+
return item;
|
|
1278
|
+
},
|
|
1279
|
+
remove(id) {
|
|
1280
|
+
const target = items.find((i) => i.id === id);
|
|
1281
|
+
if (target === void 0) return;
|
|
1282
|
+
items = items.filter((i) => i.id !== id);
|
|
1283
|
+
persist(target.sessionId);
|
|
1284
|
+
notify();
|
|
1285
|
+
},
|
|
1286
|
+
clearSession(sessionId) {
|
|
1287
|
+
if (!items.some((i) => i.sessionId === sessionId)) return;
|
|
1288
|
+
items = items.filter((i) => i.sessionId !== sessionId);
|
|
1289
|
+
persist(sessionId);
|
|
1290
|
+
notify();
|
|
1291
|
+
},
|
|
1292
|
+
list: (sessionId) => items.filter((i) => i.sessionId === sessionId)
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
//#endregion
|
|
1296
|
+
//#region src/client/bridge.ts
|
|
1297
|
+
const sideChatBridge = { current: null };
|
|
1298
|
+
//#endregion
|
|
834
1299
|
//#region src/client/chat/cards.ts
|
|
835
1300
|
/** ContentBlock[] → 纯文本(transcript.ts 同款的极简版,避免互依赖)。 */
|
|
836
1301
|
function blocksText(content) {
|
|
@@ -1694,92 +2159,92 @@ window.__ModuleLoader__.load({
|
|
|
1694
2159
|
document.head.appendChild(tag);
|
|
1695
2160
|
}
|
|
1696
2161
|
var sidechat_module_css_default = {
|
|
1697
|
-
"composerCard": "_5KEEsG_composerCard",
|
|
1698
|
-
"streamingHint": "_5KEEsG_streamingHint",
|
|
1699
|
-
"stopButton": "_5KEEsG_stopButton",
|
|
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",
|
|
1707
|
-
"empty": "_5KEEsG_empty",
|
|
1708
|
-
"stateScreen": "_5KEEsG_stateScreen",
|
|
1709
|
-
"slashMenu": "_5KEEsG_slashMenu",
|
|
1710
|
-
"userBubble": "_5KEEsG_userBubble",
|
|
1711
|
-
"dotPending": "_5KEEsG_dotPending",
|
|
1712
|
-
"pendingOptionActive": "_5KEEsG_pendingOptionActive",
|
|
1713
|
-
"pendingDesc": "_5KEEsG_pendingDesc",
|
|
1714
|
-
"pendingActions": "_5KEEsG_pendingActions",
|
|
1715
|
-
"todoDot": "_5KEEsG_todoDot",
|
|
1716
|
-
"todoText": "_5KEEsG_todoText",
|
|
1717
|
-
"emptyIcon": "_5KEEsG_emptyIcon",
|
|
1718
|
-
"reflowButton": "_5KEEsG_reflowButton",
|
|
1719
|
-
"pendingBtnPrimary": "_5KEEsG_pendingBtnPrimary",
|
|
1720
2162
|
"reasoningBody": "_5KEEsG_reasoningBody",
|
|
1721
|
-
"
|
|
1722
|
-
"
|
|
1723
|
-
"
|
|
1724
|
-
"
|
|
1725
|
-
"slashItemName": "_5KEEsG_slashItemName",
|
|
2163
|
+
"pendingTitle": "_5KEEsG_pendingTitle",
|
|
2164
|
+
"todoItem": "_5KEEsG_todoItem",
|
|
2165
|
+
"todoDot": "_5KEEsG_todoDot",
|
|
2166
|
+
"composer": "_5KEEsG_composer",
|
|
1726
2167
|
"sendButton": "_5KEEsG_sendButton",
|
|
1727
|
-
"
|
|
2168
|
+
"userBubble": "_5KEEsG_userBubble",
|
|
2169
|
+
"toolLocations": "_5KEEsG_toolLocations",
|
|
2170
|
+
"pendingCard": "_5KEEsG_pendingCard",
|
|
2171
|
+
"pendingQuestion": "_5KEEsG_pendingQuestion",
|
|
2172
|
+
"errorRow": "_5KEEsG_errorRow",
|
|
2173
|
+
"composerFoot": "_5KEEsG_composerFoot",
|
|
2174
|
+
"sentChip": "_5KEEsG_sentChip",
|
|
2175
|
+
"noticeRow": "_5KEEsG_noticeRow",
|
|
2176
|
+
"stopButton": "_5KEEsG_stopButton",
|
|
1728
2177
|
"stateDetail": "_5KEEsG_stateDetail",
|
|
2178
|
+
"flowRowError": "_5KEEsG_flowRowError",
|
|
1729
2179
|
"emptyTitle": "_5KEEsG_emptyTitle",
|
|
2180
|
+
"slashGroupTitle": "_5KEEsG_slashGroupTitle",
|
|
1730
2181
|
"pendingCustom": "_5KEEsG_pendingCustom",
|
|
1731
|
-
"
|
|
1732
|
-
"
|
|
1733
|
-
"
|
|
1734
|
-
"modelLabel": "_5KEEsG_modelLabel",
|
|
1735
|
-
"body": "_5KEEsG_body",
|
|
1736
|
-
"toolHead": "_5KEEsG_toolHead",
|
|
1737
|
-
"reasoning": "_5KEEsG_reasoning",
|
|
1738
|
-
"pendingQHeader": "_5KEEsG_pendingQHeader",
|
|
2182
|
+
"pendingBtnGhost": "_5KEEsG_pendingBtnGhost",
|
|
2183
|
+
"attachButton": "_5KEEsG_attachButton",
|
|
2184
|
+
"pendingOptionDesc": "_5KEEsG_pendingOptionDesc",
|
|
1739
2185
|
"writeNotice": "_5KEEsG_writeNotice",
|
|
1740
2186
|
"input": "_5KEEsG_input",
|
|
2187
|
+
"userRow": "_5KEEsG_userRow",
|
|
2188
|
+
"sidechat-pulse": "_5KEEsG_sidechat-pulse",
|
|
2189
|
+
"empty": "_5KEEsG_empty",
|
|
2190
|
+
"emptyIcon": "_5KEEsG_emptyIcon",
|
|
2191
|
+
"toolCard": "_5KEEsG_toolCard",
|
|
2192
|
+
"composerCard": "_5KEEsG_composerCard",
|
|
2193
|
+
"toolError": "_5KEEsG_toolError",
|
|
2194
|
+
"attachImg": "_5KEEsG_attachImg",
|
|
2195
|
+
"pendingQHeader": "_5KEEsG_pendingQHeader",
|
|
2196
|
+
"pendingActions": "_5KEEsG_pendingActions",
|
|
2197
|
+
"toolHead": "_5KEEsG_toolHead",
|
|
1741
2198
|
"sentChipRow": "_5KEEsG_sentChipRow",
|
|
1742
|
-
"
|
|
1743
|
-
"
|
|
1744
|
-
"
|
|
2199
|
+
"pendingBtnPrimary": "_5KEEsG_pendingBtnPrimary",
|
|
2200
|
+
"dotPending": "_5KEEsG_dotPending",
|
|
2201
|
+
"rowActions": "_5KEEsG_rowActions",
|
|
2202
|
+
"transcript": "_5KEEsG_transcript",
|
|
2203
|
+
"modelLabel": "_5KEEsG_modelLabel",
|
|
1745
2204
|
"flowRow": "_5KEEsG_flowRow",
|
|
1746
|
-
"
|
|
2205
|
+
"toolBodyText": "_5KEEsG_toolBodyText",
|
|
2206
|
+
"toolGenericBody": "_5KEEsG_toolGenericBody",
|
|
1747
2207
|
"composerChips": "_5KEEsG_composerChips",
|
|
1748
|
-
"
|
|
1749
|
-
"
|
|
1750
|
-
"
|
|
1751
|
-
"
|
|
1752
|
-
"
|
|
1753
|
-
"
|
|
1754
|
-
"
|
|
1755
|
-
"
|
|
1756
|
-
"root": "_5KEEsG_root",
|
|
1757
|
-
"emptyText": "_5KEEsG_emptyText",
|
|
1758
|
-
"slashGroupTitle": "_5KEEsG_slashGroupTitle",
|
|
1759
|
-
"toolCard": "_5KEEsG_toolCard",
|
|
2208
|
+
"attachRail": "_5KEEsG_attachRail",
|
|
2209
|
+
"pendingQText": "_5KEEsG_pendingQText",
|
|
2210
|
+
"pendingOption": "_5KEEsG_pendingOption",
|
|
2211
|
+
"dotIdle": "_5KEEsG_dotIdle",
|
|
2212
|
+
"stateScreen": "_5KEEsG_stateScreen",
|
|
2213
|
+
"attachRemove": "_5KEEsG_attachRemove",
|
|
2214
|
+
"dotRunning": "_5KEEsG_dotRunning",
|
|
2215
|
+
"actionGroup": "_5KEEsG_actionGroup",
|
|
1760
2216
|
"assistantBody": "_5KEEsG_assistantBody",
|
|
1761
|
-
"
|
|
1762
|
-
"
|
|
1763
|
-
"
|
|
1764
|
-
"
|
|
1765
|
-
"toolBodyText": "_5KEEsG_toolBodyText",
|
|
1766
|
-
"pendingCard": "_5KEEsG_pendingCard",
|
|
2217
|
+
"todoText": "_5KEEsG_todoText",
|
|
2218
|
+
"pendingReason": "_5KEEsG_pendingReason",
|
|
2219
|
+
"root": "_5KEEsG_root",
|
|
2220
|
+
"pendingOptionActive": "_5KEEsG_pendingOptionActive",
|
|
1767
2221
|
"parentStrip": "_5KEEsG_parentStrip",
|
|
1768
|
-
"
|
|
1769
|
-
"
|
|
2222
|
+
"toolRunning": "_5KEEsG_toolRunning",
|
|
2223
|
+
"modelSelect": "_5KEEsG_modelSelect",
|
|
2224
|
+
"pendingDesc": "_5KEEsG_pendingDesc",
|
|
2225
|
+
"body": "_5KEEsG_body",
|
|
2226
|
+
"densityToggle": "_5KEEsG_densityToggle",
|
|
1770
2227
|
"headerSide": "_5KEEsG_headerSide",
|
|
1771
|
-
"
|
|
1772
|
-
"
|
|
1773
|
-
"
|
|
1774
|
-
"
|
|
1775
|
-
"
|
|
1776
|
-
"
|
|
1777
|
-
"sentChip": "_5KEEsG_sentChip",
|
|
2228
|
+
"reflowButton": "_5KEEsG_reflowButton",
|
|
2229
|
+
"emptyText": "_5KEEsG_emptyText",
|
|
2230
|
+
"densityRow": "_5KEEsG_densityRow",
|
|
2231
|
+
"jumpBottom": "_5KEEsG_jumpBottom",
|
|
2232
|
+
"inheritedBody": "_5KEEsG_inheritedBody",
|
|
2233
|
+
"reasoning": "_5KEEsG_reasoning",
|
|
1778
2234
|
"pendingBar": "_5KEEsG_pendingBar",
|
|
1779
|
-
"
|
|
1780
|
-
"
|
|
2235
|
+
"toolBody": "_5KEEsG_toolBody",
|
|
2236
|
+
"writeNoticeClose": "_5KEEsG_writeNoticeClose",
|
|
2237
|
+
"slashItemActive": "_5KEEsG_slashItemActive",
|
|
2238
|
+
"slashMenu": "_5KEEsG_slashMenu",
|
|
2239
|
+
"todoBody": "_5KEEsG_todoBody",
|
|
2240
|
+
"streamingHint": "_5KEEsG_streamingHint",
|
|
2241
|
+
"rowPreview": "_5KEEsG_rowPreview",
|
|
2242
|
+
"slashItem": "_5KEEsG_slashItem",
|
|
2243
|
+
"slashItemDetail": "_5KEEsG_slashItemDetail",
|
|
1781
2244
|
"attachThumb": "_5KEEsG_attachThumb",
|
|
1782
|
-
"
|
|
2245
|
+
"pendingHead": "_5KEEsG_pendingHead",
|
|
2246
|
+
"slashItemName": "_5KEEsG_slashItemName",
|
|
2247
|
+
"assistantRow": "_5KEEsG_assistantRow"
|
|
1783
2248
|
};
|
|
1784
2249
|
//#endregion
|
|
1785
2250
|
//#region src/client/chat/ToolCard.tsx
|
|
@@ -2258,266 +2723,71 @@ window.__ModuleLoader__.load({
|
|
|
2258
2723
|
className: sidechat_module_css_default.toolBody,
|
|
2259
2724
|
children: message.text
|
|
2260
2725
|
})]
|
|
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
|
-
}
|
|
2726
|
+
});
|
|
2727
|
+
case "error": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2728
|
+
className: sidechat_module_css_default.errorRow,
|
|
2729
|
+
children: message.text
|
|
2730
|
+
});
|
|
2731
|
+
case "notice": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2732
|
+
className: sidechat_module_css_default.noticeRow,
|
|
2733
|
+
children: message.text
|
|
2372
2734
|
});
|
|
2373
|
-
return true;
|
|
2374
|
-
} catch (error) {
|
|
2375
|
-
console.warn("[dsh-sidenote] 重开侧边聊天失败:", error);
|
|
2376
|
-
return false;
|
|
2377
2735
|
}
|
|
2736
|
+
}, rowPropsEqual);
|
|
2737
|
+
/** memo 比较器:transcriptOf 每次快照重建消息对象(引用必变),按字段值比。 */
|
|
2738
|
+
function rowPropsEqual(prev, next) {
|
|
2739
|
+
if (prev.fold !== next.fold || prev.reflow !== next.reflow) return false;
|
|
2740
|
+
if (prev.parentSessionId !== next.parentSessionId || prev.sideTitle !== next.sideTitle) return false;
|
|
2741
|
+
if (prev.question !== next.question) return false;
|
|
2742
|
+
const a = prev.message;
|
|
2743
|
+
const b = next.message;
|
|
2744
|
+
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;
|
|
2378
2745
|
}
|
|
2379
2746
|
//#endregion
|
|
2380
|
-
//#region src/client/sidechat/
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
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 } : {}
|
|
2419
|
-
});
|
|
2420
|
-
}
|
|
2421
|
-
} catch (error) {
|
|
2422
|
-
console.warn("[dsh-sidenote] 同步主会话模型失败(子会话用默认模型):", error);
|
|
2423
|
-
}
|
|
2424
|
-
return forked;
|
|
2425
|
-
}
|
|
2426
|
-
/** Tab meta 读-改-写(布局持久化寄存处);tab 已消失时 no-op。 */
|
|
2427
|
-
function updateTabMeta(ctx, tabId, mutate) {
|
|
2428
|
-
const current = parseSideChatMeta(readTab(ctx, tabId)?.meta);
|
|
2429
|
-
ctx.betterSidebar.updateTab(tabId, { meta: mutate(current) });
|
|
2430
|
-
}
|
|
2431
|
-
/**
|
|
2432
|
-
* 程序化入口(/side、bridge 划选提问)打开 Tab 时面板可能处于折叠态——
|
|
2433
|
-
* 类型型 openTab 不自动展开(better-sidebar 只对 path/url 内容型打开展开)。
|
|
2434
|
-
* 幂等展开(off-face:store.update 是 SidebarStore 公开 mutator 面;
|
|
2435
|
-
* feature-check + 吞错,兜底失败不影响功能)。
|
|
2436
|
-
*/
|
|
2437
|
-
function ensurePanelOpen(store) {
|
|
2438
|
-
const mutable = store;
|
|
2439
|
-
try {
|
|
2440
|
-
mutable?.update?.((state) => {
|
|
2441
|
-
if (state.panelOpen === false) state.panelOpen = true;
|
|
2442
|
-
});
|
|
2443
|
-
} catch {}
|
|
2444
|
-
}
|
|
2445
|
-
/**
|
|
2446
|
-
* 打开非 staged 会话的事件窗口(off-face:open() 在 concrete Session 上是
|
|
2447
|
-
* public 且幂等,但不在 SessionFace 契约上——契约面只有 staged 会话会被
|
|
2448
|
-
* 运行时自动 open)。feature-check + 吞错,运行时若移除则降级为只发不收。
|
|
2449
|
-
*/
|
|
2450
|
-
function openSessionWindow(session) {
|
|
2451
|
-
const openable = session;
|
|
2452
|
-
if (typeof openable?.open !== "function") return;
|
|
2453
|
-
openable.open().catch((error) => {
|
|
2454
|
-
console.warn("[dsh-sidenote] 会话窗口打开失败:", error);
|
|
2455
|
-
});
|
|
2456
|
-
}
|
|
2457
|
-
/** 读子会话当前模型名(composer 模型标签用);任何失败回退 null(默认文案)。 */
|
|
2458
|
-
async function readModelName(ctx, sessionId) {
|
|
2747
|
+
//#region src/client/sidechat/recentClosed.ts
|
|
2748
|
+
const KEY_PREFIX = "dsh-sidenote:closed-side:v1:";
|
|
2749
|
+
const CAP = 5;
|
|
2750
|
+
function readAll(storageKey) {
|
|
2459
2751
|
try {
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2752
|
+
const raw = localStorage.getItem(storageKey);
|
|
2753
|
+
if (raw === null) return [];
|
|
2754
|
+
const parsed = JSON.parse(raw);
|
|
2755
|
+
if (!Array.isArray(parsed)) return [];
|
|
2756
|
+
return parsed.filter((e) => typeof e === "object" && e !== null && typeof e.childId === "string" && typeof e.parentSessionId === "string" && typeof e.title === "string");
|
|
2462
2757
|
} catch {
|
|
2463
|
-
return
|
|
2758
|
+
return [];
|
|
2464
2759
|
}
|
|
2465
2760
|
}
|
|
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;
|
|
2761
|
+
function recordClosedSideChat(parentSessionId, entry) {
|
|
2476
2762
|
try {
|
|
2477
|
-
const
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
getLegacy: () => {
|
|
2481
|
-
return target.getSnapshot()?.legacy ?? null;
|
|
2482
|
-
}
|
|
2483
|
-
};
|
|
2763
|
+
const key = KEY_PREFIX + parentSessionId;
|
|
2764
|
+
const list = [entry, ...readAll(key).filter((e) => e.childId !== entry.childId)].slice(0, CAP);
|
|
2765
|
+
localStorage.setItem(key, JSON.stringify(list));
|
|
2484
2766
|
} 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
2767
|
}
|
|
2491
|
-
/**
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
return res.result.ok ? res.result.value : null;
|
|
2496
|
-
} catch {
|
|
2497
|
-
return null;
|
|
2498
|
-
}
|
|
2768
|
+
/** 读某主会话的最近关闭列表(新者在前)。 */
|
|
2769
|
+
function listClosedSideChats(parentSessionId) {
|
|
2770
|
+
if (typeof localStorage === "undefined") return [];
|
|
2771
|
+
return readAll(KEY_PREFIX + parentSessionId);
|
|
2499
2772
|
}
|
|
2500
|
-
/**
|
|
2501
|
-
|
|
2773
|
+
/** 重开消费:从列表移除(重开后若再关会重新入列)。 */
|
|
2774
|
+
function dropClosedSideChat(parentSessionId, childId) {
|
|
2502
2775
|
try {
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
} catch {
|
|
2509
|
-
return null;
|
|
2510
|
-
}
|
|
2776
|
+
const key = KEY_PREFIX + parentSessionId;
|
|
2777
|
+
const list = readAll(key).filter((e) => e.childId !== childId);
|
|
2778
|
+
if (list.length === 0) localStorage.removeItem(key);
|
|
2779
|
+
else localStorage.setItem(key, JSON.stringify(list));
|
|
2780
|
+
} catch {}
|
|
2511
2781
|
}
|
|
2512
2782
|
//#endregion
|
|
2513
2783
|
//#region src/client/sidechat/ModelMenu.tsx
|
|
2514
2784
|
/**
|
|
2515
2785
|
* 模型选择器(WI-02):composer 脚部的模型 chip → 两级 Menu(provider 分组
|
|
2516
|
-
* 标签 +
|
|
2517
|
-
*
|
|
2786
|
+
* 标签 + 型号行)。数据面走 lifecycle.ts 的双版本链(0.1.5 的
|
|
2787
|
+
* remote.session.modelCatalog/selectModel 优先,<= 0.1.2 的
|
|
2788
|
+
* connection.api.sessions 回退);切的是子会话自己的选择,不回写主会话。
|
|
2518
2789
|
*
|
|
2519
|
-
* 懒加载:首次打开才拉目录(
|
|
2520
|
-
* (selectModel 返回的 selected 为准)。
|
|
2790
|
+
* 懒加载:首次打开才拉目录(catalog RPC 有成本);切换后本地标签立即更新。
|
|
2521
2791
|
*/
|
|
2522
2792
|
function ModelMenu(props) {
|
|
2523
2793
|
useLocaleTick();
|
|
@@ -3437,6 +3707,47 @@ window.__ModuleLoader__.load({
|
|
|
3437
3707
|
ctx,
|
|
3438
3708
|
tab.id
|
|
3439
3709
|
]);
|
|
3710
|
+
const composerRef = (0, react.useRef)(composer);
|
|
3711
|
+
composerRef.current = composer;
|
|
3712
|
+
const metaRef = (0, react.useRef)(tab.meta);
|
|
3713
|
+
metaRef.current = tab.meta;
|
|
3714
|
+
const phaseRef = (0, react.useRef)(phase);
|
|
3715
|
+
phaseRef.current = phase;
|
|
3716
|
+
const titleRef = (0, react.useRef)(tab.title);
|
|
3717
|
+
titleRef.current = tab.title;
|
|
3718
|
+
(0, react.useEffect)(() => {
|
|
3719
|
+
const metaNow = parseSideChatMeta(metaRef.current);
|
|
3720
|
+
if (nativeSidebarHost(ctx) && metaNow.childId !== void 0) dropClosedSideChat(scope.sessionId, metaNow.childId);
|
|
3721
|
+
const dispose = registerLiveSideChat(scope.sessionId, tab.id, (text) => {
|
|
3722
|
+
if (phaseRef.current !== "chat") {
|
|
3723
|
+
updateTabMeta(ctx, tab.id, (m) => ({
|
|
3724
|
+
...m,
|
|
3725
|
+
pendingDraft: appendDraftText(m.pendingDraft ?? "", text)
|
|
3726
|
+
}));
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
composerRef.current.appendDraft(text);
|
|
3730
|
+
requestAnimationFrame(() => {
|
|
3731
|
+
rootRef.current?.querySelector("textarea")?.focus();
|
|
3732
|
+
});
|
|
3733
|
+
}, () => metaRef.current);
|
|
3734
|
+
return () => {
|
|
3735
|
+
dispose();
|
|
3736
|
+
if (nativeSidebarHost(ctx)) {
|
|
3737
|
+
const m = parseSideChatMeta(metaRef.current);
|
|
3738
|
+
if (m.childId !== void 0 && m.parentSessionId !== void 0) recordClosedSideChat(m.parentSessionId, {
|
|
3739
|
+
childId: m.childId,
|
|
3740
|
+
parentSessionId: m.parentSessionId,
|
|
3741
|
+
title: titleRef.current,
|
|
3742
|
+
closedAt: Date.now()
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
};
|
|
3746
|
+
}, [
|
|
3747
|
+
scope.sessionId,
|
|
3748
|
+
tab.id,
|
|
3749
|
+
ctx
|
|
3750
|
+
]);
|
|
3440
3751
|
const promote = (0, react.useCallback)(() => {
|
|
3441
3752
|
if (childId === void 0) return;
|
|
3442
3753
|
(async () => {
|
|
@@ -3607,42 +3918,6 @@ window.__ModuleLoader__.load({
|
|
|
3607
3918
|
});
|
|
3608
3919
|
}
|
|
3609
3920
|
//#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
3921
|
//#region src/client/sidechat/header.tsx
|
|
3647
3922
|
/**
|
|
3648
3923
|
* 顶栏「侧边」入口(Delivery_02 Workitem_02 发现性):注册进宿主的
|
|
@@ -3684,11 +3959,16 @@ window.__ModuleLoader__.load({
|
|
|
3684
3959
|
}
|
|
3685
3960
|
//#endregion
|
|
3686
3961
|
//#region src/client/sidechat/slash.ts
|
|
3962
|
+
/** 0.1.5 信号:`connection.api.sessions` 面在 0.1.5 被删(与模型面迁移同判据)。 */
|
|
3963
|
+
function isDsh015Plus(ctx) {
|
|
3964
|
+
const legacy = ctx.connection?.api?.sessions;
|
|
3965
|
+
return legacy === void 0 || legacy === null;
|
|
3966
|
+
}
|
|
3687
3967
|
/** 单个命令名对应的完整贡献(side / 侧边 共用)。 */
|
|
3688
3968
|
function makeContribution(ctx, name) {
|
|
3689
3969
|
return {
|
|
3690
3970
|
name,
|
|
3691
|
-
description: t("cmdDesc"),
|
|
3971
|
+
description: isDsh015Plus(ctx) ? () => t("cmdDesc") : t("cmdDesc"),
|
|
3692
3972
|
available: (session) => canForkFrom(ctx, session.sessionId),
|
|
3693
3973
|
ui: {
|
|
3694
3974
|
kind: "popupSelect",
|
|
@@ -4389,43 +4669,43 @@ window.__ModuleLoader__.load({
|
|
|
4389
4669
|
document.head.appendChild(tag);
|
|
4390
4670
|
}
|
|
4391
4671
|
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
4672
|
"editorNew": "_SVViG_editorNew",
|
|
4401
|
-
"badge": "_SVViG_badge",
|
|
4402
|
-
"editorAskRow": "_SVViG_editorAskRow",
|
|
4403
4673
|
"chipText": "_SVViG_chipText",
|
|
4404
|
-
"
|
|
4405
|
-
"
|
|
4406
|
-
"
|
|
4407
|
-
"
|
|
4408
|
-
"chipPanel": "_SVViG_chipPanel",
|
|
4409
|
-
"chipNote": "_SVViG_chipNote",
|
|
4674
|
+
"toolbarBelow": "_SVViG_toolbarBelow",
|
|
4675
|
+
"sentCardTitle": "_SVViG_sentCardTitle",
|
|
4676
|
+
"chipClearAll": "_SVViG_chipClearAll",
|
|
4677
|
+
"chip": "_SVViG_chip",
|
|
4410
4678
|
"highlight": "_SVViG_highlight",
|
|
4679
|
+
"chipNote": "_SVViG_chipNote",
|
|
4680
|
+
"chipRemove": "_SVViG_chipRemove",
|
|
4681
|
+
"editorEdit": "_SVViG_editorEdit",
|
|
4682
|
+
"sentCardNote": "_SVViG_sentCardNote",
|
|
4683
|
+
"editorFooter": "_SVViG_editorFooter",
|
|
4684
|
+
"toolbar": "_SVViG_toolbar",
|
|
4411
4685
|
"dshSidenoteHintIn": "_SVViG_dshSidenoteHintIn",
|
|
4412
|
-
"sentChipRow": "_SVViG_sentChipRow",
|
|
4413
|
-
"hintClose": "_SVViG_hintClose",
|
|
4414
|
-
"chipNumber": "_SVViG_chipNumber",
|
|
4415
|
-
"saveButton": "_SVViG_saveButton",
|
|
4416
|
-
"chip": "_SVViG_chip",
|
|
4417
|
-
"chipRow": "_SVViG_chipRow",
|
|
4418
|
-
"sentChip": "_SVViG_sentChip",
|
|
4419
4686
|
"sentCardQuote": "_SVViG_sentCardQuote",
|
|
4420
|
-
"hint": "_SVViG_hint",
|
|
4421
4687
|
"editorInput": "_SVViG_editorInput",
|
|
4422
|
-
"
|
|
4688
|
+
"deleteButton": "_SVViG_deleteButton",
|
|
4689
|
+
"badge": "_SVViG_badge",
|
|
4423
4690
|
"confirmButton": "_SVViG_confirmButton",
|
|
4424
|
-
"
|
|
4425
|
-
"
|
|
4691
|
+
"saveButton": "_SVViG_saveButton",
|
|
4692
|
+
"badgeSent": "_SVViG_badgeSent",
|
|
4693
|
+
"sentChip": "_SVViG_sentChip",
|
|
4694
|
+
"chipNumber": "_SVViG_chipNumber",
|
|
4695
|
+
"hint": "_SVViG_hint",
|
|
4696
|
+
"hintClose": "_SVViG_hintClose",
|
|
4426
4697
|
"editorTarget": "_SVViG_editorTarget",
|
|
4427
|
-
"
|
|
4428
|
-
"
|
|
4698
|
+
"chipWrap": "_SVViG_chipWrap",
|
|
4699
|
+
"cancelButton": "_SVViG_cancelButton",
|
|
4700
|
+
"editorError": "_SVViG_editorError",
|
|
4701
|
+
"editorAskRow": "_SVViG_editorAskRow",
|
|
4702
|
+
"sentChipRow": "_SVViG_sentChipRow",
|
|
4703
|
+
"editorTextarea": "_SVViG_editorTextarea",
|
|
4704
|
+
"editorAsk": "_SVViG_editorAsk",
|
|
4705
|
+
"chipRow": "_SVViG_chipRow",
|
|
4706
|
+
"chipPanel": "_SVViG_chipPanel",
|
|
4707
|
+
"toolbarButton": "_SVViG_toolbarButton",
|
|
4708
|
+
"editorSpacer": "_SVViG_editorSpacer"
|
|
4429
4709
|
};
|
|
4430
4710
|
//#endregion
|
|
4431
4711
|
//#region src/client/annotate/hint.tsx
|
|
@@ -5537,6 +5817,7 @@ window.__ModuleLoader__.load({
|
|
|
5537
5817
|
"locale"
|
|
5538
5818
|
];
|
|
5539
5819
|
function apply(ctx) {
|
|
5820
|
+
setRootContext(ctx);
|
|
5540
5821
|
attachLocale(ctx.locale);
|
|
5541
5822
|
probeHost(ctx);
|
|
5542
5823
|
const reflow = createReflowStore();
|