dsh-mindmap 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -958,6 +958,75 @@ window.__ModuleLoader__.load({
958
958
  }
959
959
  //#endregion
960
960
 
961
+ //#region 025 草稿保护:能力探测(宿主是否让插件读到聊天草稿)
962
+ /**
963
+ * 读取当前聊天草稿。宿主契约只保证 setDraft/submit,读取面属可选能力:
964
+ * 逐个探测已知形态,读不到返回 null(= 不可知,不等于空草稿)。
965
+ */
966
+ function readDraftText(inputActions) {
967
+ if (!inputActions) return null;
968
+ try {
969
+ if (typeof inputActions.getDraft === "function") return String(inputActions.getDraft() ?? "");
970
+ if (typeof inputActions.draft === "string") return inputActions.draft;
971
+ if (typeof inputActions.getState === "function") {
972
+ const state = inputActions.getState();
973
+ if (state && typeof state.draft === "string") return state.draft;
974
+ }
975
+ } catch {
976
+ return null;
977
+ }
978
+ return null;
979
+ }
980
+
981
+ /**
982
+ * 是否因「已有未发送草稿」而放弃自动发送。只有确实读到非空草稿才拦截;
983
+ * 读不到时不拦——否则在不暴露草稿的宿主上,点目录文件会完全打不开。
984
+ */
985
+ function draftBlocksAutoSend(inputActions) {
986
+ const draft = readDraftText(inputActions);
987
+ return typeof draft === "string" && draft.trim() !== "";
988
+ }
989
+ //#endregion
990
+
991
+ //#region 025 子树折叠:画布视图态纯函数(不进 markdown 资产,只影响呈现)
992
+ /** 折叠集合切换:恒返回新集合,React 状态可直接按引用比较。 */
993
+ function toggleCollapsed(collapsed, id) {
994
+ const next = new Set(collapsed ?? []);
995
+ if (next.has(id)) next.delete(id);
996
+ else next.add(id);
997
+ return next;
998
+ }
999
+
1000
+ /** 子孙节点总数:折叠后用于提示「隐藏了多少节点」。 */
1001
+ function countDescendants(node) {
1002
+ let total = 0;
1003
+ const walk = (n) => {
1004
+ for (const child of n.children ?? []) {
1005
+ total += 1;
1006
+ walk(child);
1007
+ }
1008
+ };
1009
+ if (node) walk(node);
1010
+ return total;
1011
+ }
1012
+
1013
+ /**
1014
+ * 丢弃当前树里已不存在的折叠 id(AI 改写文档后旧 id 会失效)。
1015
+ * 没有变化时原样返回入参,避免制造新引用触发多余重渲染。
1016
+ */
1017
+ function pruneCollapsed(collapsed, tree) {
1018
+ if (!collapsed || collapsed.size === 0) return collapsed;
1019
+ const ids = collectTreeIds(tree);
1020
+ let changed = false;
1021
+ const next = new Set();
1022
+ for (const id of collapsed) {
1023
+ if (ids.has(id)) next.add(id);
1024
+ else changed = true;
1025
+ }
1026
+ return changed ? next : collapsed;
1027
+ }
1028
+ //#endregion
1029
+
961
1030
  //#region PNG 导出(SVG 序列化 → canvas → 下载 / 剪贴板)
962
1031
  // 019 可变盒高布局:盒高按内容估行数(全量换行的导出形态),表格节点加宽;
963
1032
  // 布局契约不变——叶子自上而下占行、父节点垂直居中于其子块。
@@ -1269,8 +1338,7 @@ window.__ModuleLoader__.load({
1269
1338
  loadingErrorText: { color: "var(--dsw-alias-label-error)", fontSize: "13px", lineHeight: 1.6, margin: "0", textAlign: "center", wordBreak: "break-word", maxWidth: "90%" },
1270
1339
  loadingFailMark: { flex: "none", fontSize: "22px", lineHeight: 1, color: "var(--dsw-alias-label-error)" },
1271
1340
  retryBtn: { flex: "none", border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", color: "var(--dsw-alias-label-primary)", cursor: "pointer", font: "inherit", fontSize: "13px", padding: "6px 18px", borderRadius: "8px", lineHeight: "20px" },
1272
- // 013 目录树 tab:树容器/行样式。视觉自成一套:emoji 图标 + M 徽标 +
1273
- // 悬停高亮 + 激活指示条,不做 VSCode 式 chevron/线框。
1341
+ // 013 独立目录树:emoji 图标 + M 徽标;内嵌模式在下方单独覆盖。
1274
1342
  // 树容器 -2px 负边距抵消 body 16px 内距:树左缘 = 头部「目录」tab 左缘(14px)。
1275
1343
  treeWrap: { display: "flex", flexDirection: "column", gap: "12px", height: "100%", minHeight: 0, marginLeft: "-2px", marginRight: "-2px" },
1276
1344
  treeList: { flex: "1 1 auto", overflowY: "auto", display: "flex", flexDirection: "column", gap: "2px", minHeight: 0 },
@@ -1316,7 +1384,11 @@ window.__ModuleLoader__.load({
1316
1384
  swatchActive: { borderColor: "var(--dsw-alias-state-business-primary)", color: "var(--dsw-alias-label-primary)", boxShadow: "inset 0 0 0 1px var(--dsw-alias-state-business-primary)" },
1317
1385
  swatchDot: { width: "10px", height: "10px", borderRadius: "50%", flex: "none" },
1318
1386
  row: { display: "flex", alignItems: "center", minWidth: 0 },
1319
- childrenColumn: { display: "flex", flexDirection: "column", gap: "8px", marginLeft: "40px", minWidth: 0 },
1387
+ // 025:子列左距 16 + 折叠开关(16 + 左右各 4 外距)= 折叠前的 40,
1388
+ // 连线长度与既有版式保持一致。
1389
+ childrenColumn: { display: "flex", flexDirection: "column", gap: "8px", marginLeft: "16px", minWidth: 0 },
1390
+ // 025 折叠开关:压在连线起点上的小圆钮,叶子节点不渲染。
1391
+ collapseToggle: { flex: "none", width: "16px", height: "16px", margin: "0 4px", padding: 0, borderRadius: "50%", border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", color: "var(--dsw-alias-label-secondary)", font: "inherit", fontSize: "11px", lineHeight: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: "pointer", zIndex: 1 },
1320
1392
  // 面板树连线层:正交折线(MarkGrove 的 orthogonalPath 风格),
1321
1393
  // 覆盖整行、点击穿透、置于节点盒之下。
1322
1394
  // 016:去掉 CSS width/height 百分比——在 auto-height 的 flex 行内,
@@ -1367,7 +1439,52 @@ window.__ModuleLoader__.load({
1367
1439
  // 百分比标签:tabular-nums 防数字抖动。
1368
1440
  zoomLabel: { flex: "none", minWidth: "38px", textAlign: "center", fontSize: "11px", lineHeight: "20px", color: "var(--dsw-alias-label-secondary)", fontVariantNumeric: "tabular-nums", userSelect: "none" },
1369
1441
  zoomFitBtn: { border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: "12px", lineHeight: "20px", height: "22px", padding: "0 8px", borderRadius: "6px", color: "var(--dsw-alias-label-secondary)", flex: "none" },
1442
+ // 027 内嵌头部(sidebar 模式):BS 外层已有 Tab 头部,内嵌只保留一行
1443
+ // 紧凑工具栏——脑图列表标签 + 当前脑图标签 + 导出按钮(行尾)。
1444
+ // 上下内距比 standalone 的 header(12px 14px 0)更紧凑,行间距更小。
1445
+ sbToolbar: { display: "flex", alignItems: "center", gap: "6px", padding: "6px 10px", boxSizing: "border-box", borderBottom: "1px solid var(--dsw-alias-border-l2)", flex: "none", minWidth: 0 },
1446
+ // 紧凑标签:比 standalone 的 tab(3px 12px)更小,贴合单行工具栏。
1447
+ sbTab: { border: "none", background: "none", cursor: "pointer", padding: "2px 8px", lineHeight: "20px", borderRadius: "6px", font: "inherit", fontSize: "12px", color: "var(--dsw-alias-label-secondary)", whiteSpace: "nowrap", maxWidth: "120px", overflow: "hidden", textOverflow: "ellipsis", transition: "background 0.08s ease, color 0.08s ease" },
1448
+ sbTabActive: { background: "var(--dsw-alias-bg-layer-3)", color: "var(--dsw-alias-label-primary)" },
1449
+ // 当前脑图标签包裹:与 standalone 的 tabWrap 同构,但圆角与内距更紧凑。
1450
+ sbTabWrap: { display: "inline-flex", alignItems: "center", borderRadius: "6px", overflow: "hidden", maxWidth: "160px", transition: "background 0.08s ease" },
1451
+ sbTabTitle: { background: "none", border: "none", cursor: "pointer", font: "inherit", color: "inherit", padding: "2px 4px 2px 8px", lineHeight: "20px", whiteSpace: "nowrap", maxWidth: "110px", overflow: "hidden", textOverflow: "ellipsis" },
1452
+ sbTabClose: { background: "none", border: "none", cursor: "pointer", padding: "2px 6px 2px 2px", lineHeight: "20px", color: "var(--dsw-alias-label-tertiary)", fontSize: "11px", flex: "none" },
1453
+ };
1454
+
1455
+ // 内嵌界面与 Better Sidebar 共用宿主字体角色;变量缺失时仍有完整回退。
1456
+ // 清除字号/行高等长属性,避免覆盖 font 简写中的主题值。
1457
+ const SIDEBAR_BODY_FONT = "var(--dsw-font-s-14, 400 14px/22px sans-serif)";
1458
+ const SIDEBAR_UI_FONT = "var(--dsw-font-xxs-12, 400 12px/18px sans-serif)";
1459
+ function sidebarFontStyle(style, font = SIDEBAR_UI_FONT) {
1460
+ const { fontSize, lineHeight, fontWeight, fontFamily, ...rest } = style;
1461
+ return { ...rest, font };
1462
+ }
1463
+ const SIDEBAR_STYLES = {
1464
+ ...S,
1465
+ sbRoot: { display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0, minWidth: 0, font: SIDEBAR_BODY_FONT },
1466
+ sbToolbar: sidebarFontStyle(S.sbToolbar),
1467
+ sbTab: sidebarFontStyle(S.sbTab),
1468
+ sbTabTitle: sidebarFontStyle(S.sbTabTitle),
1469
+ action: sidebarFontStyle(S.action),
1470
+ emptyHint: sidebarFontStyle(S.emptyHint),
1471
+ treeRefresh: sidebarFontStyle(S.treeRefresh),
1472
+ treeError: sidebarFontStyle(S.treeError),
1473
+ treeMenuItem: sidebarFontStyle(S.treeMenuItem),
1474
+ loadingText: sidebarFontStyle(S.loadingText, SIDEBAR_BODY_FONT),
1475
+ loadingErrorText: sidebarFontStyle(S.loadingErrorText, SIDEBAR_BODY_FONT),
1476
+ retryBtn: sidebarFontStyle(S.retryBtn),
1477
+ treeList: { ...S.treeList, gap: 0 },
1478
+ treeRow: { ...sidebarFontStyle(S.treeRow, SIDEBAR_BODY_FONT), minHeight: "34px", flexShrink: 0, gap: "6px", color: "var(--dsw-alias-label-primary)" },
1479
+ treeRootRow: { color: "var(--dsw-alias-label-primary)" },
1480
+ treeRowMd: { color: "var(--dsw-alias-label-primary)" },
1481
+ // M 与文件名共用主题前景色;透明底避免主题品牌色与浅底碰撞。
1482
+ // 独立声明字号/字重,不受宿主字体简写与字体切换影响。
1483
+ mdBadge: { ...S.mdBadge, width: "16px", height: "16px", boxSizing: "border-box", border: "1px solid currentColor", borderRadius: "3px", fontFamily: "system-ui, sans-serif", fontSize: "12px", fontWeight: 800, lineHeight: 1, background: "transparent", color: "inherit" },
1370
1484
  };
1485
+ function workspaceStyles(variant) {
1486
+ return variant === "sidebar" ? SIDEBAR_STYLES : S;
1487
+ }
1371
1488
 
1372
1489
  /** 015 分段选择控件(线型/卡片风格)。 */
1373
1490
  function Segmented(props) {
@@ -1532,78 +1649,6 @@ window.__ModuleLoader__.load({
1532
1649
  ] });
1533
1650
  }
1534
1651
 
1535
- /**
1536
- * 会话内容节点的双代快照选择(023):dsh ≤0.1.1 的 useSession 快照带
1537
- * 平铺 nodes;0.1.2-rc.1 起 SessionSnapshot 拆成纯控制状态,会话内容
1538
- * 迁入 useChat 的 ChatSnapshot.legacy.nodes(官方兼容面,ToolResultNode
1539
- * 字段同名)。legacy 优先、旧 nodes 兜底,两代通吃。
1540
- */
1541
- function conversationNodesOf(s) {
1542
- if (!s) return EMPTY_NODES;
1543
- const legacy = s.legacy;
1544
- if (legacy && Array.isArray(legacy.nodes)) return legacy.nodes;
1545
- return Array.isArray(s.nodes) ? s.nodes : EMPTY_NODES;
1546
- }
1547
-
1548
- /**
1549
- * 「思维脑图」槽位组件(014):同一槽位渲染 M 按钮 + 悬浮面板宿主层。
1550
- * session scope 的 useSession/sessionId/inputActions 直给,经 props 传给
1551
- * MindmapDetailsPanel(无桥、无 useSyncExternalStore——shell.overlay 跨槽
1552
- * 方案实测未渲染,弃用后顺手把桥也删了)。023:内容钩子改为
1553
- * useChat(0.1.2-rc.1+)优先、useSession(≤0.1.1)兜底。
1554
- */
1555
- function MindmapSlot(props) {
1556
- const { useSession, useChat, sessionId, inputActions, mindmapFace } = props;
1557
- const nodesHook = useChat ?? useSession;
1558
- const nodes = nodesHook ? nodesHook(conversationNodesOf) : EMPTY_NODES;
1559
- // 016 可靠性加固:结构指纹作第二 selector。store 原地改数组(引用
1560
- // 不变)时,nodes prop 不换、memo 命中缓存、auto-open effect 永不
1561
- // 重跑——「AI 打开了脑图但面板不展开」的根因。指纹是原始值字符串,
1562
- // 值比较天然绕过引用相等短路;内容钩子不可用时回退空串。
1563
- const nodesVersion = nodesHook ? nodesHook((s) => nodesFingerprint(conversationNodesOf(s))) : "";
1564
- const [open, setOpen] = react.useState(false);
1565
- return (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
1566
- (0, react_jsx_runtime.jsxs)("button", {
1567
- type: "button",
1568
- title: "脑图面板:展开 / 收起",
1569
- style: S.mButton,
1570
- onClick: () => setOpen((v) => !v),
1571
- children: [
1572
- (0, react_jsx_runtime.jsx)("svg", {
1573
- width: 14,
1574
- height: 14,
1575
- viewBox: "0 0 14 14",
1576
- fill: "none",
1577
- stroke: "currentColor",
1578
- strokeWidth: 1.4,
1579
- strokeLinecap: "round",
1580
- strokeLinejoin: "round",
1581
- "aria-hidden": "true",
1582
- style: { opacity: 0.7, flex: "none" },
1583
- children: [
1584
- (0, react_jsx_runtime.jsx)("circle", { cx: 2.5, cy: 7, r: 1.7 }),
1585
- (0, react_jsx_runtime.jsx)("circle", { cx: 11.5, cy: 3.5, r: 1.7 }),
1586
- (0, react_jsx_runtime.jsx)("circle", { cx: 11.5, cy: 10.5, r: 1.7 }),
1587
- (0, react_jsx_runtime.jsx)("path", { d: "M4.1 6.2 L9.9 4.2" }),
1588
- (0, react_jsx_runtime.jsx)("path", { d: "M4.1 7.8 L9.9 9.8" }),
1589
- ],
1590
- }),
1591
- "思维脑图",
1592
- ],
1593
- }),
1594
- (0, react_jsx_runtime.jsx)(MindmapDetailsPanel, {
1595
- open,
1596
- sessionId,
1597
- inputActions,
1598
- nodes,
1599
- nodesVersion,
1600
- mindmapFace,
1601
- onOpen: () => setOpen(true),
1602
- onClose: () => setOpen(false),
1603
- }),
1604
- ] });
1605
- }
1606
-
1607
1652
  //#region 019 血肉渲染:行内格式 + 大一统链接 + 表格块(规范源:003)
1608
1653
  // 行内格式统一扫描序:图片/链接 → 行内代码 → 粗体 → 删除线 → 斜体 → 裸链接。
1609
1654
  // 先命中先生效,裸链接放最后,避免吞掉已被 [文字](url) 消费的 URL。
@@ -1784,7 +1829,10 @@ window.__ModuleLoader__.load({
1784
1829
 
1785
1830
  /** 左→右递归树:节点盒 + 右侧子节点列 + 连线层(015 支持折线/曲线两种线型)。 */
1786
1831
  function TreeRow(props) {
1787
- const { node, theme, onNodeContextMenu, reveal, selectedId, onCodePanel } = props;
1832
+ const { node, theme, onNodeContextMenu, reveal, selectedId, onCodePanel, collapsed, onToggleCollapse } = props;
1833
+ // 025 折叠:纯视图态——markdown 资产不变,导出仍取完整子树。
1834
+ const hasChildren = Boolean(node.children && node.children.length > 0);
1835
+ const isCollapsed = hasChildren && Boolean(collapsed && collapsed.has(node.id));
1788
1836
  // 018 生长动画:本节点渐显延迟(新节点盒)与本行连线渐显延迟(有新子节点)。
1789
1837
  const revealDelay = reveal && reveal.nodes ? reveal.nodes.get(node.id) : undefined;
1790
1838
  const edgeRevealDelay = reveal && reveal.edges ? reveal.edges.get(node.id) : undefined;
@@ -1821,7 +1869,8 @@ window.__ModuleLoader__.load({
1821
1869
  const rowRect = rowEl.getBoundingClientRect();
1822
1870
  const boxRect = boxEl.getBoundingClientRect();
1823
1871
  const next = [];
1824
- for (const ref of childRefs.current) {
1872
+ // 折叠时子列未挂载:不量、不画线(ref 回调已置空,这里再兜一层)。
1873
+ for (const ref of isCollapsed ? [] : childRefs.current) {
1825
1874
  if (!ref) continue;
1826
1875
  const c = ref.getBoundingClientRect();
1827
1876
  // 视觉像素 → 行本地坐标(SVG 用户空间 = 本地空间)。
@@ -1899,13 +1948,30 @@ window.__ModuleLoader__.load({
1899
1948
  } : undefined,
1900
1949
  children: (0, react_jsx_runtime.jsx)(NodeBox, { node, theme, revealDelay, selectedId, onCodePanel }),
1901
1950
  }),
1902
- node.children && node.children.length > 0
1903
- ? (0, react_jsx_runtime.jsx)("div", { style: S.childrenColumn, children: node.children.map((child, idx) => (0, react_jsx_runtime.jsx)("div", {
1951
+ // 025 折叠开关:坐在盒与子列之间的连线起点上(有子节点才出现)。
1952
+ // stopPropagation 保证点它不触发画布的「点节点聚焦 / 点空白取消选中」。
1953
+ hasChildren
1954
+ ? (0, react_jsx_runtime.jsx)("button", {
1955
+ type: "button",
1956
+ "data-mindmap-collapse": "",
1957
+ style: S.collapseToggle,
1958
+ title: isCollapsed ? `展开子树(已隐藏 ${countDescendants(node)} 个节点)` : "折叠子树",
1959
+ "aria-expanded": isCollapsed ? "false" : "true",
1960
+ onClick: (e) => {
1961
+ e.preventDefault();
1962
+ e.stopPropagation();
1963
+ if (onToggleCollapse) onToggleCollapse(node.id);
1964
+ },
1965
+ children: isCollapsed ? "+" : "−",
1966
+ })
1967
+ : null,
1968
+ hasChildren && !isCollapsed
1969
+ ? (0, react_jsx_runtime.jsx)("div", { style: S.childrenColumn, "data-mindmap-children": "", children: node.children.map((child, idx) => (0, react_jsx_runtime.jsx)("div", {
1904
1970
  key: child.id,
1905
1971
  ref: (el) => {
1906
1972
  childRefs.current[idx] = el;
1907
1973
  },
1908
- children: (0, react_jsx_runtime.jsx)(TreeRow, { node: child, theme, onNodeContextMenu, reveal, selectedId, onCodePanel }),
1974
+ children: (0, react_jsx_runtime.jsx)(TreeRow, { node: child, theme, onNodeContextMenu, reveal, selectedId, onCodePanel, collapsed, onToggleCollapse }),
1909
1975
  }, child.id)) })
1910
1976
  : null,
1911
1977
  ] });
@@ -1987,6 +2053,12 @@ window.__ModuleLoader__.load({
1987
2053
  };
1988
2054
  }
1989
2055
 
2056
+ /** 按下点是否落在画布内的交互控件上(这类按下不启动平移,留给控件自己)。 */
2057
+ function isCanvasControl(el) {
2058
+ if (!el || typeof el.closest !== "function") return false;
2059
+ return Boolean(el.closest("button, a[href], input, textarea, select, [role='button']"));
2060
+ }
2061
+
1990
2062
  /** 空格键是否落在可输入元素里(聊天框/输入框与面板同 document,不能抢空格)。 */
1991
2063
  function isTextEntry(el) {
1992
2064
  if (!el || typeof el.tagName !== "string") return false;
@@ -2045,6 +2117,9 @@ window.__ModuleLoader__.load({
2045
2117
  const nodeMenuRef = react.useRef(null);
2046
2118
  // 019 选中态:点击聚焦的节点下选选中环(002 §6 状态体系)。
2047
2119
  const [selectedId, setSelectedId] = react.useState(null);
2120
+ // 025 折叠子树:纯视图态(不写回 markdown,导出仍取完整子树)。
2121
+ // 切换文档时全部展开;AI 改写后清掉已消失节点的折叠标记。
2122
+ const [collapsed, setCollapsed] = react.useState(() => new Set());
2048
2123
  // 019 代码块悬停浮层:{node, anchor};null = 关闭。延迟关闭(150ms
2049
2124
  // 宽限)让鼠标能从节点盒移到面板上滚动全文,不闪灭。
2050
2125
  const [codePanel, setCodePanel] = react.useState(null);
@@ -2108,6 +2183,7 @@ window.__ModuleLoader__.load({
2108
2183
  userZoomedRef.current = false;
2109
2184
  lastNaturalRef.current = null;
2110
2185
  fitStampRef.current = [];
2186
+ setCollapsed((prev) => (prev.size > 0 ? new Set() : prev));
2111
2187
  const id = requestAnimationFrame(applyFit);
2112
2188
  return () => cancelAnimationFrame(id);
2113
2189
  }, [fitKey]);
@@ -2204,6 +2280,10 @@ window.__ModuleLoader__.load({
2204
2280
  const scroller = scrollRef.current;
2205
2281
  if (!scroller || panRef.current) return;
2206
2282
  const target = e.target;
2283
+ // 025:画布内的控件(折叠开关等)必须先于平移拿到这次按下。
2284
+ // 否则 setPointerCapture 会把随后的 click 改派到滚动区,按钮
2285
+ // 永远收不到点击——「折叠按钮点了没反应」的根因。
2286
+ if (isCanvasControl(target)) return;
2207
2287
  const onNode = Boolean(target && typeof target.closest === "function" && target.closest("[data-mindmap-node]"));
2208
2288
  if (!shouldStartPan(e.button, { onNode, spaceHeld: spaceRef.current, touch: e.pointerType === "touch" })) return;
2209
2289
  // 中键:掐掉浏览器自动滚动;左键:掐掉拖选文本。
@@ -2395,8 +2475,14 @@ window.__ModuleLoader__.load({
2395
2475
  setNodeMenu(null);
2396
2476
  // 019:文档内容变化时同步收掉代码浮层(节点对象已失效)。
2397
2477
  setCodePanel(null);
2478
+ // 025:树重解析后丢弃已消失节点的折叠标记(无变化时保持原引用)。
2479
+ setCollapsed((prev) => pruneCollapsed(prev, node));
2398
2480
  }, [node]);
2399
2481
 
2482
+ function toggleCollapse(id) {
2483
+ setCollapsed((prev) => toggleCollapsed(prev, id));
2484
+ }
2485
+
2400
2486
  // 017 右键节点:记录菜单锚点与目标子树(清掉上次的忙碌/错误态)。
2401
2487
  function onNodeContextMenu(e, target) {
2402
2488
  setNodeMenuBusy(null);
@@ -2461,7 +2547,7 @@ window.__ModuleLoader__.load({
2461
2547
  children:
2462
2548
  (0, react_jsx_runtime.jsx)("div", { style: S.canvasCenter, children:
2463
2549
  (0, react_jsx_runtime.jsx)("div", { ref: contentRef, style: { margin: "auto", zoom }, children:
2464
- (0, react_jsx_runtime.jsx)(TreeRow, { node, theme, onNodeContextMenu, reveal, selectedId, onCodePanel: handleCodePanel })
2550
+ (0, react_jsx_runtime.jsx)(TreeRow, { node, theme, onNodeContextMenu, reveal, selectedId, onCodePanel: handleCodePanel, collapsed, onToggleCollapse: toggleCollapse })
2465
2551
  })
2466
2552
  })
2467
2553
  }),
@@ -2568,11 +2654,23 @@ window.__ModuleLoader__.load({
2568
2654
  }
2569
2655
  //#endregion
2570
2656
 
2571
- function MindmapDetailsPanel(props) {
2572
- // 014:面板与 M 按钮同槽位(conversation.session.header.actions),
2573
- // 会话能力(sessionId/inputActions/nodes)与开合回调全部由 MindmapSlot
2574
- // props 直给(无桥、无 useSyncExternalStore)。
2575
- const { mindmapFace, open, sessionId, inputActions, nodes, nodesVersion, onOpen, onClose } = props;
2657
+ /**
2658
+ * 壳无关的脑图工作区(026 拆分):从 MindmapDetailsPanel 提取的全部
2659
+ * 共享状态与逻辑——文档合并、目录树、视图切换、导出、自动展开、焦点
2660
+ * 同步、生长动画、加载态。不含任何壳特有几何(fixed 定位、宽度拖拽、
2661
+ * layout-push CSS、头部高度对齐),这些由外层壳(独立 fixed /
2662
+ * Better Sidebar Tab 壳)提供。
2663
+ *
2664
+ * visible:内容是否对用户可见(独立壳 = open;BS Tab = visible)。
2665
+ * 不可见时仍挂载——hooks 照常跑,auto-open 能在面板/Tab 收起时触发
2666
+ * onAutoOpen 把它拉起。onAutoOpen 在独立壳里 = setOpen(true),在
2667
+ * BS Tab 里 = openTab(...)。onClose 仅独立壳提供(BS Tab 自带关闭)。
2668
+ * headerHeight:独立壳传入的对齐高度(null = BS Tab 模式,头部自适应)。
2669
+ */
2670
+ function MindmapWorkspace(props) {
2671
+ const { mindmapFace, visible, sessionId, inputActions, nodes, nodesVersion, onAutoOpen, onClose, headerHeight, variant } = props;
2672
+ // 只调整工作区界面;脑图节点与导出继续使用自己的字体层级。
2673
+ const S = workspaceStyles(variant);
2576
2674
  // 016:nodesVersion(结构指纹)作副依赖——nodes 引用不变但内容已变
2577
2675
  // (新工具结果原地落地)时强制重算;docs 新引用带动 merged →
2578
2676
  // auto-open effect 重跑(对已消费事件幂等 no-op),面板必达展开。
@@ -2589,55 +2687,13 @@ window.__ModuleLoader__.load({
2589
2687
  // AI 没调工具(S3)或任何未知成因卡住时的兜底恢复路径。
2590
2688
  const OPEN_TIMEOUT_MS = 30000;
2591
2689
  const [openTimedOut, setOpenTimedOut] = react.useState(false);
2592
- // 014 overlay 宽度:localStorage 持久化,拖拽钳制 [280, 视口 80%]。
2593
- // 窗口尺寸变化时持续钳制——只在挂载时压一次的话,窗口先放大→拖宽
2594
- // 面板→再缩小会让面板保持旧像素宽,聊天区被挤没。
2595
- const WIDTH_KEY = "dsh-mindmap.overlay-width";
2596
- const [panelWidth, setPanelWidth] = react.useState(() => {
2597
- try {
2598
- const saved = Number(localStorage.getItem(WIDTH_KEY));
2599
- if (Number.isFinite(saved) && saved >= 280) return Math.min(saved, Math.round(window.innerWidth * 0.8));
2600
- } catch {
2601
- // localStorage 不可用:走默认
2602
- }
2603
- return Math.round(window.innerWidth * 0.42);
2604
- });
2605
- react.useEffect(() => {
2606
- const clamp = () => {
2607
- setPanelWidth((prev) => {
2608
- const max = Math.round(window.innerWidth * 0.8);
2609
- return prev > max ? max : prev;
2610
- });
2611
- };
2612
- clamp();
2613
- window.addEventListener("resize", clamp);
2614
- return () => window.removeEventListener("resize", clamp);
2615
- }, []);
2616
- // 015 设置面板:没有本地拖拽记忆时,用 settings 里的默认宽度。
2617
- react.useEffect(() => {
2618
- let hasLocal = false;
2619
- try {
2620
- hasLocal = localStorage.getItem(WIDTH_KEY) !== null;
2621
- } catch {
2622
- // 忽略
2623
- }
2624
- if (hasLocal) return;
2625
- if (!mindmapFace || typeof mindmapFace.readSettings !== "function") return;
2626
- mindmapFace.readSettings().then((v) => {
2627
- const pct = v && typeof v.defaultPanelWidth === "number" ? Math.min(80, Math.max(20, v.defaultPanelWidth)) : 42;
2628
- const px = Math.round(window.innerWidth * pct / 100);
2629
- setPanelWidth((prev) => (Math.abs(prev - px) < 2 ? prev : px));
2630
- }).catch(() => {
2631
- // 读设置失败:保持 42% 默认
2632
- });
2633
- }, [mindmapFace]);
2634
2690
 
2635
- // 015 节点主题:面板每次打开、或设置总线 bump(设置页保存)时重读
2636
- // settings——面板常驻不卸载,光靠 open 变化会漏掉「开着面板改设置」。
2691
+ // 015 节点主题:面板每次可见、或设置总线 bump(设置页保存)时重读
2692
+ // settings——面板常驻不卸载,光靠 visible 变化会漏掉「开着面板改设置」。
2637
2693
  const settingsStamp = react.useSyncExternalStore(settingsBus.subscribe, settingsBus.get);
2638
2694
  const [theme, setTheme] = react.useState({ lineStyle: "elbow", cardStyle: "rounded", colorTheme: "ocean", growthAnimation: true });
2639
2695
  react.useEffect(() => {
2640
- if (!open) return;
2696
+ if (!visible) return;
2641
2697
  if (!mindmapFace || typeof mindmapFace.readSettings !== "function") return;
2642
2698
  mindmapFace.readSettings().then((v) => {
2643
2699
  if (!v) return;
@@ -2651,31 +2707,8 @@ window.__ModuleLoader__.load({
2651
2707
  }).catch(() => {
2652
2708
  // 读设置失败:保持当前主题
2653
2709
  });
2654
- }, [open, settingsStamp, mindmapFace]);
2655
- const dragStateRef = react.useRef(null);
2656
- function startResize(e) {
2657
- e.preventDefault();
2658
- dragStateRef.current = { startX: e.clientX, startWidth: panelWidth, latestWidth: panelWidth };
2659
- const onMove = (ev) => {
2660
- if (!dragStateRef.current) return;
2661
- const max = Math.round(window.innerWidth * 0.8);
2662
- const next = Math.min(max, Math.max(280, dragStateRef.current.startWidth + (dragStateRef.current.startX - ev.clientX)));
2663
- dragStateRef.current.latestWidth = next;
2664
- setPanelWidth(next);
2665
- };
2666
- const onUp = () => {
2667
- try {
2668
- localStorage.setItem(WIDTH_KEY, String(dragStateRef.current ? dragStateRef.current.latestWidth : panelWidth));
2669
- } catch {
2670
- // localStorage 不可用:忽略
2671
- }
2672
- dragStateRef.current = null;
2673
- window.removeEventListener("mousemove", onMove);
2674
- window.removeEventListener("mouseup", onUp);
2675
- };
2676
- window.addEventListener("mousemove", onMove);
2677
- window.addEventListener("mouseup", onUp);
2678
- }
2710
+ }, [visible, settingsStamp, mindmapFace]);
2711
+
2679
2712
  // 013 目录树 tab:常驻第一个 tab(TREE_TAB 哨兵,永不与绝对路径撞名)。
2680
2713
  const TREE_TAB = "__tree__";
2681
2714
  // 013 作者拍板「单脑图模式」:面板只有「目录」与「脑图」两个 tab,
@@ -2697,55 +2730,6 @@ window.__ModuleLoader__.load({
2697
2730
  // 悬停高亮键:树行用 entry.path / node.path,tab 用 TREE_TAB / 文档路径。
2698
2731
  const [hoverKey, setHoverKey] = react.useState(null);
2699
2732
 
2700
- // 007~010 头线对齐(overlay 版回归):面板头部高度动态跟随聊天区头部,
2701
- // 让两者的底部分隔线像素对齐。面板贴视口顶(fixed 宿主层),故
2702
- // 头部高度 = 聊天头部 rect.bottom - 1 - 面板顶(面板顶 ≈ 视口顶)。
2703
- // 主选 wSkVaW_header;结构链回退;合法性钳制 [40,200];失败回退 74(75-1)。
2704
- const panelRootRef = react.useRef(null);
2705
- const FALLBACK_HEADER_HEIGHT = 74;
2706
- const [headerHeight, setHeaderHeight] = react.useState(FALLBACK_HEADER_HEIGHT);
2707
- react.useLayoutEffect(() => {
2708
- const HEADER_MIN = 40;
2709
- const HEADER_MAX = 200;
2710
- const tryPaths = [
2711
- () => document.querySelector('[class*="wSkVaW_header"]'),
2712
- () => {
2713
- const frame = document.querySelector("[data-dsh-frame]");
2714
- if (!frame) return null;
2715
- const center = frame.querySelector('[data-pane="conversation"]');
2716
- return center ? center.firstElementChild : null;
2717
- },
2718
- ];
2719
- const measure = () => {
2720
- for (const path of tryPaths) {
2721
- const el = path();
2722
- if (!el) continue;
2723
- const rect = el.getBoundingClientRect();
2724
- const panelTop = panelRootRef.current
2725
- ? panelRootRef.current.getBoundingClientRect().top
2726
- : rect.top;
2727
- const h = rect.bottom - 1 - panelTop;
2728
- if (h >= HEADER_MIN && h <= HEADER_MAX) {
2729
- setHeaderHeight(Math.round(h * 10) / 10);
2730
- return;
2731
- }
2732
- }
2733
- setHeaderHeight(FALLBACK_HEADER_HEIGHT);
2734
- };
2735
- measure();
2736
- const target = tryPaths[0]() || tryPaths[1]();
2737
- let observer = null;
2738
- if (target && typeof ResizeObserver !== "undefined") {
2739
- observer = new ResizeObserver(measure);
2740
- observer.observe(target);
2741
- }
2742
- window.addEventListener("resize", measure);
2743
- return () => {
2744
- if (observer) observer.disconnect();
2745
- window.removeEventListener("resize", measure);
2746
- };
2747
- }, []);
2748
-
2749
2733
  // 单脑图模式:可见脑图 = 用户当前点选(且未被关闭)的快照/本地文档,
2750
2734
  // 否则跟随最新工具结果;隐藏过的路径不自动回弹(重新点树里文件才恢复)。
2751
2735
  const lastPath = merged.order.length > 0 ? merged.order[merged.order.length - 1] : null;
@@ -2801,14 +2785,14 @@ window.__ModuleLoader__.load({
2801
2785
  : null;
2802
2786
 
2803
2787
  // AI 自动打开:create/open 代表用户明确的「创建 / 打开 / 查看」意图。
2804
- // 无论面板当前是否收起,都展开并切到这次意图对应的文档;首次挂载的
2788
+ // 无论面板/Tab 当前是否可见,都拉起并切到这次意图对应的文档;首次挂载的
2805
2789
  // 历史快照也照常显示最近一次打开的脑图,避免出现「AI 说已打开但面板没了」。
2806
2790
  const seen = react.useRef(null);
2807
2791
  react.useEffect(() => {
2808
2792
  const targetPath = autoOpenTarget(merged, seen.current);
2809
2793
  seen.current = openingEventKeys(merged);
2810
2794
  if (targetPath) {
2811
- onOpen();
2795
+ onAutoOpen();
2812
2796
  setHiddenPath(null);
2813
2797
  setCurrentPath(targetPath);
2814
2798
  setView("mindmap");
@@ -2835,17 +2819,17 @@ window.__ModuleLoader__.load({
2835
2819
  prevIdsRef.current = { path: null, ids: null };
2836
2820
  }, [sessionId]);
2837
2821
  react.useEffect(() => {
2838
- if (!open) return; // 面板收起时不自动发消息(014 overlay 形态守卫)
2822
+ if (!visible) return; // 面板/Tab 不可见时不自动发消息
2839
2823
  if (!sessionId) return;
2840
2824
  if (!active || active === TREE_TAB) return;
2841
2825
  if (!docs.byPath[active]) return; // 本地占位:它的 open 请求已在途
2842
2826
  if (focusPath === active) return;
2843
2827
  if (focusSentRef.current === active) return; // 已发过,等 AI 结果追平
2844
2828
  const rel = fsTree.cwd ? relPathWithin(fsTree.cwd, active, stemOf(active)) : active;
2845
- if (submitEmptyDraft(`用 mindmap_open 打开 ${rel}`)) {
2829
+ if (submitChatCommand(`用 mindmap_open 打开 ${rel}`)) {
2846
2830
  focusSentRef.current = active;
2847
2831
  }
2848
- }, [active, focusPath, fsTree.cwd, docs, open]);
2832
+ }, [active, focusPath, fsTree.cwd, docs, visible]);
2849
2833
 
2850
2834
  async function onExport() {
2851
2835
  if (!tree || !doc || exporting) return;
@@ -2859,21 +2843,16 @@ window.__ModuleLoader__.load({
2859
2843
  setExporting(false);
2860
2844
  }
2861
2845
  }
2862
-
2863
2846
  //#region 013 目录树 tab:懒加载树 + 把指令填进聊天输入框
2864
- // 只有宿主能确认草稿为空时才允许替换;未知/非空均保守放行给用户,
2865
- // 不让目录打开或焦点同步静默丢失正在编辑的消息。
2866
- function draftIsEmpty() {
2867
- if (!inputActions || typeof inputActions.getDraft !== "function") return false;
2868
- try {
2869
- return !String(inputActions.getDraft() ?? "").trim();
2870
- } catch {
2871
- return false;
2872
- }
2847
+ // 草稿保护:确实读到非空草稿才让路(改走剪贴板),读不到就按既有
2848
+ // 行为直填直发——宿主不暴露草稿时不能把功能整个卡死。
2849
+ function draftBlocked() {
2850
+ return draftBlocksAutoSend(inputActions);
2873
2851
  }
2874
2852
 
2875
- function submitEmptyDraft(text) {
2876
- if (!draftIsEmpty() || typeof inputActions.setDraft !== "function" || typeof inputActions.submit !== "function") return false;
2853
+ function submitChatCommand(text) {
2854
+ if (draftBlocked()) return false;
2855
+ if (!inputActions || typeof inputActions.setDraft !== "function" || typeof inputActions.submit !== "function") return false;
2877
2856
  try {
2878
2857
  inputActions.setDraft(text);
2879
2858
  inputActions.submit();
@@ -2883,10 +2862,10 @@ window.__ModuleLoader__.load({
2883
2862
  }
2884
2863
  }
2885
2864
 
2886
- // 手动新建指令也不能覆盖草稿;空草稿时填入,否则复制到剪贴板。
2865
+ // 手动新建指令同样不覆盖已有草稿;否则填入输入框。
2887
2866
  function fillDraft(text) {
2888
2867
  try {
2889
- if (draftIsEmpty() && typeof inputActions.setDraft === "function") {
2868
+ if (!draftBlocked() && inputActions && typeof inputActions.setDraft === "function") {
2890
2869
  inputActions.setDraft(text);
2891
2870
  setFilledHint("指令已填入聊天输入框");
2892
2871
  return;
@@ -2920,8 +2899,14 @@ window.__ModuleLoader__.load({
2920
2899
  // 说即可继续编辑(002 数据流不变:内容只来自 AI 工具结果)。
2921
2900
  function openMindmap(entry) {
2922
2901
  const text = `用 mindmap_open 打开 ${relPathWithin(fsTree.cwd, entry.path, entry.name)}`;
2923
- if (!submitEmptyDraft(text)) {
2924
- setFilledHint("为保护未发送草稿,未自动打开。请发送或清空草稿后重试");
2902
+ // 有未发送草稿:不建占位、不抢输入框,改把指令交给用户自己发。
2903
+ if (draftBlocked()) {
2904
+ if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
2905
+ navigator.clipboard.writeText(text).catch(() => {});
2906
+ setFilledHint("检测到未发送的草稿,已保留;打开指令已复制到剪贴板,粘贴发送即可");
2907
+ } else {
2908
+ setFilledHint(`检测到未发送的草稿,已保留。请手动发送:${text}`);
2909
+ }
2925
2910
  return;
2926
2911
  }
2927
2912
  // 016:记录点击时刻的错误基线(errorByPath 与 latestError 的全部
@@ -2943,9 +2928,19 @@ window.__ModuleLoader__.load({
2943
2928
  renamedFrom: null,
2944
2929
  },
2945
2930
  }));
2946
- // 指令已在占位前提交;标记避免焦点同步对同一路径重复发送。
2947
- focusSentRef.current = entry.path;
2948
- setFilledHint(`已让 AI 打开「${entry.name}」,在聊天里继续说就能继续编辑`);
2931
+ // ② AI 就位:填指令并直接提交(失败降级剪贴板)。
2932
+ if (submitChatCommand(text)) {
2933
+ // 标记已发,避免焦点同步 effect 对同一路径重复发送。
2934
+ focusSentRef.current = entry.path;
2935
+ setFilledHint(`已让 AI 打开「${entry.name}」,在聊天里继续说就能继续编辑`);
2936
+ return;
2937
+ }
2938
+ if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
2939
+ navigator.clipboard.writeText(text).catch(() => {});
2940
+ setFilledHint("已复制指令到剪贴板,请粘贴到聊天输入框");
2941
+ return;
2942
+ }
2943
+ setFilledHint(text);
2949
2944
  }
2950
2945
 
2951
2946
  /** 016 加载态恢复:错误/超时后重试——重发打开指令并重启看门狗。 */
@@ -3038,6 +3033,30 @@ window.__ModuleLoader__.load({
3038
3033
  [fsTree.nodes, fsTree.expanded],
3039
3034
  );
3040
3035
 
3036
+ // 内嵌文件夹使用 14px 线框图标,Markdown 使用 M 徽标。
3037
+ // 独立目录继续使用原有 emoji / M 徽标。
3038
+ function sidebarTreeIcon(folder, expanded = false) {
3039
+ const outline = folder
3040
+ ? (expanded ? "M2 6V3h4l2 2h4v2M2 6h11l-2 6H1z" : "M1.5 3h4l2 2h5v7h-11z")
3041
+ : "M3 1.5h5l3 3V12.5H3z M8 1.5v3h3";
3042
+ return (0, react_jsx_runtime.jsx)("svg", {
3043
+ width: 14, height: 14, viewBox: "0 0 14 14", fill: "none",
3044
+ stroke: "currentColor", strokeWidth: 1, strokeLinejoin: "round", strokeLinecap: "round",
3045
+ style: { flex: "none" }, "aria-hidden": true, focusable: "false",
3046
+ children: (0, react_jsx_runtime.jsx)("path", { d: outline }),
3047
+ });
3048
+ }
3049
+ function directoryLabel(name, expanded) {
3050
+ return (0, react_jsx_runtime.jsx)("span", {
3051
+ style: { flex: "0 1 auto", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 },
3052
+ children: variant === "sidebar" ? name : `${expanded ? "📂" : "📁"} ${name}`,
3053
+ });
3054
+ }
3055
+ function blockFileInteraction(event) {
3056
+ event.preventDefault();
3057
+ event.stopPropagation();
3058
+ }
3059
+
3041
3060
  function renderTreeRow(row) {
3042
3061
  if (row.kind === "dir") {
3043
3062
  const node = row.node;
@@ -3065,10 +3084,8 @@ window.__ModuleLoader__.load({
3065
3084
  },
3066
3085
  children: [
3067
3086
  (0, react_jsx_runtime.jsx)("span", { style: S.treeCaret, children: expandedNow ? "▾" : "▸" }),
3068
- (0, react_jsx_runtime.jsx)("span", {
3069
- style: { flex: "0 1 auto", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 },
3070
- children: expandedNow ? `📂 ${node.name}` : `📁 ${node.name}`,
3071
- }),
3087
+ variant === "sidebar" ? sidebarTreeIcon(true, expandedNow) : null,
3088
+ directoryLabel(node.name, expandedNow),
3072
3089
  node.truncated ? (0, react_jsx_runtime.jsx)("span", { style: S.treeCaret, children: "…" }) : null,
3073
3090
  // 根行行内右侧的「刷新」(013:不占独立一行)。
3074
3091
  isRoot ? (0, react_jsx_runtime.jsx)("span", { style: S.spacer }) : null,
@@ -3091,8 +3108,9 @@ window.__ModuleLoader__.load({
3091
3108
  const entry = row.entry;
3092
3109
  const depthPad = row.depth * 16;
3093
3110
  const isMd = /\.md$/i.test(entry.name);
3111
+ const inactiveFile = variant === "sidebar" && !entry.isDir && !isMd;
3094
3112
  const expandedNow = entry.isDir && Boolean(fsTree.expanded[entry.path]);
3095
- const hovered = hoverKey === entry.path;
3113
+ const hovered = !inactiveFile && hoverKey === entry.path;
3096
3114
  const style = {
3097
3115
  ...S.treeRow,
3098
3116
  paddingLeft: depthPad,
@@ -3100,6 +3118,7 @@ window.__ModuleLoader__.load({
3100
3118
  ...(isMd ? S.treeRowMd : entry.isDir ? {} : S.treeRowOther),
3101
3119
  ...(entry.hidden ? { opacity: 0.6 } : {}),
3102
3120
  ...(hovered ? S.treeRowHover : {}),
3121
+ ...(inactiveFile ? { userSelect: "none" } : {}),
3103
3122
  };
3104
3123
  if (entry.isDir) {
3105
3124
  return (0, react_jsx_runtime.jsxs)("div", {
@@ -3121,25 +3140,34 @@ window.__ModuleLoader__.load({
3121
3140
  },
3122
3141
  children: [
3123
3142
  (0, react_jsx_runtime.jsx)("span", { style: S.treeCaret, children: expandedNow ? "▾" : "▸" }),
3124
- (0, react_jsx_runtime.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 }, children: expandedNow ? `📂 ${entry.name}` : `📁 ${entry.name}` }),
3143
+ variant === "sidebar" ? sidebarTreeIcon(true, expandedNow) : null,
3144
+ directoryLabel(entry.name, expandedNow),
3125
3145
  ],
3126
3146
  });
3127
3147
  }
3128
3148
  return (0, react_jsx_runtime.jsxs)("div", {
3129
3149
  key: entry.path,
3130
3150
  style,
3131
- title: isMd ? `打开脑图:${entry.path}` : entry.path,
3132
- onClick: isMd ? () => openMindmap(entry) : undefined,
3133
- onMouseEnter: () => setHoverKey(entry.path),
3134
- onMouseLeave: () => setHoverKey((k) => (k === entry.path ? null : k)),
3151
+ title: inactiveFile ? undefined : isMd ? `打开脑图:${entry.path}` : entry.path,
3152
+ "aria-disabled": inactiveFile ? true : undefined,
3153
+ draggable: inactiveFile ? false : undefined,
3154
+ // 纯展示文件仍接住事件,避免穿透到宿主或空白处的新建菜单。
3155
+ onClick: inactiveFile ? blockFileInteraction : isMd ? () => openMindmap(entry) : undefined,
3156
+ onDoubleClick: inactiveFile ? blockFileInteraction : undefined,
3157
+ onMouseDown: inactiveFile ? blockFileInteraction : undefined,
3158
+ onDragStart: inactiveFile ? blockFileInteraction : undefined,
3159
+ onMouseEnter: inactiveFile ? undefined : () => setHoverKey(entry.path),
3160
+ onMouseLeave: inactiveFile ? undefined : () => setHoverKey((k) => (k === entry.path ? null : k)),
3135
3161
  // 右键:.md 不弹菜单(左键即打开);非 .md 只拦掉默认菜单。
3136
3162
  onContextMenu: (e) => {
3137
3163
  e.preventDefault();
3138
3164
  e.stopPropagation();
3139
3165
  },
3140
3166
  children: [
3167
+ variant === "sidebar" ? (0, react_jsx_runtime.jsx)("span", { style: S.treeCaret, "aria-hidden": true }) : null,
3141
3168
  isMd
3142
- ? (0, react_jsx_runtime.jsx)("span", { style: S.mdBadge, children: "M" })
3169
+ ? (0, react_jsx_runtime.jsx)("span", { style: S.mdBadge, "aria-hidden": true, children: "M" })
3170
+ : variant === "sidebar" ? sidebarTreeIcon(false)
3143
3171
  : (0, react_jsx_runtime.jsx)("span", { style: S.fileDot, children: (0, react_jsx_runtime.jsx)("span", { style: S.fileDotCore }) }),
3144
3172
  (0, react_jsx_runtime.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 }, children: entry.name }),
3145
3173
  ],
@@ -3260,118 +3288,636 @@ window.__ModuleLoader__.load({
3260
3288
  ] });
3261
3289
  }
3262
3290
  //#endregion
3263
-
3264
- // 014 布局让位:面板打开/拖宽时把宽度写进 CSS 变量,挤窄 #root 推走
3265
- // 聊天区(better-sidebar 同款);关闭/卸载时移除变量恢复全宽。
3266
- react.useLayoutEffect(() => {
3267
- if (typeof document === "undefined") return;
3268
- if (open) {
3269
- document.documentElement.style.setProperty("--dsh-mindmap-width", `${panelWidth}px`);
3270
- } else {
3271
- document.documentElement.style.removeProperty("--dsh-mindmap-width");
3272
- }
3273
- return () => {
3274
- document.documentElement.style.removeProperty("--dsh-mindmap-width");
3275
- };
3276
- }, [open, panelWidth]);
3277
-
3278
- if (!open) return null;
3279
- // 014 overlay 外壳:fixed 宿主层(点击穿透)套右缘贴边全高悬浮面板,
3280
- // 左缘拖拽调宽([280, 视口 80%],localStorage 持久化);遮盖聊天区是
3281
- // 该形态的已知代价(作者拍板,见 docs/014)。宿主层挂在 header 槽位里,
3282
- // better-sidebar 同款「fixed 自举」思路。
3283
- return (0, react_jsx_runtime.jsx)("div", { style: S.panelHost, children: (0, react_jsx_runtime.jsxs)("div", { ref: panelRootRef, style: { ...S.overlayRoot, width: panelWidth }, children: [
3284
- (0, react_jsx_runtime.jsx)("div", { style: S.overlayHandle, onMouseDown: startResize }),
3285
- (0, react_jsx_runtime.jsxs)("div", { style: { ...S.header, height: `${headerHeight - 1}px` }, children: [
3286
- (0, react_jsx_runtime.jsxs)("div", { style: S.headerTop, children: [
3287
- (0, react_jsx_runtime.jsx)("span", { style: S.spacer }),
3291
+ //#region 026 工作区渲染:壳无关的内容(头部 tab 行 + 导出 + 画布/树/加载态)
3292
+ // 027 内嵌头部视觉对齐:variant="sidebar" Better Sidebar 外层已有 Tab 头部,
3293
+ // 内嵌只保留一行紧凑工具栏——「脑图列表」标签 + 当前脑图标签 + 导出图片按钮
3294
+ //(导出在行尾,不再独占一行)。variant="standalone" 时保持原双层头部(headerTop
3295
+ // spacer + 导出 + 关闭 → tabRow)不变。
3296
+ // 不可见时仍挂载(hooks 已在上文跑完),只跳过 JSX——auto-open effect
3297
+ // 在 visible=false 时仍能调 onAutoOpen 把壳拉起。
3298
+ if (!visible) return null;
3299
+ // headerHeight 由独立 fixed 壳传入(对齐聊天头部底部分隔线);
3300
+ // BS Tab 模式传 null → 头部高度自适应(Better Sidebar 管自己的外壳)。
3301
+ const wsHeaderStyle = headerHeight != null ? { ...S.header, height: `${headerHeight - 1}px` } : S.header;
3302
+
3303
+ // 027 导出按钮(两种模式共用):disabled 语义 = 无树 / 导出中 / 本地占位。
3304
+ const exportBtn = (0, react_jsx_runtime.jsx)("button", {
3305
+ type: "button",
3306
+ style: S.action,
3307
+ disabled: !tree || exporting || (doc && doc.op === "local"),
3308
+ onClick: onExport,
3309
+ children: exporting ? "导出中…" : "导出图片",
3310
+ });
3311
+ const exportErrorSpan = exportError
3312
+ ? (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-error)", fontSize: "12px" }, children: exportError })
3313
+ : null;
3314
+
3315
+ // 027 目录/列表标签文案:sidebar 模式叫「脑图列表」,standalone 模式叫「目录」。
3316
+ const treeTabLabel = variant === "sidebar" ? "脑图列表" : "目录";
3317
+
3318
+ if (variant === "sidebar") {
3319
+ // 027 sidebar 模式:单行紧凑工具栏。BS 外层已有 Tab 头部与关闭按钮,
3320
+ // 内嵌不再加重复外壳标题或关闭按钮。
3321
+ return (0, react_jsx_runtime.jsxs)("div", { style: S.sbRoot, children: [
3322
+ (0, react_jsx_runtime.jsxs)("div", { style: S.sbToolbar, children: [
3323
+ (0, react_jsx_runtime.jsx)("button", {
3324
+ type: "button",
3325
+ style: active === TREE_TAB ? { ...S.sbTab, ...S.sbTabActive } : (hoverKey === TREE_TAB ? { ...S.sbTab, ...S.tabHover } : S.sbTab),
3326
+ title: fsTree.cwd ?? "工作目录",
3327
+ onClick: () => setView("tree"),
3328
+ onMouseEnter: () => setHoverKey(TREE_TAB),
3329
+ onMouseLeave: () => setHoverKey((k) => (k === TREE_TAB ? null : k)),
3330
+ onContextMenu: (e) => {
3331
+ e.preventDefault();
3332
+ e.stopPropagation();
3333
+ setTabMenu({ x: e.clientX, y: e.clientY, path: TREE_TAB });
3334
+ },
3335
+ children: treeTabLabel,
3336
+ }, TREE_TAB),
3337
+ shown ? (0, react_jsx_runtime.jsxs)("span", {
3338
+ key: shown,
3339
+ style: { ...S.sbTabWrap, ...(active !== TREE_TAB ? S.sbTabActive : {}), ...(active === TREE_TAB && hoverKey === shown ? S.tabHover : {}) },
3340
+ onMouseEnter: () => setHoverKey(shown),
3341
+ onMouseLeave: () => setHoverKey((k) => (k === shown ? null : k)),
3342
+ onContextMenu: (e) => {
3343
+ e.preventDefault();
3344
+ e.stopPropagation();
3345
+ setTabMenu({ x: e.clientX, y: e.clientY, path: shown });
3346
+ },
3347
+ children: [
3288
3348
  (0, react_jsx_runtime.jsx)("button", {
3289
3349
  type: "button",
3290
- style: S.action,
3291
- disabled: !tree || exporting || (doc && doc.op === "local"),
3292
- onClick: onExport,
3293
- children: exporting ? "导出中…" : "导出图片",
3350
+ style: S.sbTabTitle,
3351
+ title: shown,
3352
+ onClick: () => setView("mindmap"),
3353
+ children: merged.byPath[shown].rootTitle,
3294
3354
  }),
3295
- exportError ? (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-error)", fontSize: "12px" } , children: exportError }) : null,
3296
3355
  (0, react_jsx_runtime.jsx)("button", {
3297
3356
  type: "button",
3298
- style: S.action,
3299
- title: "收起脑图面板",
3300
- onClick: () => onClose(),
3357
+ style: S.sbTabClose,
3358
+ title: "关闭脑图",
3359
+ onClick: () => closeMindmap(shown),
3301
3360
  children: "✕",
3302
3361
  }),
3303
- ] }),
3304
- (0, react_jsx_runtime.jsxs)("div", { style: S.tabRow, children: [
3305
- (0, react_jsx_runtime.jsx)("button", {
3306
- type: "button",
3307
- style: active === TREE_TAB ? { ...S.tab, ...S.tabActive } : (hoverKey === TREE_TAB ? { ...S.tab, ...S.tabHover } : S.tab),
3308
- title: fsTree.cwd ?? "工作目录",
3309
- onClick: () => setView("tree"),
3310
- onMouseEnter: () => setHoverKey(TREE_TAB),
3311
- onMouseLeave: () => setHoverKey((k) => (k === TREE_TAB ? null : k)),
3312
- onContextMenu: (e) => {
3313
- e.preventDefault();
3314
- e.stopPropagation();
3315
- setTabMenu({ x: e.clientX, y: e.clientY, path: TREE_TAB });
3316
- },
3317
- children: "目录",
3318
- }, TREE_TAB),
3319
- shown ? (0, react_jsx_runtime.jsxs)("span", {
3320
- key: shown,
3321
- style: { ...S.tabWrap, ...(active !== TREE_TAB ? S.tabActive : {}), ...(active === TREE_TAB && hoverKey === shown ? S.tabHover : {}) },
3322
- onMouseEnter: () => setHoverKey(shown),
3323
- onMouseLeave: () => setHoverKey((k) => (k === shown ? null : k)),
3324
- onContextMenu: (e) => {
3325
- e.preventDefault();
3326
- e.stopPropagation();
3327
- setTabMenu({ x: e.clientX, y: e.clientY, path: shown });
3328
- },
3329
- children: [
3330
- (0, react_jsx_runtime.jsx)("button", {
3331
- type: "button",
3332
- style: S.tabTitle,
3333
- title: shown,
3334
- onClick: () => setView("mindmap"),
3335
- children: merged.byPath[shown].rootTitle,
3336
- }),
3337
- (0, react_jsx_runtime.jsx)("button", {
3338
- type: "button",
3339
- style: S.tabClose,
3340
- title: "关闭脑图",
3341
- onClick: () => closeMindmap(shown),
3342
- children: "✕",
3343
- }),
3344
- ],
3345
- }, shown) : null,
3346
- ] }),
3347
- ] }),
3348
- // 016:脑图视图走 MindmapCanvas(自带滚动 + 居中 + 右上角缩放控制条),
3349
- // 不再套 S.body(避免嵌套滚动容器与双重 padding);目录/加载/空态保持原样。
3350
- active === TREE_TAB || (doc && doc.op === "local") || !tree
3351
- ? (0, react_jsx_runtime.jsx)("div", { style: S.body, children: active === TREE_TAB
3352
- ? renderTree()
3353
- : (doc && doc.op === "local")
3354
- ? renderLoading()
3355
- : renderTree() })
3356
- : (0, react_jsx_runtime.jsx)(MindmapCanvas, { node: tree, theme, fitKey: doc && doc.path, reveal }),
3357
- tabMenu ? (0, react_jsx_runtime.jsxs)("div", {
3358
- style: { ...S.treeMenu, left: tabMenu.x, top: tabMenu.y },
3359
- onContextMenu: (e) => e.preventDefault(),
3360
- children: [
3361
- (0, react_jsx_runtime.jsx)("button", {
3362
- type: "button",
3363
- style: S.treeMenuItem,
3364
- onClick: () => {
3365
- setTabMenu(null);
3366
- if (tabMenu.path === TREE_TAB) loadTree(undefined);
3367
- else closeMindmap(tabMenu.path);
3368
- },
3369
- children: tabMenu.path === TREE_TAB ? "刷新目录树" : "关闭脑图",
3370
- }),
3371
3362
  ],
3372
- }) : null,
3363
+ }, shown) : null,
3364
+ // 导出按钮 + 错误推到行尾。
3365
+ (0, react_jsx_runtime.jsx)("span", { style: S.spacer }),
3366
+ exportErrorSpan,
3367
+ exportBtn,
3368
+ ] }),
3369
+ // 016:脑图视图走 MindmapCanvas(自带滚动 + 居中 + 右上角缩放控制条),
3370
+ // 不再套 S.body(避免嵌套滚动容器与双重 padding);目录/加载/空态保持原样。
3371
+ active === TREE_TAB || (doc && doc.op === "local") || !tree
3372
+ ? (0, react_jsx_runtime.jsx)("div", { style: S.body, children: active === TREE_TAB
3373
+ ? renderTree()
3374
+ : (doc && doc.op === "local")
3375
+ ? renderLoading()
3376
+ : renderTree() })
3377
+ : (0, react_jsx_runtime.jsx)(MindmapCanvas, { node: tree, theme, fitKey: doc && doc.path, reveal }),
3378
+ tabMenu ? (0, react_jsx_runtime.jsxs)("div", {
3379
+ style: { ...S.treeMenu, left: tabMenu.x, top: tabMenu.y },
3380
+ onContextMenu: (e) => e.preventDefault(),
3381
+ children: [
3382
+ (0, react_jsx_runtime.jsx)("button", {
3383
+ type: "button",
3384
+ style: S.treeMenuItem,
3385
+ onClick: () => {
3386
+ setTabMenu(null);
3387
+ if (tabMenu.path === TREE_TAB) loadTree(undefined);
3388
+ else closeMindmap(tabMenu.path);
3389
+ },
3390
+ children: tabMenu.path === TREE_TAB ? "刷新脑图列表" : "关闭脑图",
3391
+ }),
3392
+ ],
3393
+ }) : null,
3394
+ ] });
3395
+ }
3396
+
3397
+ // 027 standalone 模式:原双层头部(headerTop spacer + 导出 + 关闭 → tabRow)不变。
3398
+ const wsHeaderChildren = [
3399
+ (0, react_jsx_runtime.jsxs)("div", { style: S.headerTop, children: [
3400
+ (0, react_jsx_runtime.jsx)("span", { style: S.spacer }),
3401
+ exportBtn,
3402
+ exportErrorSpan,
3403
+ // 关闭按钮:仅独立 fixed 壳提供 onClose(BS Tab 自带关闭)。
3404
+ onClose ? (0, react_jsx_runtime.jsx)("button", {
3405
+ type: "button",
3406
+ style: S.action,
3407
+ title: "收起脑图面板",
3408
+ onClick: () => onClose(),
3409
+ children: "✕",
3410
+ }) : null,
3411
+ ] }),
3412
+ (0, react_jsx_runtime.jsxs)("div", { style: S.tabRow, children: [
3413
+ (0, react_jsx_runtime.jsx)("button", {
3414
+ type: "button",
3415
+ style: active === TREE_TAB ? { ...S.tab, ...S.tabActive } : (hoverKey === TREE_TAB ? { ...S.tab, ...S.tabHover } : S.tab),
3416
+ title: fsTree.cwd ?? "工作目录",
3417
+ onClick: () => setView("tree"),
3418
+ onMouseEnter: () => setHoverKey(TREE_TAB),
3419
+ onMouseLeave: () => setHoverKey((k) => (k === TREE_TAB ? null : k)),
3420
+ onContextMenu: (e) => {
3421
+ e.preventDefault();
3422
+ e.stopPropagation();
3423
+ setTabMenu({ x: e.clientX, y: e.clientY, path: TREE_TAB });
3424
+ },
3425
+ children: treeTabLabel,
3426
+ }, TREE_TAB),
3427
+ shown ? (0, react_jsx_runtime.jsxs)("span", {
3428
+ key: shown,
3429
+ style: { ...S.tabWrap, ...(active !== TREE_TAB ? S.tabActive : {}), ...(active === TREE_TAB && hoverKey === shown ? S.tabHover : {}) },
3430
+ onMouseEnter: () => setHoverKey(shown),
3431
+ onMouseLeave: () => setHoverKey((k) => (k === shown ? null : k)),
3432
+ onContextMenu: (e) => {
3433
+ e.preventDefault();
3434
+ e.stopPropagation();
3435
+ setTabMenu({ x: e.clientX, y: e.clientY, path: shown });
3436
+ },
3437
+ children: [
3438
+ (0, react_jsx_runtime.jsx)("button", {
3439
+ type: "button",
3440
+ style: S.tabTitle,
3441
+ title: shown,
3442
+ onClick: () => setView("mindmap"),
3443
+ children: merged.byPath[shown].rootTitle,
3444
+ }),
3445
+ (0, react_jsx_runtime.jsx)("button", {
3446
+ type: "button",
3447
+ style: S.tabClose,
3448
+ title: "关闭脑图",
3449
+ onClick: () => closeMindmap(shown),
3450
+ children: "✕",
3451
+ }),
3452
+ ],
3453
+ }, shown) : null,
3454
+ ] }),
3455
+ ];
3456
+ return (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0 }, children: [
3457
+ (0, react_jsx_runtime.jsx)("div", { style: wsHeaderStyle, children: wsHeaderChildren }),
3458
+ // 016:脑图视图走 MindmapCanvas(自带滚动 + 居中 + 右上角缩放控制条),
3459
+ // 不再套 S.body(避免嵌套滚动容器与双重 padding);目录/加载/空态保持原样。
3460
+ active === TREE_TAB || (doc && doc.op === "local") || !tree
3461
+ ? (0, react_jsx_runtime.jsx)("div", { style: S.body, children: active === TREE_TAB
3462
+ ? renderTree()
3463
+ : (doc && doc.op === "local")
3464
+ ? renderLoading()
3465
+ : renderTree() })
3466
+ : (0, react_jsx_runtime.jsx)(MindmapCanvas, { node: tree, theme, fitKey: doc && doc.path, reveal }),
3467
+ tabMenu ? (0, react_jsx_runtime.jsxs)("div", {
3468
+ style: { ...S.treeMenu, left: tabMenu.x, top: tabMenu.y },
3469
+ onContextMenu: (e) => e.preventDefault(),
3470
+ children: [
3471
+ (0, react_jsx_runtime.jsx)("button", {
3472
+ type: "button",
3473
+ style: S.treeMenuItem,
3474
+ onClick: () => {
3475
+ setTabMenu(null);
3476
+ if (tabMenu.path === TREE_TAB) loadTree(undefined);
3477
+ else closeMindmap(tabMenu.path);
3478
+ },
3479
+ children: tabMenu.path === TREE_TAB ? "刷新目录树" : "关闭脑图",
3480
+ }),
3481
+ ],
3482
+ }) : null,
3483
+ ] });
3484
+ }
3485
+ //#endregion
3486
+
3487
+ /**
3488
+ * 独立 fixed 壳(026 拆分):仅 Better Sidebar 未安装或服务不可用时使用。
3489
+ * 管理壳特有几何——右缘贴边悬浮、左缘拖拽调宽(localStorage 持久化)、
3490
+ * 头部高度对齐聊天区、layout-push CSS 变量(--dsh-mindmap-width)。
3491
+ * 壳内始终挂载 MindmapWorkspace(visible=open):收起时 display:none 隐藏,
3492
+ * 但 hooks 照常跑——auto-open effect 能在面板关着时调 onAutoOpen 把它拉起。
3493
+ */
3494
+ function MindmapDetailsPanel(props) {
3495
+ const { mindmapFace, open, sessionId, inputActions, nodes, nodesVersion, onOpen, onClose } = props;
3496
+ // 014 overlay 宽度:localStorage 持久化,拖拽钳制 [280, 视口 80%]。
3497
+ // 窗口尺寸变化时持续钳制——只在挂载时压一次的话,窗口先放大→拖宽
3498
+ // 面板→再缩小会让面板保持旧像素宽,聊天区被挤没。
3499
+ const WIDTH_KEY = "dsh-mindmap.overlay-width";
3500
+ const [panelWidth, setPanelWidth] = react.useState(() => {
3501
+ try {
3502
+ const saved = Number(localStorage.getItem(WIDTH_KEY));
3503
+ if (Number.isFinite(saved) && saved >= 280) return Math.min(saved, Math.round(window.innerWidth * 0.8));
3504
+ } catch {
3505
+ // localStorage 不可用:走默认
3506
+ }
3507
+ return Math.round(window.innerWidth * 0.42);
3508
+ });
3509
+ react.useEffect(() => {
3510
+ const clamp = () => {
3511
+ setPanelWidth((prev) => {
3512
+ const max = Math.round(window.innerWidth * 0.8);
3513
+ return prev > max ? max : prev;
3514
+ });
3515
+ };
3516
+ clamp();
3517
+ window.addEventListener("resize", clamp);
3518
+ return () => window.removeEventListener("resize", clamp);
3519
+ }, []);
3520
+ // 015 设置面板:没有本地拖拽记忆时,用 settings 里的默认宽度。
3521
+ react.useEffect(() => {
3522
+ let hasLocal = false;
3523
+ try {
3524
+ hasLocal = localStorage.getItem(WIDTH_KEY) !== null;
3525
+ } catch {
3526
+ // 忽略
3527
+ }
3528
+ if (hasLocal) return;
3529
+ if (!mindmapFace || typeof mindmapFace.readSettings !== "function") return;
3530
+ mindmapFace.readSettings().then((v) => {
3531
+ const pct = v && typeof v.defaultPanelWidth === "number" ? Math.min(80, Math.max(20, v.defaultPanelWidth)) : 42;
3532
+ const px = Math.round(window.innerWidth * pct / 100);
3533
+ setPanelWidth((prev) => (Math.abs(prev - px) < 2 ? prev : px));
3534
+ }).catch(() => {
3535
+ // 读设置失败:保持 42% 默认
3536
+ });
3537
+ }, [mindmapFace]);
3538
+
3539
+ const dragStateRef = react.useRef(null);
3540
+ function startResize(e) {
3541
+ e.preventDefault();
3542
+ dragStateRef.current = { startX: e.clientX, startWidth: panelWidth, latestWidth: panelWidth };
3543
+ const onMove = (ev) => {
3544
+ if (!dragStateRef.current) return;
3545
+ const max = Math.round(window.innerWidth * 0.8);
3546
+ const next = Math.min(max, Math.max(280, dragStateRef.current.startWidth + (dragStateRef.current.startX - ev.clientX)));
3547
+ dragStateRef.current.latestWidth = next;
3548
+ setPanelWidth(next);
3549
+ };
3550
+ const onUp = () => {
3551
+ try {
3552
+ localStorage.setItem(WIDTH_KEY, String(dragStateRef.current ? dragStateRef.current.latestWidth : panelWidth));
3553
+ } catch {
3554
+ // localStorage 不可用:忽略
3555
+ }
3556
+ dragStateRef.current = null;
3557
+ window.removeEventListener("mousemove", onMove);
3558
+ window.removeEventListener("mouseup", onUp);
3559
+ };
3560
+ window.addEventListener("mousemove", onMove);
3561
+ window.addEventListener("mouseup", onUp);
3562
+ }
3563
+
3564
+ // 007~010 头线对齐(overlay 版回归):面板头部高度动态跟随聊天区头部,
3565
+ // 让两者的底部分隔线像素对齐。面板贴视口顶(fixed 宿主层),故
3566
+ // 头部高度 = 聊天头部 rect.bottom - 1 - 面板顶(面板顶 ≈ 视口顶)。
3567
+ // 主选 wSkVaW_header;结构链回退;合法性钳制 [40,200];失败回退 74(75-1)。
3568
+ const panelRootRef = react.useRef(null);
3569
+ const FALLBACK_HEADER_HEIGHT = 74;
3570
+ const [headerHeight, setHeaderHeight] = react.useState(FALLBACK_HEADER_HEIGHT);
3571
+ react.useLayoutEffect(() => {
3572
+ const HEADER_MIN = 40;
3573
+ const HEADER_MAX = 200;
3574
+ const tryPaths = [
3575
+ () => document.querySelector('[class*="wSkVaW_header"]'),
3576
+ () => {
3577
+ const frame = document.querySelector("[data-dsh-frame]");
3578
+ if (!frame) return null;
3579
+ const center = frame.querySelector('[data-pane="conversation"]');
3580
+ return center ? center.firstElementChild : null;
3581
+ },
3582
+ ];
3583
+ const measure = () => {
3584
+ for (const path of tryPaths) {
3585
+ const el = path();
3586
+ if (!el) continue;
3587
+ const rect = el.getBoundingClientRect();
3588
+ const panelTop = panelRootRef.current
3589
+ ? panelRootRef.current.getBoundingClientRect().top
3590
+ : rect.top;
3591
+ const h = rect.bottom - 1 - panelTop;
3592
+ if (h >= HEADER_MIN && h <= HEADER_MAX) {
3593
+ setHeaderHeight(Math.round(h * 10) / 10);
3594
+ return;
3595
+ }
3596
+ }
3597
+ setHeaderHeight(FALLBACK_HEADER_HEIGHT);
3598
+ };
3599
+ measure();
3600
+ const target = tryPaths[0]() || tryPaths[1]();
3601
+ let observer = null;
3602
+ if (target && typeof ResizeObserver !== "undefined") {
3603
+ observer = new ResizeObserver(measure);
3604
+ observer.observe(target);
3605
+ }
3606
+ window.addEventListener("resize", measure);
3607
+ return () => {
3608
+ if (observer) observer.disconnect();
3609
+ window.removeEventListener("resize", measure);
3610
+ };
3611
+ }, []);
3612
+
3613
+ // 014 布局让位:面板打开/拖宽时把宽度写进 CSS 变量,挤窄 #root 推走
3614
+ // 聊天区(better-sidebar 同款);关闭/卸载时移除变量恢复全宽。
3615
+ // 仅独立壳模式启用——BS Tab 模式不渲染本组件,不写此变量。
3616
+ react.useLayoutEffect(() => {
3617
+ if (typeof document === "undefined") return;
3618
+ if (open) {
3619
+ document.documentElement.style.setProperty("--dsh-mindmap-width", `${panelWidth}px`);
3620
+ } else {
3621
+ document.documentElement.style.removeProperty("--dsh-mindmap-width");
3622
+ }
3623
+ return () => {
3624
+ document.documentElement.style.removeProperty("--dsh-mindmap-width");
3625
+ };
3626
+ }, [open, panelWidth]);
3627
+
3628
+ // 始终挂载 MindmapWorkspace:收起时用 display:none 隐藏外壳,
3629
+ // 但组件实例保留——hooks(auto-open / 焦点同步 / 目录树)照常跑。
3630
+ // 切换 open 时 MindmapWorkspace 在 children 数组里的位置不变(index 1),
3631
+ // React 保持实例不卸载,state 不丢失。
3632
+ const workspace = (0, react_jsx_runtime.jsx)(MindmapWorkspace, {
3633
+ sessionId,
3634
+ nodes,
3635
+ nodesVersion,
3636
+ inputActions,
3637
+ mindmapFace,
3638
+ visible: open,
3639
+ onAutoOpen: onOpen,
3640
+ onClose,
3641
+ headerHeight,
3642
+ variant: "standalone",
3643
+ });
3644
+ return (0, react_jsx_runtime.jsx)("div", { style: open ? S.panelHost : { display: "none" }, children: (0, react_jsx_runtime.jsxs)("div", { ref: panelRootRef, style: open ? { ...S.overlayRoot, width: panelWidth } : { display: "none" }, children: [
3645
+ open ? (0, react_jsx_runtime.jsx)("div", { style: S.overlayHandle, onMouseDown: startResize }) : null,
3646
+ workspace,
3373
3647
  ] }) });
3374
3648
  }
3649
+
3650
+ /**
3651
+ * Better Sidebar Tab 壳(026):Better Sidebar 服务可用时,apply() 注册
3652
+ * 此组件为单实例 Tab(id = dsh-mindmap:mindmap)。它从 sessionStore 读取
3653
+ * 头部槽位(MindmapSlot)捕获的 nodes/nodesVersion/inputActions/mindmapFace,
3654
+ * 传给壳无关的 MindmapWorkspace 渲染。
3655
+ *
3656
+ * TabComponentProps(由 Better Sidebar 传入):{ ctx, scope, tab, visible, ... }
3657
+ * visible = false 时组件仍挂载(BS 性能门控),MindmapWorkspace 的 hooks
3658
+ * 照常跑——auto-open 能在 Tab 不可见时调 onAutoOpen → openTab 把它拉起。
3659
+ *
3660
+ * 如果 BS 卸载了不可见的 Tab 组件,MindmapSlot 的 sidebar 模式 auto-open
3661
+ * 兜底调 openTab;Tab 重新挂载后 MindmapWorkspace 的 seen=null 首挂载语义
3662
+ * 恢复最近一次打开的脑图。
3663
+ */
3664
+ function MindmapSidebarTab(props) {
3665
+ const { ctx, scope, visible } = props;
3666
+ const sessionId = scope && scope.sessionId;
3667
+
3668
+ // 从 sessionStore 读头部槽位写入的会话数据(按 sessionId 隔离)。
3669
+ // useCallback 保证 subscribe/getSnapshot 仅在 sessionId 变化时重建,
3670
+ // 避免每帧重订阅。
3671
+ const subscribe = react.useCallback(
3672
+ (fn) => sessionStore.subscribe(sessionId, fn),
3673
+ [sessionId],
3674
+ );
3675
+ const getSnapshot = react.useCallback(
3676
+ () => sessionStore.get(sessionId),
3677
+ [sessionId],
3678
+ );
3679
+ const data = react.useSyncExternalStore(subscribe, getSnapshot);
3680
+
3681
+ // auto-open 回调:新的 mindmap_create/open 到达时聚焦本 Tab。
3682
+ const onAutoOpen = react.useCallback(() => {
3683
+ try { ctx && ctx.betterSidebar && ctx.betterSidebar.openTab({ type: "dsh-mindmap:mindmap" }, scope); } catch { /* BS 已卸载或方法缺失 */ }
3684
+ }, [ctx, scope]);
3685
+
3686
+ if (!data) {
3687
+ // MindmapSlot 尚未写入数据(Tab 先于会话激活打开)。
3688
+ return (0, react_jsx_runtime.jsx)("div", { style: S.loadingWrap, children:
3689
+ (0, react_jsx_runtime.jsx)("p", { style: SIDEBAR_STYLES.loadingText, children: "等待会话数据…" })
3690
+ });
3691
+ }
3692
+
3693
+ return (0, react_jsx_runtime.jsx)(MindmapWorkspace, {
3694
+ sessionId,
3695
+ nodes: data.nodes,
3696
+ nodesVersion: data.nodesVersion,
3697
+ inputActions: data.inputActions,
3698
+ mindmapFace: data.mindmapFace,
3699
+ visible,
3700
+ onAutoOpen,
3701
+ onClose: undefined,
3702
+ headerHeight: null,
3703
+ variant: "sidebar",
3704
+ });
3705
+ }
3706
+
3707
+ /**
3708
+ * 会话内容节点的双代快照选择(023):dsh ≤0.1.1 的 useSession 快照带
3709
+ * 平铺 nodes;0.1.2-rc.1 起 SessionSnapshot 拆成纯控制状态,会话内容
3710
+ * 迁入 useChat 的 ChatSnapshot.legacy.nodes(官方兼容面,ToolResultNode
3711
+ * 字段同名)。legacy 优先、旧 nodes 兜底,两代通吃。
3712
+ */
3713
+ function conversationNodesOf(s) {
3714
+ if (!s) return EMPTY_NODES;
3715
+ const legacy = s.legacy;
3716
+ if (legacy && Array.isArray(legacy.nodes)) return legacy.nodes;
3717
+ return Array.isArray(s.nodes) ? s.nodes : EMPTY_NODES;
3718
+ }
3719
+
3720
+ /**
3721
+ * 「思维脑图」槽位组件(014 + 026 双模式):同一槽位渲染 M 按钮。
3722
+ * 026 起,betterSidebar 服务可用时(sidebarBus 检测)走原生 Tab 模式——
3723
+ * M 按钮调 openTab 聚焦 Tab,不渲染独立 fixed 面板;会话数据写入
3724
+ * sessionStore 供 MindmapSidebarTab 读取。服务不可用时维持头部按钮 +
3725
+ * 独立悬浮面板(MindmapDetailsPanel)。
3726
+ * session scope 的 useSession/sessionId/inputActions 直给,经 props 传给
3727
+ * MindmapDetailsPanel(无桥、无 useSyncExternalStore——shell.overlay 跨槽
3728
+ * 方案实测未渲染,弃用后顺手把桥也删了)。023:内容钩子改为
3729
+ * useChat(0.1.2-rc.1+)优先、useSession(≤0.1.1)兜底。
3730
+ */
3731
+ function MindmapSlot(props) {
3732
+ const { useSession, useChat, sessionId, inputActions, mindmapFace } = props;
3733
+ const nodesHook = useChat ?? useSession;
3734
+ const nodes = nodesHook ? nodesHook(conversationNodesOf) : EMPTY_NODES;
3735
+ // 016 可靠性加固:结构指纹作第二 selector。store 原地改数组(引用
3736
+ // 不变)时,nodes prop 不换、memo 命中缓存、auto-open effect 永不
3737
+ // 重跑——「AI 打开了脑图但面板不展开」的根因。指纹是原始值字符串,
3738
+ // 值比较天然绕过引用相等短路;内容钩子不可用时回退空串。
3739
+ const nodesVersion = nodesHook ? nodesHook((s) => nodesFingerprint(conversationNodesOf(s))) : "";
3740
+
3741
+ // 026 sidebar 模式检测:betterSidebar 服务可用时走原生 Tab,否则走独立面板。
3742
+ const sidebar = react.useSyncExternalStore(sidebarBus.subscribe, sidebarBus.get);
3743
+ const sidebarMode = sidebar !== null;
3744
+
3745
+ // 026 会话数据桥:sidebar 模式下把头部槽位捕获的数据写入 sessionStore,
3746
+ // 供 MindmapSidebarTab 组件读取(Tab 组件不接收 header 槽位 props)。
3747
+ // standalone 模式不写——数据直接经 props 传给 MindmapDetailsPanel。
3748
+ // 028 生命周期清理:会话切换时删旧 sessionId 的快照,组件卸载时删
3749
+ // 当前 sessionId 的快照——模块级 Map 不残留旧会话的 nodes/inputActions。
3750
+ react.useEffect(() => {
3751
+ if (!sidebarMode || !sessionId) return;
3752
+ sessionStore.set(sessionId, { nodes, nodesVersion, inputActions, mindmapFace });
3753
+ }, [sidebarMode, sessionId, nodes, nodesVersion, inputActions, mindmapFace]);
3754
+ // 028 会话切换 / 退出 sidebar 模式时清理旧快照。
3755
+ const lastSessionRef = react.useRef(null);
3756
+ react.useEffect(() => {
3757
+ if (!sidebarMode) {
3758
+ // 退出 sidebar 模式:清理上次的快照。
3759
+ if (lastSessionRef.current) {
3760
+ sessionStore.delete(lastSessionRef.current);
3761
+ lastSessionRef.current = null;
3762
+ }
3763
+ return;
3764
+ }
3765
+ // 会话切换:清理旧 sessionId 的快照。
3766
+ if (lastSessionRef.current && lastSessionRef.current !== sessionId) {
3767
+ sessionStore.delete(lastSessionRef.current);
3768
+ }
3769
+ lastSessionRef.current = sessionId;
3770
+ }, [sidebarMode, sessionId]);
3771
+ // 028 组件卸载时清理当前 sessionId 的快照。
3772
+ react.useEffect(() => {
3773
+ return () => {
3774
+ if (lastSessionRef.current) {
3775
+ sessionStore.delete(lastSessionRef.current);
3776
+ lastSessionRef.current = null;
3777
+ }
3778
+ };
3779
+ }, []);
3780
+
3781
+ // 026 sidebar auto-open 兜底:BS 可能卸载不可见的 Tab 组件,此时
3782
+ // MindmapWorkspace 的 auto-open effect 不跑。MindmapSlot 始终在头部
3783
+ // 挂载,在这里检测新的 create/open 结果并调 openTab 把 Tab 拉起。
3784
+ // 首次进入 sidebar 模式时只记基线(不弹历史文档),之后只响应新事件。
3785
+ const sidebarDocs = react.useMemo(() => reduceDocuments(nodes), [nodes, nodesVersion]);
3786
+ const sidebarSeen = react.useRef(null);
3787
+ const sidebarInitedRef = react.useRef(false);
3788
+ react.useEffect(() => {
3789
+ if (!sidebarMode || !sessionId) {
3790
+ sidebarInitedRef.current = false;
3791
+ return;
3792
+ }
3793
+ if (!sidebarInitedRef.current) {
3794
+ sidebarInitedRef.current = true;
3795
+ sidebarSeen.current = openingEventKeys(sidebarDocs);
3796
+ return;
3797
+ }
3798
+ const target = autoOpenTarget(sidebarDocs, sidebarSeen.current);
3799
+ sidebarSeen.current = openingEventKeys(sidebarDocs);
3800
+ if (target) {
3801
+ try { sidebar.openTab({ type: "dsh-mindmap:mindmap" }, { sessionId }); } catch { /* BS 已卸载或方法缺失 */ }
3802
+ }
3803
+ }, [sidebarDocs, sidebarMode, sessionId, sidebar]);
3804
+
3805
+ const [open, setOpen] = react.useState(false);
3806
+
3807
+ // M 按钮的 SVG 图标(两种模式共用)。
3808
+ const mButtonIcon = (0, react_jsx_runtime.jsx)("svg", {
3809
+ width: 14,
3810
+ height: 14,
3811
+ viewBox: "0 0 14 14",
3812
+ fill: "none",
3813
+ stroke: "currentColor",
3814
+ strokeWidth: 1.4,
3815
+ strokeLinecap: "round",
3816
+ strokeLinejoin: "round",
3817
+ "aria-hidden": "true",
3818
+ style: { opacity: 0.7, flex: "none" },
3819
+ children: [
3820
+ (0, react_jsx_runtime.jsx)("circle", { cx: 2.5, cy: 7, r: 1.7 }),
3821
+ (0, react_jsx_runtime.jsx)("circle", { cx: 11.5, cy: 3.5, r: 1.7 }),
3822
+ (0, react_jsx_runtime.jsx)("circle", { cx: 11.5, cy: 10.5, r: 1.7 }),
3823
+ (0, react_jsx_runtime.jsx)("path", { d: "M4.1 6.2 L9.9 4.2" }),
3824
+ (0, react_jsx_runtime.jsx)("path", { d: "M4.1 7.8 L9.9 9.8" }),
3825
+ ],
3826
+ });
3827
+
3828
+ if (sidebarMode) {
3829
+ // sidebar 模式:M 按钮调 openTab 聚焦 Better Sidebar Tab,不渲染独立面板。
3830
+ return (0, react_jsx_runtime.jsx)(react.Fragment, { children: (0, react_jsx_runtime.jsxs)("button", {
3831
+ type: "button",
3832
+ title: "脑图面板:展开 / 收起",
3833
+ style: S.mButton,
3834
+ onClick: () => {
3835
+ try { sidebar.openTab({ type: "dsh-mindmap:mindmap" }, { sessionId }); } catch { /* BS 已卸载或方法缺失 */ }
3836
+ },
3837
+ children: [mButtonIcon, "思维脑图"],
3838
+ }) });
3839
+ }
3840
+
3841
+ // standalone 模式:M 按钮 + 独立 fixed 面板。
3842
+ return (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
3843
+ (0, react_jsx_runtime.jsxs)("button", {
3844
+ type: "button",
3845
+ title: "脑图面板:展开 / 收起",
3846
+ style: S.mButton,
3847
+ onClick: () => setOpen((v) => !v),
3848
+ children: [mButtonIcon, "思维脑图"],
3849
+ }),
3850
+ (0, react_jsx_runtime.jsx)(MindmapDetailsPanel, {
3851
+ open,
3852
+ sessionId,
3853
+ inputActions,
3854
+ nodes,
3855
+ nodesVersion,
3856
+ mindmapFace,
3857
+ onOpen: () => setOpen(true),
3858
+ onClose: () => setOpen(false),
3859
+ }),
3860
+ ] });
3861
+ }
3862
+
3863
+ //#region better-sidebar 共存:服务总线 + 会话数据桥
3864
+ // sidebarBus:betterSidebar 服务引用的可观察容器。apply() 检测到服务时
3865
+ // set(svc),MindmapSlot 用 useSyncExternalStore 订阅,决定渲染独立面板
3866
+ // 还是只渲染 M 按钮(面板交给 Better Sidebar Tab)。服务不可用时 get()
3867
+ // 返回 null——独立面板照常工作,无需安装额外依赖。
3868
+ const sidebarBus = (() => {
3869
+ let service = null;
3870
+ const listeners = new Set();
3871
+ return {
3872
+ get: () => service,
3873
+ set(svc) {
3874
+ service = svc || null;
3875
+ for (const fn of listeners) fn();
3876
+ },
3877
+ subscribe(fn) {
3878
+ listeners.add(fn);
3879
+ return () => { listeners.delete(fn); };
3880
+ },
3881
+ };
3882
+ })();
3883
+
3884
+ // sessionStore:按 sessionId 隔离的数据桥。MindmapSlot 始终在头部槽位里
3885
+ // 调用 useChat/useSession 钩子获取 nodes/nodesVersion/inputActions,写入
3886
+ // 对应 sessionId 的快照;MindmapSidebarTab 组件用 useSyncExternalStore
3887
+ // 订阅自己 sessionId 的快照,拿到数据后渲染 MindmapWorkspace。
3888
+ // 028 生命周期清理:MindmapSlot 在会话切换(sessionId 变化)和组件卸载
3889
+ // 时删除对应 sessionId 的快照——模块级 Map 不残留旧会话的
3890
+ // nodes/inputActions。退出 sidebar 模式(Better Sidebar 卸载)时也清理。
3891
+ const sessionStore = (() => {
3892
+ const sessions = new Map();
3893
+ const listeners = new Map();
3894
+ function notify(sessionId) {
3895
+ const set = listeners.get(sessionId);
3896
+ if (set) for (const fn of set) fn();
3897
+ }
3898
+ return {
3899
+ get(sessionId) {
3900
+ return sessions.get(sessionId) || null;
3901
+ },
3902
+ set(sessionId, data) {
3903
+ sessions.set(sessionId, data);
3904
+ notify(sessionId);
3905
+ },
3906
+ delete(sessionId) {
3907
+ sessions.delete(sessionId);
3908
+ notify(sessionId);
3909
+ },
3910
+ subscribe(sessionId, fn) {
3911
+ let set = listeners.get(sessionId);
3912
+ if (!set) { set = new Set(); listeners.set(sessionId, set); }
3913
+ set.add(fn);
3914
+ return () => {
3915
+ set.delete(fn);
3916
+ if (set.size === 0) listeners.delete(sessionId);
3917
+ };
3918
+ },
3919
+ };
3920
+ })();
3375
3921
  //#endregion
3376
3922
 
3377
3923
  /**
@@ -3395,29 +3941,25 @@ window.__ModuleLoader__.load({
3395
3941
  function apply(ctx) {
3396
3942
  const face = {};
3397
3943
 
3398
- // 014「布局让位」CSS(better-sidebar 同款机制):面板打开时给 #root
3399
- // margin-right + 宽度挤压,把聊天区推到左边、面板占右侧腾出的空间,
3400
- // 互不遮挡。015 修复级联冲突:它家(dsh-better-sidebar)同样注入
3401
- // #root 规则,后注入者胜导致我们的推挤被压掉——我们的规则加
3402
- // !important 且把双方变量相加(它开面板时聊天同样让位),无论注入
3403
- // 顺序如何都稳定生效。若它家未来也用 !important,需再评估(见 docs/014)。
3404
- if (typeof document !== "undefined") {
3405
- const style = document.createElement("style");
3406
- style.setAttribute("data-dsh-mindmap", "layout-push");
3407
- style.textContent = [
3408
- "#root{",
3409
- "margin-right:calc(var(--dsh-mindmap-width,0px) + var(--dsh-sidebar-width,0px))!important;",
3410
- "width:calc(100% - var(--dsh-mindmap-width,0px) - var(--dsh-sidebar-width,0px))!important;",
3411
- "transition:margin-right var(--ds-transition-duration-slow) var(--ds-ease-in-out),width var(--ds-transition-duration-slow) var(--ds-ease-in-out);",
3412
- "}",
3413
- ].join("");
3414
- document.head.appendChild(style);
3415
- }
3416
-
3417
- // 018 生长动画:新增/变化节点错峰渐显(节点盒 = 淡入 + 左移浮现,
3418
- // 连线 = 淡入;延迟由内联 animationDelay 提供)。fill mode both 保证
3419
- // 延迟期间保持隐藏;动画只挂新节点,旧节点不受影响。尊重系统减弱动效。
3420
- if (typeof document !== "undefined") {
3944
+ // 026 生长动画 CSS:无论 standalone 还是 sidebar 模式都需要(节点渐显
3945
+ // 与布局无关)。纳入 ctx.effect 清理——HMR / 插件禁用后 <head> 不残留。
3946
+ if (typeof ctx.effect === "function") {
3947
+ ctx.effect(() => {
3948
+ if (typeof document === "undefined") return;
3949
+ const animStyle = document.createElement("style");
3950
+ animStyle.setAttribute("data-dsh-mindmap", "growth-anim");
3951
+ animStyle.textContent = [
3952
+ "@keyframes dsh-mm-node-in{from{opacity:0;transform:translateX(-10px)}to{opacity:1;transform:none}}",
3953
+ ".dsh-mm-reveal{opacity:0;animation:dsh-mm-node-in 320ms ease-out both}",
3954
+ "@keyframes dsh-mm-fade-in{from{opacity:0}to{opacity:1}}",
3955
+ ".dsh-mm-edge-reveal{opacity:0;animation:dsh-mm-fade-in 320ms ease-out both}",
3956
+ "@media (prefers-reduced-motion: reduce){.dsh-mm-reveal,.dsh-mm-edge-reveal{animation:none;opacity:1}}",
3957
+ ].join("");
3958
+ document.head.appendChild(animStyle);
3959
+ return () => { animStyle.remove(); };
3960
+ });
3961
+ } else if (typeof document !== "undefined") {
3962
+ // ctx.effect 不可用(旧运行时 / 测试桩):直接注入,无清理。
3421
3963
  const animStyle = document.createElement("style");
3422
3964
  animStyle.setAttribute("data-dsh-mindmap", "growth-anim");
3423
3965
  animStyle.textContent = [
@@ -3430,6 +3972,101 @@ window.__ModuleLoader__.load({
3430
3972
  document.head.appendChild(animStyle);
3431
3973
  }
3432
3974
 
3975
+ // 026+028+029 betterSidebar 生命周期统一收口:三种状态(启动时已存在、
3976
+ // 运行中后到达、不存在/已卸载)都走同一条 ctx.inject 路径。ctx.inject 的
3977
+ // 语义:服务已存在时回调立即执行;后到达时等到达后执行;服务消失时
3978
+ // inject 返回的 disposer 自动执行。把 registerTab + sidebarBus.set 放在
3979
+ // inject 回调里,disposer 绑定到 Better Sidebar 依赖 fiber——只卸载
3980
+ // Better Sidebar、不卸载 dsh-mindmap 时,disposer 照常执行,sidebarBus
3981
+ // 归零,layout-push effect 自动恢复 standalone 布局。
3982
+ if (typeof ctx.inject === "function") {
3983
+ try {
3984
+ ctx.inject(["betterSidebar"], (ctx2) => {
3985
+ const svc = ctx2 && ctx2.betterSidebar;
3986
+ if (!svc || typeof svc.registerTab !== "function") return;
3987
+ // 029 注册 Tab 并设 sidebarBus。disposer 清 bus + 注销 Tab——
3988
+ // 不用 ctx.effect 包裹,disposer 直接由 ctx.inject 的依赖
3989
+ // 生命周期管理(Better Sidebar fiber 卸载时触发)。
3990
+ const dispose = svc.registerTab({
3991
+ id: "dsh-mindmap:mindmap",
3992
+ title: () => "思维脑图",
3993
+ icon: (size) => (0, react_jsx_runtime.jsx)("svg", {
3994
+ width: size, height: size, viewBox: "0 0 14 14",
3995
+ fill: "none", stroke: "currentColor", strokeWidth: 1.4,
3996
+ strokeLinecap: "round", strokeLinejoin: "round",
3997
+ children: [
3998
+ (0, react_jsx_runtime.jsx)("circle", { cx: 2.5, cy: 7, r: 1.7 }),
3999
+ (0, react_jsx_runtime.jsx)("circle", { cx: 11.5, cy: 3.5, r: 1.7 }),
4000
+ (0, react_jsx_runtime.jsx)("circle", { cx: 11.5, cy: 10.5, r: 1.7 }),
4001
+ (0, react_jsx_runtime.jsx)("path", { d: "M4.1 6.2 L9.9 4.2" }),
4002
+ (0, react_jsx_runtime.jsx)("path", { d: "M4.1 7.8 L9.9 9.8" }),
4003
+ ],
4004
+ }),
4005
+ order: 100,
4006
+ single: true,
4007
+ component: MindmapSidebarTab,
4008
+ });
4009
+ sidebarBus.set(svc);
4010
+ // 返回 disposer:Better Sidebar 依赖消失时执行。
4011
+ return () => {
4012
+ sidebarBus.set(null);
4013
+ dispose();
4014
+ };
4015
+ });
4016
+ } catch {
4017
+ // ctx.inject 不支持或服务名未注册:standalone 模式。
4018
+ }
4019
+ }
4020
+
4021
+ // 026+028 layout-push CSS 可逆 effect:持续监听 sidebarBus——standalone
4022
+ // 模式(bus===null)时注入 #root 推挤规则,sidebar 模式(bus!==null)
4023
+ // 时移除。模式切换时自动翻转,不需要一次性删除。纳入 ctx.effect 清理。
4024
+ if (typeof ctx.effect === "function") {
4025
+ ctx.effect(() => {
4026
+ if (typeof document === "undefined") return;
4027
+ let layoutStyle = null;
4028
+ function ensureLayoutPush() {
4029
+ if (typeof document === "undefined") return;
4030
+ if (sidebarBus.get()) {
4031
+ // sidebar 模式:不推 #root(Better Sidebar 管自己的布局)。
4032
+ if (layoutStyle) { layoutStyle.remove(); layoutStyle = null; }
4033
+ } else {
4034
+ // standalone 模式:注入推挤规则(仅一份)。
4035
+ if (!layoutStyle) {
4036
+ layoutStyle = document.createElement("style");
4037
+ layoutStyle.setAttribute("data-dsh-mindmap", "layout-push");
4038
+ layoutStyle.textContent = [
4039
+ "#root{",
4040
+ "margin-right:calc(var(--dsh-mindmap-width,0px) + var(--dsh-sidebar-width,0px))!important;",
4041
+ "width:calc(100% - var(--dsh-mindmap-width,0px) - var(--dsh-sidebar-width,0px))!important;",
4042
+ "transition:margin-right var(--ds-transition-duration-slow) var(--ds-ease-in-out),width var(--ds-transition-duration-slow) var(--ds-ease-in-out);",
4043
+ "}",
4044
+ ].join("");
4045
+ document.head.appendChild(layoutStyle);
4046
+ }
4047
+ }
4048
+ }
4049
+ ensureLayoutPush();
4050
+ const unsub = sidebarBus.subscribe(ensureLayoutPush);
4051
+ return () => {
4052
+ unsub();
4053
+ if (layoutStyle) { layoutStyle.remove(); layoutStyle = null; }
4054
+ };
4055
+ });
4056
+ } else if (typeof document !== "undefined" && !sidebarBus.get()) {
4057
+ // ctx.effect 不可用:直接注入(015 原始行为),standalone 模式。
4058
+ const style = document.createElement("style");
4059
+ style.setAttribute("data-dsh-mindmap", "layout-push");
4060
+ style.textContent = [
4061
+ "#root{",
4062
+ "margin-right:calc(var(--dsh-mindmap-width,0px) + var(--dsh-sidebar-width,0px))!important;",
4063
+ "width:calc(100% - var(--dsh-mindmap-width,0px) - var(--dsh-sidebar-width,0px))!important;",
4064
+ "transition:margin-right var(--ds-transition-duration-slow) var(--ds-ease-in-out),width var(--ds-transition-duration-slow) var(--ds-ease-in-out);",
4065
+ "}",
4066
+ ].join("");
4067
+ document.head.appendChild(style);
4068
+ }
4069
+
3433
4070
  // 013 目录树 tab:host 自建只读路由 /mindmap/api/tree(dsh-better-sidebar
3434
4071
  // 同款机制——官方 host.listDirectory 在 native picker 环境必挂,见 013)。
3435
4072
  // 客户端只读目录,仍无任何写文件通道。
@@ -3514,7 +4151,8 @@ window.__ModuleLoader__.load({
3514
4151
  // better-sidebar 同款「fixed 宿主层自举」思路(它的宿主层挂在
3515
4152
  // conversation.chat.turnTail);session scope 全套 props 直给,无需跨槽。
3516
4153
  // details 槽已归还官方(原生「工具详情」栏恢复);shell.overlay 方案
3517
- // 实测未渲染,已弃用(见 docs/014 排障)。
4154
+ // 实测未渲染,已弃用(见 docs/014 排障)。026:MindmapSlot 内部按
4155
+ // sidebarBus 自动切换 sidebar Tab 模式 / standalone 面板模式。
3518
4156
  ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
3519
4157
  name: "conversation.session.header.actions",
3520
4158
  id: "dsh-mindmap",
@@ -3544,6 +4182,14 @@ window.__ModuleLoader__.load({
3544
4182
  planGrowthReveal,
3545
4183
  relPathWithin,
3546
4184
  visibleTreeRows,
4185
+ // 025 草稿保护:宿主草稿读取面的能力探测(供测试)。
4186
+ readDraftText,
4187
+ draftBlocksAutoSend,
4188
+ // 025 子树折叠:视图态纯函数 + 节点行组件(供测试)。
4189
+ toggleCollapsed,
4190
+ countDescendants,
4191
+ pruneCollapsed,
4192
+ TreeRow,
3547
4193
  // 019 皮肤层与血肉层纯函数(供测试)。
3548
4194
  resolveToken,
3549
4195
  resolveNodeStyle,
@@ -3576,6 +4222,15 @@ window.__ModuleLoader__.load({
3576
4222
  settingsNamespacesOf,
3577
4223
  // 021 画布组件:仅供测试驱动平移手势(不参与运行时契约)。
3578
4224
  MindmapCanvas,
4225
+ // 026 better-sidebar 共存:服务总线 + 会话数据桥 + Tab 壳(供测试)。
4226
+ sidebarBus,
4227
+ sessionStore,
4228
+ MindmapSidebarTab,
4229
+ // 027 内嵌头部视觉对齐:壳无关工作区组件 + 样式表(供测试验证 variant 分支)。
4230
+ MindmapWorkspace,
4231
+ S,
4232
+ // 029 会话清理组件测试:MindmapSlot 直接调用(供测试验证 store 清理)。
4233
+ MindmapSlot,
3579
4234
  });
3580
4235
  return module.exports;
3581
4236
  }