dsh-trivium 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -26,7 +26,15 @@ window.__ModuleLoader__.load({
26
26
  recallAnchor: "实体名折中",
27
27
  recallAnchorHint: "用户文本命中已有 entity 的 name/aliases 时,注入该实体 1 跳 about/decided/broke/fixed 邻居(≤300 token)。",
28
28
  extract: "compaction/end 抽取",
29
- extractHint: "规则 + 一次小 prompt;失败只记日志。turn 结束后约 12 秒空闲会再抽一次,下次 session-start 也会重放 pending。",
29
+ extractHint: "规则 + 一次小 prompt;失败只记日志。turn 结束后约 12 秒空闲会再抽一次,下次 session-start 也会重放 pending。每轮写入正文合计 ≤3000 字、最多 24 条。",
30
+ markdown: "Markdown 投影",
31
+ markdownHint: "只读导出,带边(- 鉴权走 header X —about→ AuthGateway)。给编辑器和 git 看。改完回写用 JSON 导入或本页编辑,不解析自由散文。",
32
+ exportMd: "导出 Markdown",
33
+ workbuddy: "WorkBuddy 一次性导入",
34
+ workbuddyHint: "只读 ~/.workbuddy/MEMORY.md 与工作区 .workbuddy/memory/MEMORY.md。偏严宁漏:闲聊、一次性改文件、密钥、无规则线索的段落跳过。不持续同步。每批同样 ≤3000 字 / 24 条。",
35
+ workbuddyImport: "导入找到的 MEMORY.md",
36
+ workbuddyNone: "当前没有找到这些文件。",
37
+ workbuddyFound: "找到",
30
38
  embed: "远程 embedding(OpenAI 兼容,默认关)",
31
39
  embedHint: "失败退回关键词 + 图检索。需单独的 embeddings endpoint;官方 DeepSeek chat API 不含 embedding。",
32
40
  embedReady: "已就绪",
@@ -143,6 +151,8 @@ window.__ModuleLoader__.load({
143
151
  const [edits, setEdits] = react.useState({});
144
152
  const [mergeFrom, setMergeFrom] = react.useState({});
145
153
  const fileRef = react.useRef(null);
154
+ const [buddyFiles, setBuddyFiles] = react.useState([]);
155
+ const [buddyNote, setBuddyNote] = react.useState("");
146
156
 
147
157
  const load = react.useCallback(async () => {
148
158
  setError("");
@@ -163,6 +173,8 @@ window.__ModuleLoader__.load({
163
173
  const qs = params.toString();
164
174
  const list = await api("/api/dsh-trivium/nodes" + (qs ? "?" + qs : ""));
165
175
  setNodes(list.nodes || []);
176
+ const wb = await api("/api/dsh-trivium/workbuddy");
177
+ setBuddyFiles(wb.files || []);
166
178
  } catch (err) {
167
179
  setError(String(err.message || err));
168
180
  }
@@ -277,6 +289,56 @@ window.__ModuleLoader__.load({
277
289
  }
278
290
  };
279
291
 
292
+ const exportMd = async () => {
293
+ try {
294
+ const resp = await fetch("/api/dsh-trivium/export.md");
295
+ const text = await resp.text();
296
+ if (!resp.ok) {
297
+ let message = "HTTP " + resp.status;
298
+ try {
299
+ message = JSON.parse(text).message || message;
300
+ } catch {
301
+ // keep
302
+ }
303
+ throw new Error(message);
304
+ }
305
+ const blob = new Blob([text], { type: "text/markdown;charset=utf-8" });
306
+ const a = document.createElement("a");
307
+ a.href = URL.createObjectURL(blob);
308
+ a.download = "MEMORY.md";
309
+ a.click();
310
+ URL.revokeObjectURL(a.href);
311
+ } catch (err) {
312
+ setError(String(err.message || err));
313
+ }
314
+ };
315
+
316
+ const importBuddy = async () => {
317
+ setBusy(true);
318
+ setBuddyNote("");
319
+ try {
320
+ const r = await api("/api/dsh-trivium/import-workbuddy", {
321
+ method: "POST",
322
+ headers: { "content-type": "application/json" },
323
+ body: "{}",
324
+ });
325
+ setSaved(true);
326
+ setBuddyNote(
327
+ "导入 " +
328
+ (r.created || 0) +
329
+ " 新建 / " +
330
+ (r.merged || 0) +
331
+ " 合并,跳过 " +
332
+ (r.skipped || 0),
333
+ );
334
+ await load();
335
+ } catch (err) {
336
+ setError(String(err.message || err));
337
+ } finally {
338
+ setBusy(false);
339
+ }
340
+ };
341
+
280
342
  const importJson = async (file) => {
281
343
  if (!file) return;
282
344
  setBusy(true);
@@ -358,6 +420,7 @@ window.__ModuleLoader__.load({
358
420
  }),
359
421
  jsx(Button, { variant: "outline", size: "sm", onClick: load, children: L.refresh }),
360
422
  jsx(Button, { variant: "outline", size: "sm", onClick: exportJson, children: L.exportJson }),
423
+ jsx(Button, { variant: "outline", size: "sm", onClick: exportMd, children: L.exportMd }),
361
424
  jsx(Button, {
362
425
  variant: "outline",
363
426
  size: "sm",
@@ -425,6 +488,37 @@ window.__ModuleLoader__.load({
425
488
  jsx("div", { style: ui.hint, children: L.extractHint }),
426
489
  ],
427
490
  }),
491
+ jsxs("div", {
492
+ style: ui.card,
493
+ children: [
494
+ jsx("div", { style: ui.cardTitle, children: L.markdown }),
495
+ jsx("div", { style: ui.hint, children: L.markdownHint }),
496
+ jsx("div", { style: ui.cardTitle, children: L.workbuddy }),
497
+ jsx("div", { style: ui.hint, children: L.workbuddyHint }),
498
+ jsx("div", {
499
+ style: { ...ui.hint, wordBreak: "break-all" },
500
+ children: (buddyFiles || []).some((f) => f.exists)
501
+ ? L.workbuddyFound +
502
+ " " +
503
+ buddyFiles
504
+ .filter((f) => f.exists)
505
+ .map((f) => f.path)
506
+ .join(" · ")
507
+ : L.workbuddyNone,
508
+ }),
509
+ buddyNote ? jsx("div", { style: ui.saved, children: buddyNote }) : null,
510
+ jsx("div", {
511
+ style: { display: "flex", flexWrap: "wrap", gap: 8 },
512
+ children: jsx(Button, {
513
+ variant: "outline",
514
+ size: "sm",
515
+ disabled: busy || !(buddyFiles || []).some((f) => f.exists),
516
+ onClick: importBuddy,
517
+ children: L.workbuddyImport,
518
+ }),
519
+ }),
520
+ ],
521
+ }),
428
522
  jsxs("div", {
429
523
  style: ui.card,
430
524
  children: [
@@ -779,7 +873,1321 @@ window.__ModuleLoader__.load({
779
873
  });
780
874
  }
781
875
 
876
+ function relTime(iso) {
877
+ const t = Date.parse(iso);
878
+ if (!Number.isFinite(t)) return "";
879
+ const sec = Math.max(0, Math.round((Date.now() - t) / 1000));
880
+ if (sec < 60) return "刚刚";
881
+ if (sec < 3600) return Math.floor(sec / 60) + " 分钟前";
882
+ if (sec < 86400) return Math.floor(sec / 3600) + " 小时前";
883
+ return Math.floor(sec / 86400) + " 天前";
884
+ }
885
+
886
+ function kindLabel(kind, current) {
887
+ if (current || kind === "tail") return "后续";
888
+ if (kind === "fork") return "分支";
889
+ return "检查点";
890
+ }
891
+
892
+ const GENERIC_BOX_NAMES = { "当前": 1, "已压缩": 1, "分叉": 1, "分叉点": 1, "后续": 1, "分支": 1 };
893
+ const MAP_BOX_W = 220;
894
+ const MAP_ARROW_W = 96;
895
+ const MAP_PAD = 40;
896
+ const MAP_BOX_H = 120;
897
+ const MAP_KID_GAP = 16;
898
+ const MAP_KID_H = 96;
899
+ const MAP_FORK_WIRE = 28;
900
+ const MAP_COL_H = MAP_PAD * 2 + MAP_BOX_H + 2 * (MAP_KID_H + MAP_KID_GAP + MAP_FORK_WIRE);
901
+
902
+ let workspacesList = null;
903
+
904
+ function customBoxTitle(node) {
905
+ const name = String(node?.name || "").trim();
906
+ if (!name || GENERIC_BOX_NAMES[name]) return "";
907
+ const summary = String(node?.summary || "").replace(/\s+/g, " ").trim();
908
+ if (name === summary) return "";
909
+ if (summary.length > 48 && (name === summary.slice(0, 47) + "…" || name === summary.slice(0, 48))) return "";
910
+ return name;
911
+ }
912
+
913
+ function isGenericCopy(value) {
914
+ const text = String(value || "").replace(/\s+/g, " ").trim();
915
+ return !text || !!GENERIC_BOX_NAMES[text];
916
+ }
917
+
918
+ function proseOf(raw) {
919
+ return String(raw || "")
920
+ .replace(/```[\s\S]*?```/g, " ")
921
+ .replace(/^#{1,6}\s+/gm, "")
922
+ .replace(/\*\*/g, "")
923
+ .replace(/\s+/g, " ")
924
+ .trim();
925
+ }
926
+
927
+ function boxBody(node, livePreview) {
928
+ const custom = customBoxTitle(node);
929
+ let raw = proseOf(node?.summary);
930
+ if (isGenericCopy(raw)) raw = "";
931
+ if (!raw && livePreview) raw = proseOf(livePreview);
932
+ if (!raw && custom) raw = custom;
933
+ return raw;
934
+ }
935
+
936
+ function lastUserPreview(chat) {
937
+ const order = chat?.order || [];
938
+ const nodes = chat?.nodes;
939
+ if (!nodes || !order.length) return "";
940
+ for (let i = order.length - 1; i >= 0; i -= 1) {
941
+ const n = typeof nodes.get === "function" ? nodes.get(order[i]) : null;
942
+ if (!n || (n.kind !== "user" && n.kind !== "steering")) continue;
943
+ const data = n.data || {};
944
+ const blocks = data.blocks || data.content || data.message?.content;
945
+ let text = data.text || "";
946
+ if (!text && Array.isArray(blocks)) {
947
+ text = blocks
948
+ .map((b) => (typeof b === "string" ? b : b.text || b.content || ""))
949
+ .join(" ");
950
+ }
951
+ const s = proseOf(text);
952
+ if (s && !s.startsWith("/")) return s.slice(0, 240);
953
+ }
954
+ return "";
955
+ }
956
+
957
+ function chatAnchorKey(chat, node) {
958
+ const nodes = chat?.nodes;
959
+ if (!nodes || typeof nodes.values !== "function") return "";
960
+ const wantId = String(node?.compactionId || "");
961
+ const wantSeq = Number(node?.atSeq);
962
+ let best = "";
963
+ let bestDist = Infinity;
964
+ for (const n of nodes.values()) {
965
+ const data = n.data || {};
966
+ const id = String(data.id || data.compactionId || "");
967
+ const seq = Number(n.anchorSeq ?? data.seq);
968
+ if (wantId && id && id === wantId) return n.key;
969
+ if (n.kind === "compaction" || n.kind === "manual-compaction") {
970
+ if (Number.isFinite(seq) && Number.isFinite(wantSeq)) {
971
+ if (seq === wantSeq) return n.key;
972
+ const dist = Math.abs(seq - wantSeq);
973
+ if (dist < bestDist) {
974
+ bestDist = dist;
975
+ best = n.key;
976
+ }
977
+ }
978
+ }
979
+ }
980
+ return best;
981
+ }
982
+
983
+ function scrollConversation({ key, bottom }) {
984
+ let n = 0;
985
+ const tick = () => {
986
+ const host = document.querySelector("[data-conversation-scroll]");
987
+ if (bottom && host) {
988
+ host.scrollTop = host.scrollHeight;
989
+ return;
990
+ }
991
+ const root = host || document;
992
+ if (key) {
993
+ for (const row of root.querySelectorAll("[data-chat-anchor-key]")) {
994
+ if (row.dataset.chatAnchorKey === key) {
995
+ row.scrollIntoView({ block: "center", inline: "nearest" });
996
+ return;
997
+ }
998
+ }
999
+ }
1000
+ if (n < 24) {
1001
+ n += 1;
1002
+ setTimeout(tick, 50);
1003
+ }
1004
+ };
1005
+ setTimeout(tick, 40);
1006
+ }
1007
+
1008
+ function layoutChain(nodes, edges, sessionId, liveSessions) {
1009
+ const mine = (nodes || []).filter((n) => n.sessionId === sessionId);
1010
+ const checks = mine
1011
+ .filter((n) => n.kind === "checkpoint")
1012
+ .sort((a, b) => (a.atSeq || 0) - (b.atSeq || 0) || a.id - b.id);
1013
+ const tail = mine.find((n) => n.kind === "tail");
1014
+ const chain = tail ? checks.concat(tail) : checks;
1015
+ const byId = new Map((nodes || []).map((n) => [n.id, n]));
1016
+ const attachId = (cutId) => {
1017
+ const cut = byId.get(cutId);
1018
+ if (!cut || cut.kind !== "fork") return cutId;
1019
+ const seq = cut.atSeq || 0;
1020
+ const hit = [...checks].reverse().find((n) => (n.atSeq || 0) <= seq);
1021
+ return (hit || tail || cut).id;
1022
+ };
1023
+ const kids = new Map();
1024
+ for (const e of edges || []) {
1025
+ if (e.label !== "forks_from") continue;
1026
+ const child = byId.get(e.from);
1027
+ if (!child || child.sessionId === sessionId) continue;
1028
+ if (liveSessions && !liveSessions.has(String(child.sessionId))) continue;
1029
+ const to = attachId(e.to);
1030
+ const list = kids.get(to) || [];
1031
+ list.push(child);
1032
+ kids.set(to, list);
1033
+ }
1034
+ return { chain, kids };
1035
+ }
1036
+
1037
+ function increasedForkTitle(title) {
1038
+ const text = String(title || "").trim();
1039
+ if (!text) return "分叉 (1)";
1040
+ const ascii = /^(.*?)\((\d+)\)$/u.exec(text);
1041
+ if (ascii?.[1] != null && ascii[2] != null) return ascii[1] + "(" + (Number(ascii[2]) + 1) + ")";
1042
+ const fullWidth = /^(.*?)((\d+))$/u.exec(text);
1043
+ if (fullWidth?.[1] != null && fullWidth[2] != null) return fullWidth[1] + "(" + (Number(fullWidth[2]) + 1) + ")";
1044
+ return text + " (1)";
1045
+ }
1046
+
1047
+ function nextForkTitle(byId, sourceId) {
1048
+ const row = byId?.[sourceId] || {};
1049
+ return increasedForkTitle(row.title || row.displayTitle || "");
1050
+ }
1051
+
1052
+ function boxKicker(node, { current, child, sessionTitle }) {
1053
+ if (child) return sessionTitle || "分叉";
1054
+ if (current || node?.kind === "tail") return "后续";
1055
+ if (node?.atSeq != null && Number(node.atSeq) > 0) return "seq " + node.atSeq;
1056
+ return kindLabel(node?.kind, current);
1057
+ }
1058
+
1059
+ function splitKids(list) {
1060
+ const above = [];
1061
+ const below = [];
1062
+ (list || []).forEach((kid, i) => {
1063
+ if (i % 2 === 0) above.push(kid);
1064
+ else below.push(kid);
1065
+ });
1066
+ return { above, below };
1067
+ }
1068
+
1069
+ function chainWorldWidth(len) {
1070
+ const n = Math.max(1, len);
1071
+ return MAP_PAD * 2 + n * MAP_BOX_W + Math.max(0, n - 1) * MAP_ARROW_W;
1072
+ }
1073
+
1074
+ function minPanX(chainLen, canvasW, scale) {
1075
+ return Math.min(0, (canvasW || 640) - chainWorldWidth(chainLen) * scale);
1076
+ }
1077
+
1078
+ function centerPan(chainLen, canvas, scale) {
1079
+ const cw = canvas?.clientWidth || 640;
1080
+ const ch = canvas?.clientHeight || 260;
1081
+ const worldW = chainWorldWidth(Math.max(1, chainLen));
1082
+ const worldH = MAP_COL_H;
1083
+ return {
1084
+ x: (cw - worldW * scale) / 2,
1085
+ y: (ch - worldH * scale) / 2,
1086
+ };
1087
+ }
1088
+
1089
+ function panToForkChild(nodes, edges, parentSessionId, childSessionId, canvas, scale, liveSessions) {
1090
+ const { chain, kids } = layoutChain(nodes, edges, parentSessionId, liveSessions);
1091
+ let col = Math.max(0, chain.length - 1);
1092
+ let foundKid = false;
1093
+ let split = { above: [], below: [] };
1094
+ for (let i = 0; i < chain.length; i += 1) {
1095
+ const list = kids.get(chain[i].id) || [];
1096
+ if (list.some((k) => k.sessionId === childSessionId)) {
1097
+ col = i;
1098
+ foundKid = true;
1099
+ split = splitKids(list);
1100
+ break;
1101
+ }
1102
+ }
1103
+ const cw = canvas?.clientWidth || 640;
1104
+ const ch = canvas?.clientHeight || 260;
1105
+ const x = MAP_PAD + col * (MAP_BOX_W + MAP_ARROW_W);
1106
+ const midY = MAP_PAD + (MAP_COL_H - MAP_PAD * 2 - MAP_BOX_H) / 2;
1107
+ const aboveH = foundKid && split.above.length ? MAP_KID_H + MAP_KID_GAP + MAP_FORK_WIRE : 0;
1108
+ const belowH = foundKid && split.below.length ? MAP_KID_H + MAP_KID_GAP + MAP_FORK_WIRE : 0;
1109
+ const y = midY - aboveH;
1110
+ const w = foundKid ? 188 : MAP_BOX_W;
1111
+ const h = MAP_BOX_H + aboveH + belowH;
1112
+ let nextScale = scale;
1113
+ if (w * nextScale > cw - 32) nextScale = Math.max(0.35, (cw - 32) / w);
1114
+ if ((y + h) * nextScale > ch - 24) {
1115
+ nextScale = Math.max(0.35, Math.min(nextScale, (ch - 24) / (y + h)));
1116
+ }
1117
+ const left = x * nextScale;
1118
+ const right = (x + w) * nextScale;
1119
+ const top = y * nextScale;
1120
+ const bottom = (y + h) * nextScale;
1121
+ let panX = 0;
1122
+ let panY = 0;
1123
+ if (right > cw - 16) panX = cw - 16 - right;
1124
+ if (left + panX < 16) panX = 16 - left;
1125
+ if (bottom > ch - 16) panY = ch - 16 - bottom;
1126
+ if (top + panY < 16) panY = 16 - top;
1127
+ return { pan: { x: panX, y: panY }, scale: nextScale };
1128
+ }
1129
+
1130
+ function svgIcon(children) {
1131
+ return jsxs("svg", {
1132
+ width: 15,
1133
+ height: 15,
1134
+ viewBox: "0 0 16 16",
1135
+ fill: "none",
1136
+ stroke: "currentColor",
1137
+ strokeWidth: 1.6,
1138
+ strokeLinecap: "round",
1139
+ strokeLinejoin: "round",
1140
+ "aria-hidden": true,
1141
+ children,
1142
+ });
1143
+ }
1144
+
1145
+ function ForkIcon() {
1146
+ return svgIcon([
1147
+ jsx("circle", { cx: 3.2, cy: 8, r: 1.55, fill: "currentColor", stroke: "none" }, "a"),
1148
+ jsx("circle", { cx: 12.6, cy: 3.2, r: 1.55, fill: "currentColor", stroke: "none" }, "b"),
1149
+ jsx("circle", { cx: 12.6, cy: 12.8, r: 1.55, fill: "currentColor", stroke: "none" }, "c"),
1150
+ jsx("path", { d: "M4.7 8h2.1c1.6 0 2.4-.6 3.3-1.8L11.4 4.4" }, "d"),
1151
+ jsx("path", { d: "M6.8 8c1.6 0 2.4.6 3.3 1.8l1.3 1.8" }, "e"),
1152
+ ]);
1153
+ }
1154
+
1155
+ function TrajectoryIcon() {
1156
+ return svgIcon([
1157
+ jsx("path", { d: "M1.8 11.6h2.3l1.7-7.2h3.1l1.4 4.4H14" }, "a"),
1158
+ jsx("circle", { cx: 14, cy: 8.8, r: 1.25, fill: "currentColor", stroke: "none" }, "b"),
1159
+ ]);
1160
+ }
1161
+
1162
+ function ArrowH() {
1163
+ const w = MAP_ARROW_W;
1164
+ const y = 9;
1165
+ const head = w - 12;
1166
+ return jsxs("svg", {
1167
+ width: w,
1168
+ height: 18,
1169
+ viewBox: "0 0 " + w + " 18",
1170
+ "aria-hidden": true,
1171
+ children: [
1172
+ jsx("line", {
1173
+ x1: 1,
1174
+ y1: y,
1175
+ x2: head,
1176
+ y2: y,
1177
+ stroke: "currentColor",
1178
+ strokeWidth: 2.4,
1179
+ strokeLinecap: "round",
1180
+ }),
1181
+ jsx("polygon", {
1182
+ points: head + ",3 " + (w - 1) + "," + y + " " + head + ",15",
1183
+ fill: "currentColor",
1184
+ }),
1185
+ ],
1186
+ });
1187
+ }
1188
+
1189
+ function ArrowV({ dir }) {
1190
+ const down = dir !== "up";
1191
+ const h = MAP_FORK_WIRE;
1192
+ const x = 9;
1193
+ const head = h - 10;
1194
+ return jsxs("svg", {
1195
+ width: 18,
1196
+ height: h,
1197
+ viewBox: "0 0 18 " + h,
1198
+ "aria-hidden": true,
1199
+ style: { flexShrink: 0, transform: down ? undefined : "rotate(180deg)" },
1200
+ children: [
1201
+ jsx("line", {
1202
+ x1: x,
1203
+ y1: 1,
1204
+ x2: x,
1205
+ y2: head,
1206
+ stroke: "currentColor",
1207
+ strokeWidth: 2.4,
1208
+ strokeLinecap: "round",
1209
+ }),
1210
+ jsx("polygon", {
1211
+ points: "3," + head + " " + x + "," + (h - 1) + " 15," + head,
1212
+ fill: "currentColor",
1213
+ }),
1214
+ ],
1215
+ });
1216
+ }
1217
+
1218
+ function sharedChatStore(ctx) {
1219
+ try {
1220
+ const views = typeof ctx.slots.entries === "function" ? ctx.slots.entries("conversation.view") : [];
1221
+ const chat = (views || []).find((e) => e?.options?.id === "chat");
1222
+ if (chat?.store) return chat.store;
1223
+ const sessions = typeof ctx.slots.entries === "function" ? ctx.slots.entries("conversation.session") : [];
1224
+ return sessions?.[0]?.store;
1225
+ } catch {
1226
+ return undefined;
1227
+ }
1228
+ }
1229
+
1230
+ const mapUi = {
1231
+ root: {
1232
+ display: "flex",
1233
+ flexDirection: "column",
1234
+ height: "100%",
1235
+ minHeight: 0,
1236
+ gap: 10,
1237
+ padding: "8px 4px 12px",
1238
+ position: "relative",
1239
+ },
1240
+ chipsHead: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, flexWrap: "wrap" },
1241
+ chipsTitle: { fontSize: 13, fontWeight: 650 },
1242
+ hint: { fontSize: 12, opacity: 0.62 },
1243
+ chips: { display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" },
1244
+ chip: {
1245
+ display: "flex",
1246
+ alignItems: "center",
1247
+ gap: 6,
1248
+ fontSize: 12,
1249
+ cursor: "pointer",
1250
+ border: "1px solid rgba(127,127,127,0.28)",
1251
+ borderRadius: 999,
1252
+ padding: "4px 10px",
1253
+ maxWidth: 280,
1254
+ },
1255
+ chipSuggest: { borderColor: "currentColor", background: "rgba(127,127,127,0.08)" },
1256
+ suggest: { fontSize: 10, fontWeight: 650, opacity: 0.78, flexShrink: 0 },
1257
+ search: {
1258
+ width: 180,
1259
+ boxSizing: "border-box",
1260
+ fontSize: 12,
1261
+ padding: "4px 8px",
1262
+ borderRadius: 8,
1263
+ border: "1px solid rgba(127,127,127,0.28)",
1264
+ background: "transparent",
1265
+ color: "inherit",
1266
+ },
1267
+ inherit: { display: "flex", alignItems: "center", gap: 6, fontSize: 12, cursor: "pointer", margin: 0 },
1268
+ titleRow: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 },
1269
+ title: {
1270
+ fontSize: 14,
1271
+ fontWeight: 700,
1272
+ lineHeight: 1.3,
1273
+ overflow: "hidden",
1274
+ textOverflow: "ellipsis",
1275
+ whiteSpace: "nowrap",
1276
+ minWidth: 0,
1277
+ flex: 1,
1278
+ },
1279
+ titleInput: {
1280
+ flex: 1,
1281
+ minWidth: 0,
1282
+ boxSizing: "border-box",
1283
+ fontSize: 13,
1284
+ fontWeight: 700,
1285
+ padding: "3px 6px",
1286
+ borderRadius: 6,
1287
+ border: "1px solid currentColor",
1288
+ background: "transparent",
1289
+ color: "inherit",
1290
+ },
1291
+ ghostBtn: {
1292
+ fontSize: 10,
1293
+ padding: "1px 6px",
1294
+ borderRadius: 6,
1295
+ border: "1px solid rgba(127,127,127,0.35)",
1296
+ background: "transparent",
1297
+ color: "inherit",
1298
+ cursor: "pointer",
1299
+ flexShrink: 0,
1300
+ opacity: 0.85,
1301
+ },
1302
+ iconRow: {
1303
+ position: "absolute",
1304
+ right: 8,
1305
+ bottom: 8,
1306
+ zIndex: 3,
1307
+ display: "flex",
1308
+ flexDirection: "row",
1309
+ alignItems: "center",
1310
+ gap: 6,
1311
+ pointerEvents: "auto",
1312
+ },
1313
+ iconBtn: {
1314
+ width: 32,
1315
+ height: 32,
1316
+ boxSizing: "border-box",
1317
+ padding: 0,
1318
+ borderRadius: 7,
1319
+ border: "1px solid rgba(127,127,127,0.35)",
1320
+ background: "rgba(127,127,127,0.12)",
1321
+ color: "inherit",
1322
+ cursor: "pointer",
1323
+ display: "flex",
1324
+ alignItems: "center",
1325
+ justifyContent: "center",
1326
+ flexShrink: 0,
1327
+ pointerEvents: "auto",
1328
+ position: "relative",
1329
+ zIndex: 4,
1330
+ },
1331
+ canvas: {
1332
+ flex: 1,
1333
+ minHeight: 260,
1334
+ overflow: "hidden",
1335
+ position: "relative",
1336
+ cursor: "grab",
1337
+ border: "1px solid rgba(127,127,127,0.22)",
1338
+ borderRadius: 10,
1339
+ touchAction: "none",
1340
+ },
1341
+ world: {
1342
+ transformOrigin: "0 0",
1343
+ display: "inline-flex",
1344
+ padding: "0 " + MAP_PAD + "px",
1345
+ alignItems: "stretch",
1346
+ gap: 0,
1347
+ },
1348
+ col: {
1349
+ display: "grid",
1350
+ gridTemplateRows: "minmax(0,1fr) auto minmax(0,1fr)",
1351
+ justifyItems: "center",
1352
+ minHeight: MAP_COL_H,
1353
+ width: MAP_BOX_W,
1354
+ },
1355
+ kidStack: {
1356
+ display: "flex",
1357
+ flexDirection: "column",
1358
+ alignItems: "center",
1359
+ justifyContent: "flex-end",
1360
+ gap: MAP_KID_GAP,
1361
+ paddingBottom: MAP_KID_GAP,
1362
+ width: "100%",
1363
+ minHeight: 0,
1364
+ },
1365
+ kidStackBelow: {
1366
+ display: "flex",
1367
+ flexDirection: "column",
1368
+ alignItems: "center",
1369
+ justifyContent: "flex-start",
1370
+ gap: MAP_KID_GAP,
1371
+ paddingTop: MAP_KID_GAP,
1372
+ width: "100%",
1373
+ minHeight: 0,
1374
+ },
1375
+ row: { display: "flex", alignItems: "stretch" },
1376
+ arrow: {
1377
+ display: "grid",
1378
+ gridTemplateRows: "minmax(0,1fr) auto minmax(0,1fr)",
1379
+ width: MAP_ARROW_W,
1380
+ flexShrink: 0,
1381
+ },
1382
+ arrowMid: {
1383
+ height: MAP_BOX_H,
1384
+ display: "flex",
1385
+ alignItems: "center",
1386
+ justifyContent: "center",
1387
+ color: "currentColor",
1388
+ },
1389
+ forkArrow: {
1390
+ display: "flex",
1391
+ alignItems: "center",
1392
+ justifyContent: "center",
1393
+ color: "currentColor",
1394
+ opacity: 0.92,
1395
+ },
1396
+ box: {
1397
+ width: 220,
1398
+ minHeight: MAP_BOX_H,
1399
+ position: "relative",
1400
+ borderRadius: 10,
1401
+ padding: "10px 12px 40px",
1402
+ border: "1px solid rgba(127,127,127,0.35)",
1403
+ cursor: "default",
1404
+ display: "flex",
1405
+ flexDirection: "column",
1406
+ gap: 4,
1407
+ background: "transparent",
1408
+ textAlign: "left",
1409
+ pointerEvents: "auto",
1410
+ zIndex: 1,
1411
+ },
1412
+ boxMain: {
1413
+ flex: 1,
1414
+ minWidth: 0,
1415
+ minHeight: 0,
1416
+ display: "flex",
1417
+ flexDirection: "column",
1418
+ gap: 4,
1419
+ cursor: "pointer",
1420
+ },
1421
+ boxCurrent: { boxShadow: "0 0 0 2px currentColor", borderColor: "currentColor" },
1422
+ boxKid: { width: 188, minHeight: MAP_KID_H, padding: "8px 10px 36px", opacity: 0.92 },
1423
+ kicker: { fontSize: 11, opacity: 0.62, fontWeight: 650 },
1424
+ summary: {
1425
+ fontSize: 13,
1426
+ lineHeight: 1.4,
1427
+ display: "-webkit-box",
1428
+ WebkitLineClamp: 3,
1429
+ WebkitBoxOrient: "vertical",
1430
+ overflow: "hidden",
1431
+ },
1432
+ error: { color: "#c44", fontSize: 13 },
1433
+ foldBtn: {
1434
+ fontSize: 12,
1435
+ padding: "3px 10px",
1436
+ borderRadius: 8,
1437
+ border: "1px solid rgba(127,127,127,0.35)",
1438
+ background: "transparent",
1439
+ color: "inherit",
1440
+ cursor: "pointer",
1441
+ flexShrink: 0,
1442
+ },
1443
+ overlay: {
1444
+ position: "absolute",
1445
+ inset: 0,
1446
+ zIndex: 20,
1447
+ display: "flex",
1448
+ alignItems: "center",
1449
+ justifyContent: "center",
1450
+ background: "rgba(0,0,0,0.28)",
1451
+ },
1452
+ dialog: {
1453
+ width: 360,
1454
+ maxWidth: "92%",
1455
+ borderRadius: 12,
1456
+ border: "1px solid rgba(127,127,127,0.35)",
1457
+ background: "var(--dsw-alias-bg-base, #fff)",
1458
+ padding: "14px 16px 12px",
1459
+ display: "flex",
1460
+ flexDirection: "column",
1461
+ gap: 10,
1462
+ boxShadow: "0 8px 28px rgba(0,0,0,0.18)",
1463
+ },
1464
+ dialogTitle: { fontSize: 14, fontWeight: 650 },
1465
+ dialogRow: { display: "flex", justifyContent: "flex-end", gap: 8 },
1466
+ };
1467
+
1468
+ function EpisodeBox({ node, current, child, sessionTitle, onJump, onFork, onInspect, onRename, canSetView, busy, livePreview }) {
1469
+ const [editing, setEditing] = react.useState(false);
1470
+ const [draft, setDraft] = react.useState("");
1471
+ const customTitle = customBoxTitle(node);
1472
+ const canRename = typeof onRename === "function" && Number.isFinite(Number(node?.id));
1473
+ const startEdit = (e) => {
1474
+ e.stopPropagation();
1475
+ e.preventDefault();
1476
+ if (!canRename) return;
1477
+ setDraft(customTitle);
1478
+ setEditing(true);
1479
+ };
1480
+ const commitEdit = () => {
1481
+ if (!editing) return;
1482
+ setEditing(false);
1483
+ const next = String(draft || "").trim();
1484
+ if (next === customTitle) return;
1485
+ onRename(node, next);
1486
+ };
1487
+ const stopIcon = (e) => {
1488
+ e.stopPropagation();
1489
+ };
1490
+ const inspectBtn = jsx("button", {
1491
+ type: "button",
1492
+ title: "轨迹",
1493
+ "aria-label": "轨迹",
1494
+ "data-map-inspect": "1",
1495
+ style: mapUi.iconBtn,
1496
+ onPointerDown: stopIcon,
1497
+ onPointerUp: stopIcon,
1498
+ onClick: (e) => {
1499
+ stopIcon(e);
1500
+ if (typeof onInspect === "function") onInspect(node);
1501
+ },
1502
+ children: jsx(TrajectoryIcon, {}),
1503
+ });
1504
+ const forkBtn = jsx("button", {
1505
+ type: "button",
1506
+ title: "分叉",
1507
+ "aria-label": "分叉",
1508
+ "data-map-fork": "1",
1509
+ disabled: !!busy,
1510
+ style: { ...mapUi.iconBtn, ...(busy ? { opacity: 0.4 } : {}) },
1511
+ onPointerDown: stopIcon,
1512
+ onPointerUp: stopIcon,
1513
+ onClick: (e) => {
1514
+ stopIcon(e);
1515
+ onFork(node);
1516
+ },
1517
+ children: jsx(ForkIcon, {}),
1518
+ });
1519
+ const openChatBtn =
1520
+ !canSetView
1521
+ ? jsx("button", {
1522
+ type: "button",
1523
+ title: "打开对话",
1524
+ "aria-label": "打开对话",
1525
+ "data-map-open": "1",
1526
+ style: mapUi.iconBtn,
1527
+ onPointerDown: stopIcon,
1528
+ onPointerUp: stopIcon,
1529
+ onClick: (e) => {
1530
+ stopIcon(e);
1531
+ onJump(node);
1532
+ },
1533
+ children: "对",
1534
+ })
1535
+ : null;
1536
+ const titleNode = editing
1537
+ ? jsx("input", {
1538
+ value: draft,
1539
+ autoFocus: true,
1540
+ "aria-label": "方框标题",
1541
+ style: mapUi.titleInput,
1542
+ onPointerDown: (e) => e.stopPropagation(),
1543
+ onClick: (e) => e.stopPropagation(),
1544
+ onChange: (e) => setDraft(e.target.value),
1545
+ onKeyDown: (e) => {
1546
+ e.stopPropagation();
1547
+ if (e.key === "Enter") {
1548
+ e.preventDefault();
1549
+ commitEdit();
1550
+ }
1551
+ if (e.key === "Escape") {
1552
+ e.preventDefault();
1553
+ setEditing(false);
1554
+ }
1555
+ },
1556
+ onBlur: commitEdit,
1557
+ })
1558
+ : jsx("div", {
1559
+ style: mapUi.title,
1560
+ title: "双击改名",
1561
+ onClick: (e) => e.stopPropagation(),
1562
+ onDoubleClick: startEdit,
1563
+ children: customTitle,
1564
+ });
1565
+ const body = boxBody(node, !child && (current || node.kind === "tail") ? livePreview : "");
1566
+ const jump = (e) => {
1567
+ if (e) e.stopPropagation();
1568
+ if (editing) return;
1569
+ onJump(node);
1570
+ };
1571
+ return jsxs("div", {
1572
+ "data-map-box": child ? "child" : current ? "current" : "main",
1573
+ "data-map-session": String(node.sessionId || ""),
1574
+ "data-map-kind": String(node.kind || ""),
1575
+ onPointerDown: (e) => e.stopPropagation(),
1576
+ onPointerUp: (e) => e.stopPropagation(),
1577
+ style: {
1578
+ ...mapUi.box,
1579
+ ...(current ? mapUi.boxCurrent : {}),
1580
+ ...(child ? mapUi.boxKid : {}),
1581
+ },
1582
+ children: [
1583
+ jsxs("div", {
1584
+ role: "button",
1585
+ tabIndex: 0,
1586
+ "data-map-jump": "1",
1587
+ style: mapUi.boxMain,
1588
+ onClick: jump,
1589
+ onKeyDown: (e) => {
1590
+ if (editing) return;
1591
+ if (e.key === "Enter" || e.key === " ") {
1592
+ e.preventDefault();
1593
+ onJump(node);
1594
+ }
1595
+ },
1596
+ children: [
1597
+ jsxs("div", {
1598
+ style: mapUi.titleRow,
1599
+ children: [
1600
+ jsxs("div", {
1601
+ style: { ...mapUi.kicker, flex: 1, minWidth: 0 },
1602
+ title: canRename ? "双击改名" : undefined,
1603
+ onDoubleClick: startEdit,
1604
+ children: [
1605
+ boxKicker(node, { current, child, sessionTitle }),
1606
+ ],
1607
+ }),
1608
+ canRename
1609
+ ? jsx("button", {
1610
+ type: "button",
1611
+ style: mapUi.ghostBtn,
1612
+ onPointerDown: stopIcon,
1613
+ onClick: startEdit,
1614
+ children: "改名",
1615
+ })
1616
+ : null,
1617
+ ],
1618
+ }),
1619
+ customTitle || editing ? titleNode : null,
1620
+ jsx("div", { style: mapUi.summary, children: body || "(尚无摘要)" }),
1621
+ jsx("div", { style: mapUi.hint, children: relTime(node.createdAt) }),
1622
+ ],
1623
+ }),
1624
+ jsxs("div", {
1625
+ style: mapUi.iconRow,
1626
+ onPointerDown: stopIcon,
1627
+ onClick: stopIcon,
1628
+ children: [inspectBtn, forkBtn, openChatBtn],
1629
+ }),
1630
+ ],
1631
+ });
1632
+ }
1633
+
1634
+ let pendingSessionNav = null;
1635
+
1636
+ function SessionMapView({ sessionId, useSessions, useSession, fork, open, renameSession, actions }) {
1637
+ const canSetView = typeof actions?.setView === "function";
1638
+ const chat = useSession((s) => s.chat);
1639
+ const cwd = useSessions((s) => (sessionId == null ? "" : s.byId[sessionId]?.cwd || ""));
1640
+ const sessionById = useSessions((s) => s.byId);
1641
+ const [archivedIds, setArchivedIds] = react.useState([]);
1642
+ const lineageKey = useSessions((s) => {
1643
+ const byId = s.byId || {};
1644
+ const kids = [];
1645
+ for (const id of Object.keys(byId)) {
1646
+ if (byId[id]?.parentSessionId === sessionId) kids.push(id);
1647
+ }
1648
+ kids.sort();
1649
+ return String(byId[sessionId]?.parentSessionId || "") + ":" + kids.join(",") + ":" + archivedIds.join(",");
1650
+ });
1651
+ const [nodes, setNodes] = react.useState([]);
1652
+ const [edges, setEdges] = react.useState([]);
1653
+ const [chips, setChips] = react.useState([]);
1654
+ const [pinIds, setPinIds] = react.useState([]);
1655
+ const [clipped, setClipped] = react.useState(false);
1656
+ const [error, setError] = react.useState("");
1657
+ const [busy, setBusy] = react.useState(false);
1658
+ const [scale, setScale] = react.useState(1);
1659
+ const [pan, setPan] = react.useState({ x: 0, y: 0 });
1660
+ const [chipQuery, setChipQuery] = react.useState("");
1661
+ const [inheritPins, setInheritPins] = react.useState(false);
1662
+ const [chipsOpen, setChipsOpen] = react.useState(false);
1663
+ const [forkDraft, setForkDraft] = react.useState(null);
1664
+ const canvasRef = react.useRef(null);
1665
+ const dragRef = react.useRef(null);
1666
+ const userMovedRef = react.useRef(false);
1667
+
1668
+ react.useEffect(() => {
1669
+ const list = workspacesList;
1670
+ if (!list || typeof list.subscribe !== "function" || typeof list.getSnapshot !== "function") {
1671
+ return undefined;
1672
+ }
1673
+ const pull = () => {
1674
+ try {
1675
+ const ids = list.getSnapshot()?.archivedSessionIds || [];
1676
+ setArchivedIds(Array.from(ids, String));
1677
+ } catch {
1678
+ setArchivedIds([]);
1679
+ }
1680
+ };
1681
+ pull();
1682
+ return list.subscribe(pull);
1683
+ }, []);
1684
+
1685
+ const liveSessions = react.useMemo(() => {
1686
+ const archived = new Set(archivedIds);
1687
+ const live = new Set();
1688
+ for (const id of Object.keys(sessionById || {})) {
1689
+ if (!archived.has(id)) live.add(id);
1690
+ }
1691
+ if (sessionId) live.add(String(sessionId));
1692
+ return live;
1693
+ }, [archivedIds, sessionById, sessionId]);
1694
+
1695
+ const qs = react.useMemo(() => {
1696
+ const params = new URLSearchParams();
1697
+ if (cwd) params.set("cwd", cwd);
1698
+ if (sessionId) params.set("sessionId", String(sessionId));
1699
+ const s = params.toString();
1700
+ return s ? "?" + s : "";
1701
+ }, [cwd, sessionId]);
1702
+
1703
+ const load = react.useCallback(async () => {
1704
+ if (!sessionId) return { nodes: [], edges: [] };
1705
+ try {
1706
+ const map = await api("/api/dsh-trivium/map" + qs);
1707
+ setNodes(map.nodes || []);
1708
+ setEdges(map.edges || []);
1709
+ const chipResp = await api("/api/dsh-trivium/chips" + qs);
1710
+ setChips(chipResp.chips || []);
1711
+ const pinResp = await api("/api/dsh-trivium/pins" + qs);
1712
+ setPinIds(pinResp.ids || []);
1713
+ setClipped(!!pinResp.clipped);
1714
+ setError("");
1715
+ return map;
1716
+ } catch (err) {
1717
+ setError(String(err.message || err));
1718
+ return { nodes: [], edges: [] };
1719
+ }
1720
+ }, [qs, sessionId]);
1721
+
1722
+ react.useEffect(() => {
1723
+ load();
1724
+ }, [load, lineageKey]);
1725
+
1726
+ const { chain, kids } = react.useMemo(
1727
+ () => layoutChain(nodes, edges, sessionId, liveSessions),
1728
+ [nodes, edges, sessionId, liveSessions],
1729
+ );
1730
+
1731
+ react.useEffect(() => {
1732
+ userMovedRef.current = false;
1733
+ setScale(1);
1734
+ }, [sessionId]);
1735
+
1736
+ react.useEffect(() => {
1737
+ const nav = pendingSessionNav;
1738
+ if (!nav || nav.sessionId !== sessionId) return undefined;
1739
+ pendingSessionNav = null;
1740
+ if (nav.view && typeof actions?.setView === "function") actions.setView(nav.view);
1741
+ if (nav.view === "chat") {
1742
+ scrollConversation({ bottom: !!nav.bottom, key: nav.key || "" });
1743
+ }
1744
+ return undefined;
1745
+ }, [sessionId]);
1746
+
1747
+ react.useEffect(() => {
1748
+ const el = canvasRef.current;
1749
+ if (!el) return undefined;
1750
+ const place = () => {
1751
+ if (userMovedRef.current) return;
1752
+ setPan(centerPan(chain.length, el, 1));
1753
+ };
1754
+ place();
1755
+ const ro = typeof ResizeObserver === "function" ? new ResizeObserver(place) : null;
1756
+ if (ro) ro.observe(el);
1757
+ return () => {
1758
+ if (ro) ro.disconnect();
1759
+ };
1760
+ }, [chain.length, lineageKey, nodes, edges, sessionId]);
1761
+
1762
+ react.useEffect(() => {
1763
+ const el = canvasRef.current;
1764
+ if (!el) return undefined;
1765
+ const onWheel = (e) => {
1766
+ e.preventDefault();
1767
+ userMovedRef.current = true;
1768
+ const factor = e.deltaY > 0 ? 0.92 : 1.08;
1769
+ setScale((s) => Math.min(2.4, Math.max(0.35, s * factor)));
1770
+ };
1771
+ el.addEventListener("wheel", onWheel, { passive: false });
1772
+ return () => el.removeEventListener("wheel", onWheel);
1773
+ }, []);
1774
+
1775
+ const onPointerDown = (e) => {
1776
+ if (e.button !== 0) return;
1777
+ if (e.target !== e.currentTarget && e.target.closest?.("[data-map-box]")) return;
1778
+ dragRef.current = { x: e.clientX, y: e.clientY, panX: pan.x, panY: pan.y };
1779
+ e.currentTarget.setPointerCapture?.(e.pointerId);
1780
+ };
1781
+ const onPointerMove = (e) => {
1782
+ const drag = dragRef.current;
1783
+ if (!drag) return;
1784
+ userMovedRef.current = true;
1785
+ setPan({ x: drag.panX + (e.clientX - drag.x), y: drag.panY + (e.clientY - drag.y) });
1786
+ };
1787
+ const onPointerUp = () => {
1788
+ dragRef.current = null;
1789
+ };
1790
+
1791
+ const openSession = (id, nav) => {
1792
+ if (typeof open !== "function") {
1793
+ setError("无法打开分叉会话:会话图没有拿到 session.open");
1794
+ return false;
1795
+ }
1796
+ try {
1797
+ pendingSessionNav = nav;
1798
+ open(id);
1799
+ return true;
1800
+ } catch (err) {
1801
+ pendingSessionNav = null;
1802
+ setError(String(err.message || err));
1803
+ return false;
1804
+ }
1805
+ };
1806
+
1807
+ const jumpToChat = (node) => {
1808
+ if (node.sessionId && node.sessionId !== sessionId) {
1809
+ openSession(node.sessionId, { sessionId: node.sessionId, view: "chat", bottom: true });
1810
+ return;
1811
+ }
1812
+ if (typeof actions?.setView !== "function") {
1813
+ setError("无法切换到对话页:会话图没有拿到同一 chat store 的 setView");
1814
+ return;
1815
+ }
1816
+ const toEnd = node.kind === "tail" || !!node.current || !Number(node.atSeq);
1817
+ actions.setView("chat");
1818
+ if (typeof actions.select === "function") {
1819
+ if (toEnd) actions.select(null);
1820
+ else actions.select({ turnSeq: Number(node.atSeq) });
1821
+ }
1822
+ scrollConversation({
1823
+ bottom: toEnd,
1824
+ key: toEnd ? "" : chatAnchorKey(chat, node),
1825
+ });
1826
+ };
1827
+
1828
+ const jumpToTrajectory = (node) => {
1829
+ if (node?.sessionId && node.sessionId !== sessionId) {
1830
+ openSession(node.sessionId, { sessionId: node.sessionId, view: "trajectory" });
1831
+ return;
1832
+ }
1833
+ if (typeof actions?.setView === "function") {
1834
+ actions.setView("trajectory");
1835
+ } else {
1836
+ setError("无法切换到轨迹页:会话图没有拿到同一 chat store 的 setView");
1837
+ }
1838
+ };
1839
+
1840
+ const renameBox = async (node, name) => {
1841
+ if (!Number.isFinite(Number(node?.id))) return;
1842
+ try {
1843
+ await api("/api/dsh-trivium/episodes/" + node.id + (cwd ? "?cwd=" + encodeURIComponent(cwd) : ""), {
1844
+ method: "POST",
1845
+ headers: { "content-type": "application/json" },
1846
+ body: JSON.stringify({ name }),
1847
+ });
1848
+ await load();
1849
+ } catch (err) {
1850
+ setError(String(err.message || err));
1851
+ }
1852
+ };
1853
+
1854
+ const forkBox = async (node, title) => {
1855
+ if (busy) return;
1856
+ setBusy(true);
1857
+ setError("");
1858
+ try {
1859
+ if (typeof fork !== "function") throw new Error("当前壳没有 session.fork");
1860
+ const sourceId = String(node.sessionId || sessionId);
1861
+ const opts = { sessionId: sourceId, increaseTitle: true };
1862
+ if (!node.current && node.kind !== "tail" && Number.isFinite(Number(node.atSeq))) {
1863
+ opts.atSeq = Number(node.atSeq);
1864
+ }
1865
+ const childId = await fork(opts);
1866
+ const wanted = String(title || "").trim();
1867
+ if (childId && wanted && typeof renameSession === "function") {
1868
+ try {
1869
+ await renameSession(childId, wanted);
1870
+ } catch (err) {
1871
+ setError("会话已分叉,但侧边栏改名失败:" + String(err.message || err));
1872
+ }
1873
+ }
1874
+ if (inheritPins && childId) {
1875
+ await api("/api/dsh-trivium/pins" + (cwd ? "?cwd=" + encodeURIComponent(cwd) : ""), {
1876
+ method: "POST",
1877
+ headers: { "content-type": "application/json" },
1878
+ body: JSON.stringify({ sessionId: childId, inherit: true, inheritFrom: sourceId }),
1879
+ });
1880
+ }
1881
+ const snap = await load();
1882
+ userMovedRef.current = true;
1883
+ const next = panToForkChild(
1884
+ snap.nodes || [],
1885
+ snap.edges || [],
1886
+ sessionId,
1887
+ childId,
1888
+ canvasRef.current,
1889
+ scale,
1890
+ liveSessions,
1891
+ );
1892
+ setPan(next.pan);
1893
+ setScale(next.scale);
1894
+ if (childId) openSession(childId, { sessionId: childId, view: "chat", bottom: true });
1895
+ } catch (err) {
1896
+ setError(String(err.message || err));
1897
+ } finally {
1898
+ setBusy(false);
1899
+ setForkDraft(null);
1900
+ }
1901
+ };
1902
+
1903
+ const askFork = (node) => {
1904
+ if (busy) return;
1905
+ const sourceId = String(node?.sessionId || sessionId);
1906
+ setForkDraft({
1907
+ node,
1908
+ title: nextForkTitle(sessionById, sourceId),
1909
+ });
1910
+ };
1911
+
1912
+ const togglePin = async (id, on) => {
1913
+ const next = on ? pinIds.concat(id).filter((x, i, a) => a.indexOf(x) === i) : pinIds.filter((x) => x !== id);
1914
+ try {
1915
+ const pin = await api("/api/dsh-trivium/pins" + (cwd ? "?cwd=" + encodeURIComponent(cwd) : ""), {
1916
+ method: "POST",
1917
+ headers: { "content-type": "application/json" },
1918
+ body: JSON.stringify({ sessionId, ids: next }),
1919
+ });
1920
+ setPinIds(pin.ids || next);
1921
+ setClipped(!!pin.clipped);
1922
+ } catch (err) {
1923
+ setError(String(err.message || err));
1924
+ }
1925
+ };
1926
+
1927
+ const pinSet = new Set(pinIds);
1928
+ const visibleChips = react.useMemo(() => {
1929
+ const needle = chipQuery.trim().toLowerCase();
1930
+ if (!needle) return chips;
1931
+ return chips.filter((c) =>
1932
+ [c.name, c.text, c.type, c.l0].some((s) => String(s || "").toLowerCase().includes(needle)),
1933
+ );
1934
+ }, [chips, chipQuery]);
1935
+
1936
+ const boxProps = {
1937
+ onJump: jumpToChat,
1938
+ onFork: askFork,
1939
+ onInspect: jumpToTrajectory,
1940
+ onRename: renameBox,
1941
+ canSetView,
1942
+ busy,
1943
+ livePreview: lastUserPreview(chat),
1944
+ };
1945
+
1946
+ return jsxs("div", {
1947
+ "data-map-build": "sessions-inject",
1948
+ style: mapUi.root,
1949
+ children: [
1950
+ jsxs("div", {
1951
+ style: mapUi.chipsHead,
1952
+ children: [
1953
+ jsx("div", { style: mapUi.chipsTitle, children: "记忆芯片" }),
1954
+ jsx("div", {
1955
+ style: mapUi.hint,
1956
+ children:
1957
+ "共 " +
1958
+ chips.length +
1959
+ " 条,已钉 " +
1960
+ pinIds.length +
1961
+ "。默认不勾选:不钉就不会按芯片白名单注入,但仍可能被开场短地图点名带上,模型也能 find 到。",
1962
+ }),
1963
+ jsx("button", {
1964
+ type: "button",
1965
+ style: mapUi.foldBtn,
1966
+ onClick: () => setChipsOpen((v) => !v),
1967
+ children: chipsOpen ? "收起" : "展开勾选",
1968
+ }),
1969
+ ],
1970
+ }),
1971
+ chipsOpen
1972
+ ? jsxs("div", {
1973
+ style: { display: "flex", flexDirection: "column", gap: 8 },
1974
+ children: [
1975
+ jsxs("div", {
1976
+ style: mapUi.chipsHead,
1977
+ children: [
1978
+ jsx("input", {
1979
+ type: "search",
1980
+ placeholder: "搜索芯片",
1981
+ value: chipQuery,
1982
+ onChange: (e) => setChipQuery(e.target.value),
1983
+ style: mapUi.search,
1984
+ }),
1985
+ jsxs("label", {
1986
+ style: mapUi.inherit,
1987
+ children: [
1988
+ jsx("input", {
1989
+ type: "checkbox",
1990
+ checked: inheritPins,
1991
+ onChange: (e) => setInheritPins(e.target.checked),
1992
+ }),
1993
+ "分叉时继承钉选",
1994
+ ],
1995
+ }),
1996
+ clipped
1997
+ ? jsx("div", { style: mapUi.hint, children: "已截到预算(≤300 token)" })
1998
+ : jsx("div", { style: mapUi.hint, children: "勾选后从当前这段的下一轮注入。空白处拖动画布,滚轮缩放。" }),
1999
+ ],
2000
+ }),
2001
+ visibleChips.length
2002
+ ? jsx("div", {
2003
+ style: mapUi.chips,
2004
+ children: visibleChips.map((c) =>
2005
+ jsxs(
2006
+ "label",
2007
+ {
2008
+ style: { ...mapUi.chip, ...(c.suggested ? mapUi.chipSuggest : {}) },
2009
+ children: [
2010
+ jsx("input", {
2011
+ type: "checkbox",
2012
+ checked: pinSet.has(c.id),
2013
+ onChange: (e) => togglePin(c.id, e.target.checked),
2014
+ }),
2015
+ c.suggested ? jsx("span", { style: mapUi.suggest, children: "建议" }) : null,
2016
+ jsx("span", {
2017
+ style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
2018
+ children: (c.type || "") + " · " + (c.l0 || c.name || "#" + c.id),
2019
+ }),
2020
+ ],
2021
+ },
2022
+ String(c.id),
2023
+ ),
2024
+ ),
2025
+ })
2026
+ : jsx("div", {
2027
+ style: mapUi.hint,
2028
+ children: chips.length
2029
+ ? "没有匹配的芯片。"
2030
+ : "还没有可钉选的 preference / decision / entity。",
2031
+ }),
2032
+ ],
2033
+ })
2034
+ : null,
2035
+ error ? jsx("div", { style: mapUi.error, children: error }) : null,
2036
+ jsx("div", {
2037
+ ref: canvasRef,
2038
+ style: mapUi.canvas,
2039
+ onPointerDown,
2040
+ onPointerMove,
2041
+ onPointerUp,
2042
+ onPointerCancel: onPointerUp,
2043
+ children: jsx("div", {
2044
+ style: {
2045
+ ...mapUi.world,
2046
+ transform: "translate(" + pan.x + "px, " + pan.y + "px) scale(" + scale + ")",
2047
+ },
2048
+ children:
2049
+ chain.length === 0
2050
+ ? jsxs("div", {
2051
+ style: mapUi.col,
2052
+ children: [
2053
+ jsx("div", { style: mapUi.kidStack }),
2054
+ jsx(EpisodeBox, {
2055
+ node: { kind: "tail", summary: "", atSeq: undefined, current: true },
2056
+ current: true,
2057
+ ...boxProps,
2058
+ }),
2059
+ jsx("div", { style: mapUi.kidStackBelow }),
2060
+ ],
2061
+ })
2062
+ : chain.map((node, i) => {
2063
+ const split = splitKids(kids.get(node.id) || []);
2064
+ return jsxs(
2065
+ "div",
2066
+ {
2067
+ style: mapUi.row,
2068
+ children: [
2069
+ jsxs("div", {
2070
+ style: mapUi.col,
2071
+ children: [
2072
+ jsxs("div", {
2073
+ style: mapUi.kidStack,
2074
+ children: [
2075
+ split.above.map((kid) =>
2076
+ jsx(
2077
+ EpisodeBox,
2078
+ {
2079
+ node: kid,
2080
+ child: true,
2081
+ sessionTitle:
2082
+ (sessionById?.[kid.sessionId] || {}).displayTitle ||
2083
+ (sessionById?.[kid.sessionId] || {}).title,
2084
+ ...boxProps,
2085
+ },
2086
+ "k" + kid.id,
2087
+ ),
2088
+ ),
2089
+ split.above.length
2090
+ ? jsx("div", { style: mapUi.forkArrow, children: jsx(ArrowV, { dir: "up" }) })
2091
+ : null,
2092
+ ],
2093
+ }),
2094
+ jsx(EpisodeBox, {
2095
+ node,
2096
+ current: !!node.current || node.kind === "tail",
2097
+ ...boxProps,
2098
+ }),
2099
+ jsxs("div", {
2100
+ style: mapUi.kidStackBelow,
2101
+ children: [
2102
+ split.below.length
2103
+ ? jsx("div", { style: mapUi.forkArrow, children: jsx(ArrowV, { dir: "down" }) })
2104
+ : null,
2105
+ split.below.map((kid) =>
2106
+ jsx(
2107
+ EpisodeBox,
2108
+ {
2109
+ node: kid,
2110
+ child: true,
2111
+ sessionTitle:
2112
+ (sessionById?.[kid.sessionId] || {}).displayTitle ||
2113
+ (sessionById?.[kid.sessionId] || {}).title,
2114
+ ...boxProps,
2115
+ },
2116
+ "k" + kid.id,
2117
+ ),
2118
+ ),
2119
+ ],
2120
+ }),
2121
+ ],
2122
+ }),
2123
+ i < chain.length - 1
2124
+ ? jsxs("div", {
2125
+ style: mapUi.arrow,
2126
+ children: [
2127
+ jsx("div", {}),
2128
+ jsx("div", { style: mapUi.arrowMid, children: jsx(ArrowH, {}) }),
2129
+ jsx("div", {}),
2130
+ ],
2131
+ })
2132
+ : null,
2133
+ ],
2134
+ },
2135
+ String(node.id),
2136
+ );
2137
+ }),
2138
+ }),
2139
+ }),
2140
+ forkDraft
2141
+ ? jsx("div", {
2142
+ style: mapUi.overlay,
2143
+ onPointerDown: (e) => e.stopPropagation(),
2144
+ children: jsxs("form", {
2145
+ style: mapUi.dialog,
2146
+ onSubmit: (e) => {
2147
+ e.preventDefault();
2148
+ if (forkDraft?.node) forkBox(forkDraft.node, forkDraft.title);
2149
+ },
2150
+ children: [
2151
+ jsx("div", { style: mapUi.dialogTitle, children: "分叉为新会话" }),
2152
+ jsx("div", {
2153
+ style: mapUi.hint,
2154
+ children: "这个名字会出现在左侧会话列表里。",
2155
+ }),
2156
+ jsx("input", {
2157
+ autoFocus: true,
2158
+ value: forkDraft.title,
2159
+ "aria-label": "新会话名称",
2160
+ onChange: (e) => setForkDraft({ ...forkDraft, title: e.target.value }),
2161
+ style: { ...mapUi.titleInput, flex: "none", width: "100%" },
2162
+ }),
2163
+ jsxs("div", {
2164
+ style: mapUi.dialogRow,
2165
+ children: [
2166
+ jsx("button", {
2167
+ type: "button",
2168
+ style: mapUi.foldBtn,
2169
+ onClick: () => setForkDraft(null),
2170
+ children: "取消",
2171
+ }),
2172
+ jsx("button", {
2173
+ type: "submit",
2174
+ style: mapUi.foldBtn,
2175
+ disabled: busy,
2176
+ children: busy ? "分叉中…" : "分叉",
2177
+ }),
2178
+ ],
2179
+ }),
2180
+ ],
2181
+ }),
2182
+ })
2183
+ : null,
2184
+ ],
2185
+ });
2186
+ }
2187
+
782
2188
  function apply(ctx) {
2189
+ workspacesList = ctx.workspaces?.list || null;
2190
+ const sessions = ctx.sessions;
783
2191
  ctx.slots.inject(
784
2192
  "settings.section",
785
2193
  () =>
@@ -795,10 +2203,43 @@ window.__ModuleLoader__.load({
795
2203
  ),
796
2204
  "dsh-trivium: settings section",
797
2205
  );
2206
+ ctx.slots.inject(
2207
+ "conversation.view",
2208
+ () => {
2209
+ const chatStore = sharedChatStore(ctx);
2210
+ return ctx.slots.register(
2211
+ {
2212
+ name: "conversation.view",
2213
+ id: "session-map",
2214
+ order: 20,
2215
+ label: () => "会话图",
2216
+ ...(chatStore ? { store: chatStore } : {}),
2217
+ inject: () => ({
2218
+ fork: (opts) => sessions.fork(opts),
2219
+ open: (id) => sessions.open(id),
2220
+ renameSession: async (id, title) => {
2221
+ const face = sessions.binding?.(id)?.session;
2222
+ if (!face || typeof face.rename !== "function") {
2223
+ throw new Error("当前壳没有 session.rename");
2224
+ }
2225
+ const result = await face.rename(title);
2226
+ if (result && result.ok === false) {
2227
+ const err = result.error || {};
2228
+ throw new Error(err.message || err.code || "rename failed");
2229
+ }
2230
+ return result;
2231
+ },
2232
+ }),
2233
+ },
2234
+ SessionMapView,
2235
+ );
2236
+ },
2237
+ "dsh-trivium: session map",
2238
+ );
798
2239
  }
799
2240
 
800
2241
  exports.apply = apply;
801
- exports.inject = ["slots"];
2242
+ exports.inject = ["slots", "sessions", "workspaces"];
802
2243
  return module.exports;
803
2244
  },
804
2245
  });