dsh-mindmap 0.10.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
@@ -1338,8 +1338,7 @@ window.__ModuleLoader__.load({
1338
1338
  loadingErrorText: { color: "var(--dsw-alias-label-error)", fontSize: "13px", lineHeight: 1.6, margin: "0", textAlign: "center", wordBreak: "break-word", maxWidth: "90%" },
1339
1339
  loadingFailMark: { flex: "none", fontSize: "22px", lineHeight: 1, color: "var(--dsw-alias-label-error)" },
1340
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" },
1341
- // 013 目录树 tab:树容器/行样式。视觉自成一套:emoji 图标 + M 徽标 +
1342
- // 悬停高亮 + 激活指示条,不做 VSCode 式 chevron/线框。
1341
+ // 013 独立目录树:emoji 图标 + M 徽标;内嵌模式在下方单独覆盖。
1343
1342
  // 树容器 -2px 负边距抵消 body 16px 内距:树左缘 = 头部「目录」tab 左缘(14px)。
1344
1343
  treeWrap: { display: "flex", flexDirection: "column", gap: "12px", height: "100%", minHeight: 0, marginLeft: "-2px", marginRight: "-2px" },
1345
1344
  treeList: { flex: "1 1 auto", overflowY: "auto", display: "flex", flexDirection: "column", gap: "2px", minHeight: 0 },
@@ -1440,8 +1439,53 @@ window.__ModuleLoader__.load({
1440
1439
  // 百分比标签:tabular-nums 防数字抖动。
1441
1440
  zoomLabel: { flex: "none", minWidth: "38px", textAlign: "center", fontSize: "11px", lineHeight: "20px", color: "var(--dsw-alias-label-secondary)", fontVariantNumeric: "tabular-nums", userSelect: "none" },
1442
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" },
1443
1453
  };
1444
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" },
1484
+ };
1485
+ function workspaceStyles(variant) {
1486
+ return variant === "sidebar" ? SIDEBAR_STYLES : S;
1487
+ }
1488
+
1445
1489
  /** 015 分段选择控件(线型/卡片风格)。 */
1446
1490
  function Segmented(props) {
1447
1491
  const { options, value, onChange, disabled } = props;
@@ -1605,78 +1649,6 @@ window.__ModuleLoader__.load({
1605
1649
  ] });
1606
1650
  }
1607
1651
 
1608
- /**
1609
- * 会话内容节点的双代快照选择(023):dsh ≤0.1.1 的 useSession 快照带
1610
- * 平铺 nodes;0.1.2-rc.1 起 SessionSnapshot 拆成纯控制状态,会话内容
1611
- * 迁入 useChat 的 ChatSnapshot.legacy.nodes(官方兼容面,ToolResultNode
1612
- * 字段同名)。legacy 优先、旧 nodes 兜底,两代通吃。
1613
- */
1614
- function conversationNodesOf(s) {
1615
- if (!s) return EMPTY_NODES;
1616
- const legacy = s.legacy;
1617
- if (legacy && Array.isArray(legacy.nodes)) return legacy.nodes;
1618
- return Array.isArray(s.nodes) ? s.nodes : EMPTY_NODES;
1619
- }
1620
-
1621
- /**
1622
- * 「思维脑图」槽位组件(014):同一槽位渲染 M 按钮 + 悬浮面板宿主层。
1623
- * session scope 的 useSession/sessionId/inputActions 直给,经 props 传给
1624
- * MindmapDetailsPanel(无桥、无 useSyncExternalStore——shell.overlay 跨槽
1625
- * 方案实测未渲染,弃用后顺手把桥也删了)。023:内容钩子改为
1626
- * useChat(0.1.2-rc.1+)优先、useSession(≤0.1.1)兜底。
1627
- */
1628
- function MindmapSlot(props) {
1629
- const { useSession, useChat, sessionId, inputActions, mindmapFace } = props;
1630
- const nodesHook = useChat ?? useSession;
1631
- const nodes = nodesHook ? nodesHook(conversationNodesOf) : EMPTY_NODES;
1632
- // 016 可靠性加固:结构指纹作第二 selector。store 原地改数组(引用
1633
- // 不变)时,nodes prop 不换、memo 命中缓存、auto-open effect 永不
1634
- // 重跑——「AI 打开了脑图但面板不展开」的根因。指纹是原始值字符串,
1635
- // 值比较天然绕过引用相等短路;内容钩子不可用时回退空串。
1636
- const nodesVersion = nodesHook ? nodesHook((s) => nodesFingerprint(conversationNodesOf(s))) : "";
1637
- const [open, setOpen] = react.useState(false);
1638
- return (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
1639
- (0, react_jsx_runtime.jsxs)("button", {
1640
- type: "button",
1641
- title: "脑图面板:展开 / 收起",
1642
- style: S.mButton,
1643
- onClick: () => setOpen((v) => !v),
1644
- children: [
1645
- (0, react_jsx_runtime.jsx)("svg", {
1646
- width: 14,
1647
- height: 14,
1648
- viewBox: "0 0 14 14",
1649
- fill: "none",
1650
- stroke: "currentColor",
1651
- strokeWidth: 1.4,
1652
- strokeLinecap: "round",
1653
- strokeLinejoin: "round",
1654
- "aria-hidden": "true",
1655
- style: { opacity: 0.7, flex: "none" },
1656
- children: [
1657
- (0, react_jsx_runtime.jsx)("circle", { cx: 2.5, cy: 7, r: 1.7 }),
1658
- (0, react_jsx_runtime.jsx)("circle", { cx: 11.5, cy: 3.5, r: 1.7 }),
1659
- (0, react_jsx_runtime.jsx)("circle", { cx: 11.5, cy: 10.5, r: 1.7 }),
1660
- (0, react_jsx_runtime.jsx)("path", { d: "M4.1 6.2 L9.9 4.2" }),
1661
- (0, react_jsx_runtime.jsx)("path", { d: "M4.1 7.8 L9.9 9.8" }),
1662
- ],
1663
- }),
1664
- "思维脑图",
1665
- ],
1666
- }),
1667
- (0, react_jsx_runtime.jsx)(MindmapDetailsPanel, {
1668
- open,
1669
- sessionId,
1670
- inputActions,
1671
- nodes,
1672
- nodesVersion,
1673
- mindmapFace,
1674
- onOpen: () => setOpen(true),
1675
- onClose: () => setOpen(false),
1676
- }),
1677
- ] });
1678
- }
1679
-
1680
1652
  //#region 019 血肉渲染:行内格式 + 大一统链接 + 表格块(规范源:003)
1681
1653
  // 行内格式统一扫描序:图片/链接 → 行内代码 → 粗体 → 删除线 → 斜体 → 裸链接。
1682
1654
  // 先命中先生效,裸链接放最后,避免吞掉已被 [文字](url) 消费的 URL。
@@ -2682,11 +2654,23 @@ window.__ModuleLoader__.load({
2682
2654
  }
2683
2655
  //#endregion
2684
2656
 
2685
- function MindmapDetailsPanel(props) {
2686
- // 014:面板与 M 按钮同槽位(conversation.session.header.actions),
2687
- // 会话能力(sessionId/inputActions/nodes)与开合回调全部由 MindmapSlot
2688
- // props 直给(无桥、无 useSyncExternalStore)。
2689
- 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);
2690
2674
  // 016:nodesVersion(结构指纹)作副依赖——nodes 引用不变但内容已变
2691
2675
  // (新工具结果原地落地)时强制重算;docs 新引用带动 merged →
2692
2676
  // auto-open effect 重跑(对已消费事件幂等 no-op),面板必达展开。
@@ -2703,55 +2687,13 @@ window.__ModuleLoader__.load({
2703
2687
  // AI 没调工具(S3)或任何未知成因卡住时的兜底恢复路径。
2704
2688
  const OPEN_TIMEOUT_MS = 30000;
2705
2689
  const [openTimedOut, setOpenTimedOut] = react.useState(false);
2706
- // 014 overlay 宽度:localStorage 持久化,拖拽钳制 [280, 视口 80%]。
2707
- // 窗口尺寸变化时持续钳制——只在挂载时压一次的话,窗口先放大→拖宽
2708
- // 面板→再缩小会让面板保持旧像素宽,聊天区被挤没。
2709
- const WIDTH_KEY = "dsh-mindmap.overlay-width";
2710
- const [panelWidth, setPanelWidth] = react.useState(() => {
2711
- try {
2712
- const saved = Number(localStorage.getItem(WIDTH_KEY));
2713
- if (Number.isFinite(saved) && saved >= 280) return Math.min(saved, Math.round(window.innerWidth * 0.8));
2714
- } catch {
2715
- // localStorage 不可用:走默认
2716
- }
2717
- return Math.round(window.innerWidth * 0.42);
2718
- });
2719
- react.useEffect(() => {
2720
- const clamp = () => {
2721
- setPanelWidth((prev) => {
2722
- const max = Math.round(window.innerWidth * 0.8);
2723
- return prev > max ? max : prev;
2724
- });
2725
- };
2726
- clamp();
2727
- window.addEventListener("resize", clamp);
2728
- return () => window.removeEventListener("resize", clamp);
2729
- }, []);
2730
- // 015 设置面板:没有本地拖拽记忆时,用 settings 里的默认宽度。
2731
- react.useEffect(() => {
2732
- let hasLocal = false;
2733
- try {
2734
- hasLocal = localStorage.getItem(WIDTH_KEY) !== null;
2735
- } catch {
2736
- // 忽略
2737
- }
2738
- if (hasLocal) return;
2739
- if (!mindmapFace || typeof mindmapFace.readSettings !== "function") return;
2740
- mindmapFace.readSettings().then((v) => {
2741
- const pct = v && typeof v.defaultPanelWidth === "number" ? Math.min(80, Math.max(20, v.defaultPanelWidth)) : 42;
2742
- const px = Math.round(window.innerWidth * pct / 100);
2743
- setPanelWidth((prev) => (Math.abs(prev - px) < 2 ? prev : px));
2744
- }).catch(() => {
2745
- // 读设置失败:保持 42% 默认
2746
- });
2747
- }, [mindmapFace]);
2748
2690
 
2749
- // 015 节点主题:面板每次打开、或设置总线 bump(设置页保存)时重读
2750
- // settings——面板常驻不卸载,光靠 open 变化会漏掉「开着面板改设置」。
2691
+ // 015 节点主题:面板每次可见、或设置总线 bump(设置页保存)时重读
2692
+ // settings——面板常驻不卸载,光靠 visible 变化会漏掉「开着面板改设置」。
2751
2693
  const settingsStamp = react.useSyncExternalStore(settingsBus.subscribe, settingsBus.get);
2752
2694
  const [theme, setTheme] = react.useState({ lineStyle: "elbow", cardStyle: "rounded", colorTheme: "ocean", growthAnimation: true });
2753
2695
  react.useEffect(() => {
2754
- if (!open) return;
2696
+ if (!visible) return;
2755
2697
  if (!mindmapFace || typeof mindmapFace.readSettings !== "function") return;
2756
2698
  mindmapFace.readSettings().then((v) => {
2757
2699
  if (!v) return;
@@ -2765,31 +2707,8 @@ window.__ModuleLoader__.load({
2765
2707
  }).catch(() => {
2766
2708
  // 读设置失败:保持当前主题
2767
2709
  });
2768
- }, [open, settingsStamp, mindmapFace]);
2769
- const dragStateRef = react.useRef(null);
2770
- function startResize(e) {
2771
- e.preventDefault();
2772
- dragStateRef.current = { startX: e.clientX, startWidth: panelWidth, latestWidth: panelWidth };
2773
- const onMove = (ev) => {
2774
- if (!dragStateRef.current) return;
2775
- const max = Math.round(window.innerWidth * 0.8);
2776
- const next = Math.min(max, Math.max(280, dragStateRef.current.startWidth + (dragStateRef.current.startX - ev.clientX)));
2777
- dragStateRef.current.latestWidth = next;
2778
- setPanelWidth(next);
2779
- };
2780
- const onUp = () => {
2781
- try {
2782
- localStorage.setItem(WIDTH_KEY, String(dragStateRef.current ? dragStateRef.current.latestWidth : panelWidth));
2783
- } catch {
2784
- // localStorage 不可用:忽略
2785
- }
2786
- dragStateRef.current = null;
2787
- window.removeEventListener("mousemove", onMove);
2788
- window.removeEventListener("mouseup", onUp);
2789
- };
2790
- window.addEventListener("mousemove", onMove);
2791
- window.addEventListener("mouseup", onUp);
2792
- }
2710
+ }, [visible, settingsStamp, mindmapFace]);
2711
+
2793
2712
  // 013 目录树 tab:常驻第一个 tab(TREE_TAB 哨兵,永不与绝对路径撞名)。
2794
2713
  const TREE_TAB = "__tree__";
2795
2714
  // 013 作者拍板「单脑图模式」:面板只有「目录」与「脑图」两个 tab,
@@ -2811,55 +2730,6 @@ window.__ModuleLoader__.load({
2811
2730
  // 悬停高亮键:树行用 entry.path / node.path,tab 用 TREE_TAB / 文档路径。
2812
2731
  const [hoverKey, setHoverKey] = react.useState(null);
2813
2732
 
2814
- // 007~010 头线对齐(overlay 版回归):面板头部高度动态跟随聊天区头部,
2815
- // 让两者的底部分隔线像素对齐。面板贴视口顶(fixed 宿主层),故
2816
- // 头部高度 = 聊天头部 rect.bottom - 1 - 面板顶(面板顶 ≈ 视口顶)。
2817
- // 主选 wSkVaW_header;结构链回退;合法性钳制 [40,200];失败回退 74(75-1)。
2818
- const panelRootRef = react.useRef(null);
2819
- const FALLBACK_HEADER_HEIGHT = 74;
2820
- const [headerHeight, setHeaderHeight] = react.useState(FALLBACK_HEADER_HEIGHT);
2821
- react.useLayoutEffect(() => {
2822
- const HEADER_MIN = 40;
2823
- const HEADER_MAX = 200;
2824
- const tryPaths = [
2825
- () => document.querySelector('[class*="wSkVaW_header"]'),
2826
- () => {
2827
- const frame = document.querySelector("[data-dsh-frame]");
2828
- if (!frame) return null;
2829
- const center = frame.querySelector('[data-pane="conversation"]');
2830
- return center ? center.firstElementChild : null;
2831
- },
2832
- ];
2833
- const measure = () => {
2834
- for (const path of tryPaths) {
2835
- const el = path();
2836
- if (!el) continue;
2837
- const rect = el.getBoundingClientRect();
2838
- const panelTop = panelRootRef.current
2839
- ? panelRootRef.current.getBoundingClientRect().top
2840
- : rect.top;
2841
- const h = rect.bottom - 1 - panelTop;
2842
- if (h >= HEADER_MIN && h <= HEADER_MAX) {
2843
- setHeaderHeight(Math.round(h * 10) / 10);
2844
- return;
2845
- }
2846
- }
2847
- setHeaderHeight(FALLBACK_HEADER_HEIGHT);
2848
- };
2849
- measure();
2850
- const target = tryPaths[0]() || tryPaths[1]();
2851
- let observer = null;
2852
- if (target && typeof ResizeObserver !== "undefined") {
2853
- observer = new ResizeObserver(measure);
2854
- observer.observe(target);
2855
- }
2856
- window.addEventListener("resize", measure);
2857
- return () => {
2858
- if (observer) observer.disconnect();
2859
- window.removeEventListener("resize", measure);
2860
- };
2861
- }, []);
2862
-
2863
2733
  // 单脑图模式:可见脑图 = 用户当前点选(且未被关闭)的快照/本地文档,
2864
2734
  // 否则跟随最新工具结果;隐藏过的路径不自动回弹(重新点树里文件才恢复)。
2865
2735
  const lastPath = merged.order.length > 0 ? merged.order[merged.order.length - 1] : null;
@@ -2915,14 +2785,14 @@ window.__ModuleLoader__.load({
2915
2785
  : null;
2916
2786
 
2917
2787
  // AI 自动打开:create/open 代表用户明确的「创建 / 打开 / 查看」意图。
2918
- // 无论面板当前是否收起,都展开并切到这次意图对应的文档;首次挂载的
2788
+ // 无论面板/Tab 当前是否可见,都拉起并切到这次意图对应的文档;首次挂载的
2919
2789
  // 历史快照也照常显示最近一次打开的脑图,避免出现「AI 说已打开但面板没了」。
2920
2790
  const seen = react.useRef(null);
2921
2791
  react.useEffect(() => {
2922
2792
  const targetPath = autoOpenTarget(merged, seen.current);
2923
2793
  seen.current = openingEventKeys(merged);
2924
2794
  if (targetPath) {
2925
- onOpen();
2795
+ onAutoOpen();
2926
2796
  setHiddenPath(null);
2927
2797
  setCurrentPath(targetPath);
2928
2798
  setView("mindmap");
@@ -2949,7 +2819,7 @@ window.__ModuleLoader__.load({
2949
2819
  prevIdsRef.current = { path: null, ids: null };
2950
2820
  }, [sessionId]);
2951
2821
  react.useEffect(() => {
2952
- if (!open) return; // 面板收起时不自动发消息(014 overlay 形态守卫)
2822
+ if (!visible) return; // 面板/Tab 不可见时不自动发消息
2953
2823
  if (!sessionId) return;
2954
2824
  if (!active || active === TREE_TAB) return;
2955
2825
  if (!docs.byPath[active]) return; // 本地占位:它的 open 请求已在途
@@ -2959,7 +2829,7 @@ window.__ModuleLoader__.load({
2959
2829
  if (submitChatCommand(`用 mindmap_open 打开 ${rel}`)) {
2960
2830
  focusSentRef.current = active;
2961
2831
  }
2962
- }, [active, focusPath, fsTree.cwd, docs, open]);
2832
+ }, [active, focusPath, fsTree.cwd, docs, visible]);
2963
2833
 
2964
2834
  async function onExport() {
2965
2835
  if (!tree || !doc || exporting) return;
@@ -2973,7 +2843,6 @@ window.__ModuleLoader__.load({
2973
2843
  setExporting(false);
2974
2844
  }
2975
2845
  }
2976
-
2977
2846
  //#region 013 目录树 tab:懒加载树 + 把指令填进聊天输入框
2978
2847
  // 草稿保护:确实读到非空草稿才让路(改走剪贴板),读不到就按既有
2979
2848
  // 行为直填直发——宿主不暴露草稿时不能把功能整个卡死。
@@ -3164,6 +3033,30 @@ window.__ModuleLoader__.load({
3164
3033
  [fsTree.nodes, fsTree.expanded],
3165
3034
  );
3166
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
+
3167
3060
  function renderTreeRow(row) {
3168
3061
  if (row.kind === "dir") {
3169
3062
  const node = row.node;
@@ -3191,10 +3084,8 @@ window.__ModuleLoader__.load({
3191
3084
  },
3192
3085
  children: [
3193
3086
  (0, react_jsx_runtime.jsx)("span", { style: S.treeCaret, children: expandedNow ? "▾" : "▸" }),
3194
- (0, react_jsx_runtime.jsx)("span", {
3195
- style: { flex: "0 1 auto", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 },
3196
- children: expandedNow ? `📂 ${node.name}` : `📁 ${node.name}`,
3197
- }),
3087
+ variant === "sidebar" ? sidebarTreeIcon(true, expandedNow) : null,
3088
+ directoryLabel(node.name, expandedNow),
3198
3089
  node.truncated ? (0, react_jsx_runtime.jsx)("span", { style: S.treeCaret, children: "…" }) : null,
3199
3090
  // 根行行内右侧的「刷新」(013:不占独立一行)。
3200
3091
  isRoot ? (0, react_jsx_runtime.jsx)("span", { style: S.spacer }) : null,
@@ -3217,8 +3108,9 @@ window.__ModuleLoader__.load({
3217
3108
  const entry = row.entry;
3218
3109
  const depthPad = row.depth * 16;
3219
3110
  const isMd = /\.md$/i.test(entry.name);
3111
+ const inactiveFile = variant === "sidebar" && !entry.isDir && !isMd;
3220
3112
  const expandedNow = entry.isDir && Boolean(fsTree.expanded[entry.path]);
3221
- const hovered = hoverKey === entry.path;
3113
+ const hovered = !inactiveFile && hoverKey === entry.path;
3222
3114
  const style = {
3223
3115
  ...S.treeRow,
3224
3116
  paddingLeft: depthPad,
@@ -3226,6 +3118,7 @@ window.__ModuleLoader__.load({
3226
3118
  ...(isMd ? S.treeRowMd : entry.isDir ? {} : S.treeRowOther),
3227
3119
  ...(entry.hidden ? { opacity: 0.6 } : {}),
3228
3120
  ...(hovered ? S.treeRowHover : {}),
3121
+ ...(inactiveFile ? { userSelect: "none" } : {}),
3229
3122
  };
3230
3123
  if (entry.isDir) {
3231
3124
  return (0, react_jsx_runtime.jsxs)("div", {
@@ -3247,25 +3140,34 @@ window.__ModuleLoader__.load({
3247
3140
  },
3248
3141
  children: [
3249
3142
  (0, react_jsx_runtime.jsx)("span", { style: S.treeCaret, children: expandedNow ? "▾" : "▸" }),
3250
- (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),
3251
3145
  ],
3252
3146
  });
3253
3147
  }
3254
3148
  return (0, react_jsx_runtime.jsxs)("div", {
3255
3149
  key: entry.path,
3256
3150
  style,
3257
- title: isMd ? `打开脑图:${entry.path}` : entry.path,
3258
- onClick: isMd ? () => openMindmap(entry) : undefined,
3259
- onMouseEnter: () => setHoverKey(entry.path),
3260
- 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)),
3261
3161
  // 右键:.md 不弹菜单(左键即打开);非 .md 只拦掉默认菜单。
3262
3162
  onContextMenu: (e) => {
3263
3163
  e.preventDefault();
3264
3164
  e.stopPropagation();
3265
3165
  },
3266
3166
  children: [
3167
+ variant === "sidebar" ? (0, react_jsx_runtime.jsx)("span", { style: S.treeCaret, "aria-hidden": true }) : null,
3267
3168
  isMd
3268
- ? (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)
3269
3171
  : (0, react_jsx_runtime.jsx)("span", { style: S.fileDot, children: (0, react_jsx_runtime.jsx)("span", { style: S.fileDotCore }) }),
3270
3172
  (0, react_jsx_runtime.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 }, children: entry.name }),
3271
3173
  ],
@@ -3386,9 +3288,331 @@ window.__ModuleLoader__.load({
3386
3288
  ] });
3387
3289
  }
3388
3290
  //#endregion
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: [
3348
+ (0, react_jsx_runtime.jsx)("button", {
3349
+ type: "button",
3350
+ style: S.sbTabTitle,
3351
+ title: shown,
3352
+ onClick: () => setView("mindmap"),
3353
+ children: merged.byPath[shown].rootTitle,
3354
+ }),
3355
+ (0, react_jsx_runtime.jsx)("button", {
3356
+ type: "button",
3357
+ style: S.sbTabClose,
3358
+ title: "关闭脑图",
3359
+ onClick: () => closeMindmap(shown),
3360
+ children: "✕",
3361
+ }),
3362
+ ],
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
+ }, []);
3389
3612
 
3390
3613
  // 014 布局让位:面板打开/拖宽时把宽度写进 CSS 变量,挤窄 #root 推走
3391
3614
  // 聊天区(better-sidebar 同款);关闭/卸载时移除变量恢复全宽。
3615
+ // 仅独立壳模式启用——BS Tab 模式不渲染本组件,不写此变量。
3392
3616
  react.useLayoutEffect(() => {
3393
3617
  if (typeof document === "undefined") return;
3394
3618
  if (open) {
@@ -3401,103 +3625,299 @@ window.__ModuleLoader__.load({
3401
3625
  };
3402
3626
  }, [open, panelWidth]);
3403
3627
 
3404
- if (!open) return null;
3405
- // 014 overlay 外壳:fixed 宿主层(点击穿透)套右缘贴边全高悬浮面板,
3406
- // 左缘拖拽调宽([280, 视口 80%],localStorage 持久化);遮盖聊天区是
3407
- // 该形态的已知代价(作者拍板,见 docs/014)。宿主层挂在 header 槽位里,
3408
- // better-sidebar 同款「fixed 自举」思路。
3409
- 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: [
3410
- (0, react_jsx_runtime.jsx)("div", { style: S.overlayHandle, onMouseDown: startResize }),
3411
- (0, react_jsx_runtime.jsxs)("div", { style: { ...S.header, height: `${headerHeight - 1}px` }, children: [
3412
- (0, react_jsx_runtime.jsxs)("div", { style: S.headerTop, children: [
3413
- (0, react_jsx_runtime.jsx)("span", { style: S.spacer }),
3414
- (0, react_jsx_runtime.jsx)("button", {
3415
- type: "button",
3416
- style: S.action,
3417
- disabled: !tree || exporting || (doc && doc.op === "local"),
3418
- onClick: onExport,
3419
- children: exporting ? "导出中…" : "导出图片",
3420
- }),
3421
- exportError ? (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-error)", fontSize: "12px" } , children: exportError }) : null,
3422
- (0, react_jsx_runtime.jsx)("button", {
3423
- type: "button",
3424
- style: S.action,
3425
- title: "收起脑图面板",
3426
- onClick: () => onClose(),
3427
- children: "✕",
3428
- }),
3429
- ] }),
3430
- (0, react_jsx_runtime.jsxs)("div", { style: S.tabRow, children: [
3431
- (0, react_jsx_runtime.jsx)("button", {
3432
- type: "button",
3433
- style: active === TREE_TAB ? { ...S.tab, ...S.tabActive } : (hoverKey === TREE_TAB ? { ...S.tab, ...S.tabHover } : S.tab),
3434
- title: fsTree.cwd ?? "工作目录",
3435
- onClick: () => setView("tree"),
3436
- onMouseEnter: () => setHoverKey(TREE_TAB),
3437
- onMouseLeave: () => setHoverKey((k) => (k === TREE_TAB ? null : k)),
3438
- onContextMenu: (e) => {
3439
- e.preventDefault();
3440
- e.stopPropagation();
3441
- setTabMenu({ x: e.clientX, y: e.clientY, path: TREE_TAB });
3442
- },
3443
- children: "目录",
3444
- }, TREE_TAB),
3445
- shown ? (0, react_jsx_runtime.jsxs)("span", {
3446
- key: shown,
3447
- style: { ...S.tabWrap, ...(active !== TREE_TAB ? S.tabActive : {}), ...(active === TREE_TAB && hoverKey === shown ? S.tabHover : {}) },
3448
- onMouseEnter: () => setHoverKey(shown),
3449
- onMouseLeave: () => setHoverKey((k) => (k === shown ? null : k)),
3450
- onContextMenu: (e) => {
3451
- e.preventDefault();
3452
- e.stopPropagation();
3453
- setTabMenu({ x: e.clientX, y: e.clientY, path: shown });
3454
- },
3455
- children: [
3456
- (0, react_jsx_runtime.jsx)("button", {
3457
- type: "button",
3458
- style: S.tabTitle,
3459
- title: shown,
3460
- onClick: () => setView("mindmap"),
3461
- children: merged.byPath[shown].rootTitle,
3462
- }),
3463
- (0, react_jsx_runtime.jsx)("button", {
3464
- type: "button",
3465
- style: S.tabClose,
3466
- title: "关闭脑图",
3467
- onClick: () => closeMindmap(shown),
3468
- children: "✕",
3469
- }),
3470
- ],
3471
- }, shown) : null,
3472
- ] }),
3473
- ] }),
3474
- // 016:脑图视图走 MindmapCanvas(自带滚动 + 居中 + 右上角缩放控制条),
3475
- // 不再套 S.body(避免嵌套滚动容器与双重 padding);目录/加载/空态保持原样。
3476
- active === TREE_TAB || (doc && doc.op === "local") || !tree
3477
- ? (0, react_jsx_runtime.jsx)("div", { style: S.body, children: active === TREE_TAB
3478
- ? renderTree()
3479
- : (doc && doc.op === "local")
3480
- ? renderLoading()
3481
- : renderTree() })
3482
- : (0, react_jsx_runtime.jsx)(MindmapCanvas, { node: tree, theme, fitKey: doc && doc.path, reveal }),
3483
- tabMenu ? (0, react_jsx_runtime.jsxs)("div", {
3484
- style: { ...S.treeMenu, left: tabMenu.x, top: tabMenu.y },
3485
- onContextMenu: (e) => e.preventDefault(),
3486
- children: [
3487
- (0, react_jsx_runtime.jsx)("button", {
3488
- type: "button",
3489
- style: S.treeMenuItem,
3490
- onClick: () => {
3491
- setTabMenu(null);
3492
- if (tabMenu.path === TREE_TAB) loadTree(undefined);
3493
- else closeMindmap(tabMenu.path);
3494
- },
3495
- children: tabMenu.path === TREE_TAB ? "刷新目录树" : "关闭脑图",
3496
- }),
3497
- ],
3498
- }) : null,
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,
3499
3647
  ] }) });
3500
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
+ })();
3501
3921
  //#endregion
3502
3922
 
3503
3923
  /**
@@ -3521,29 +3941,25 @@ window.__ModuleLoader__.load({
3521
3941
  function apply(ctx) {
3522
3942
  const face = {};
3523
3943
 
3524
- // 014「布局让位」CSS(better-sidebar 同款机制):面板打开时给 #root
3525
- // margin-right + 宽度挤压,把聊天区推到左边、面板占右侧腾出的空间,
3526
- // 互不遮挡。015 修复级联冲突:它家(dsh-better-sidebar)同样注入
3527
- // #root 规则,后注入者胜导致我们的推挤被压掉——我们的规则加
3528
- // !important 且把双方变量相加(它开面板时聊天同样让位),无论注入
3529
- // 顺序如何都稳定生效。若它家未来也用 !important,需再评估(见 docs/014)。
3530
- if (typeof document !== "undefined") {
3531
- const style = document.createElement("style");
3532
- style.setAttribute("data-dsh-mindmap", "layout-push");
3533
- style.textContent = [
3534
- "#root{",
3535
- "margin-right:calc(var(--dsh-mindmap-width,0px) + var(--dsh-sidebar-width,0px))!important;",
3536
- "width:calc(100% - var(--dsh-mindmap-width,0px) - var(--dsh-sidebar-width,0px))!important;",
3537
- "transition:margin-right var(--ds-transition-duration-slow) var(--ds-ease-in-out),width var(--ds-transition-duration-slow) var(--ds-ease-in-out);",
3538
- "}",
3539
- ].join("");
3540
- document.head.appendChild(style);
3541
- }
3542
-
3543
- // 018 生长动画:新增/变化节点错峰渐显(节点盒 = 淡入 + 左移浮现,
3544
- // 连线 = 淡入;延迟由内联 animationDelay 提供)。fill mode both 保证
3545
- // 延迟期间保持隐藏;动画只挂新节点,旧节点不受影响。尊重系统减弱动效。
3546
- 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 不可用(旧运行时 / 测试桩):直接注入,无清理。
3547
3963
  const animStyle = document.createElement("style");
3548
3964
  animStyle.setAttribute("data-dsh-mindmap", "growth-anim");
3549
3965
  animStyle.textContent = [
@@ -3556,6 +3972,101 @@ window.__ModuleLoader__.load({
3556
3972
  document.head.appendChild(animStyle);
3557
3973
  }
3558
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
+
3559
4070
  // 013 目录树 tab:host 自建只读路由 /mindmap/api/tree(dsh-better-sidebar
3560
4071
  // 同款机制——官方 host.listDirectory 在 native picker 环境必挂,见 013)。
3561
4072
  // 客户端只读目录,仍无任何写文件通道。
@@ -3640,7 +4151,8 @@ window.__ModuleLoader__.load({
3640
4151
  // better-sidebar 同款「fixed 宿主层自举」思路(它的宿主层挂在
3641
4152
  // conversation.chat.turnTail);session scope 全套 props 直给,无需跨槽。
3642
4153
  // details 槽已归还官方(原生「工具详情」栏恢复);shell.overlay 方案
3643
- // 实测未渲染,已弃用(见 docs/014 排障)。
4154
+ // 实测未渲染,已弃用(见 docs/014 排障)。026:MindmapSlot 内部按
4155
+ // sidebarBus 自动切换 sidebar Tab 模式 / standalone 面板模式。
3644
4156
  ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
3645
4157
  name: "conversation.session.header.actions",
3646
4158
  id: "dsh-mindmap",
@@ -3710,6 +4222,15 @@ window.__ModuleLoader__.load({
3710
4222
  settingsNamespacesOf,
3711
4223
  // 021 画布组件:仅供测试驱动平移手势(不参与运行时契约)。
3712
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,
3713
4234
  });
3714
4235
  return module.exports;
3715
4236
  }