dsh-mindmap 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/client.js +295 -19
- package/index.js +47 -21
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ All notable changes to this project are documented here. Release-specific notes
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Settings page section「思维脑图」in the left nav (`settings.section`), backed by a host settings namespace (`mindmap`): node theme (line style curve/elbow, card corners rounded/square, three color themes ocean/sunset/forest), default panel width (20-80%). The `requireApproval` switch stays functional (read at tool pre-execute time) but is hidden from the UI by design. Introduces a `@deepseek-ai/schemastery` dependency for the settings schema.
|
|
10
|
+
|
|
7
11
|
## [0.1.0] - 2026-08-23
|
|
8
12
|
|
|
9
13
|
First release of dsh-mindmap: a plain Markdown file in the session working directory becomes a live mindmap.
|
package/client.js
CHANGED
|
@@ -35,6 +35,39 @@ window.__ModuleLoader__.load({
|
|
|
35
35
|
|
|
36
36
|
const EMPTY_NODES = [];
|
|
37
37
|
|
|
38
|
+
// 015 节点颜色主题(三风格:海洋蓝 / 落日橙 / 森林绿)。
|
|
39
|
+
// 根盒用主题色淡底 + 半透明描边;标题节点文字用主题主色;背景永远跟随全局。
|
|
40
|
+
const COLOR_THEMES = {
|
|
41
|
+
ocean: { rootBg: "rgba(59,91,219,0.10)", rootBorder: "rgba(59,91,219,0.45)", heading: "#3b5bdb" },
|
|
42
|
+
sunset: { rootBg: "rgba(232,110,52,0.10)", rootBorder: "rgba(232,110,52,0.45)", heading: "#d96b2a" },
|
|
43
|
+
forest: { rootBg: "rgba(42,157,104,0.10)", rootBorder: "rgba(42,157,104,0.45)", heading: "#2a9d68" },
|
|
44
|
+
};
|
|
45
|
+
/** 颜色主题名 → 色值令牌(未知名回落海洋蓝)。 */
|
|
46
|
+
function colorThemeTokens(name) {
|
|
47
|
+
return COLOR_THEMES[name] || COLOR_THEMES.ocean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 015 设置变更总线:设置面板保存成功后 bump;脑图面板订阅 stamp 重读主题。
|
|
51
|
+
// 面板组件常驻不卸载,open 不变时不会自行重读——靠总线驱动
|
|
52
|
+
// (闭包实现,不依赖 this)。
|
|
53
|
+
const settingsBus = (() => {
|
|
54
|
+
let stamp = 0;
|
|
55
|
+
const listeners = new Set();
|
|
56
|
+
return {
|
|
57
|
+
get: () => stamp,
|
|
58
|
+
bump() {
|
|
59
|
+
stamp += 1;
|
|
60
|
+
for (const fn of listeners) fn(stamp);
|
|
61
|
+
},
|
|
62
|
+
subscribe(fn) {
|
|
63
|
+
listeners.add(fn);
|
|
64
|
+
return () => {
|
|
65
|
+
listeners.delete(fn);
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
})();
|
|
70
|
+
|
|
38
71
|
//#region markdown → 脑图树(零依赖手写解析)
|
|
39
72
|
/** 规范化节点内容用于稳定 ID:折叠空白、截断到 60 字符(MarkGrove 同款)。 */
|
|
40
73
|
function normalizeForId(text) {
|
|
@@ -496,6 +529,25 @@ window.__ModuleLoader__.load({
|
|
|
496
529
|
treeError: { color: "var(--dsw-alias-label-error)", fontSize: "12px", lineHeight: 1.6, margin: "0" },
|
|
497
530
|
treeMenu: { position: "fixed", zIndex: 60, minWidth: "210px", background: "var(--dsw-alias-bg-layer-3)", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "10px", padding: "6px", boxShadow: "var(--dsw-shadow-lv2)" },
|
|
498
531
|
treeMenuItem: { display: "block", width: "100%", boxSizing: "border-box", textAlign: "left", border: "none", background: "none", cursor: "pointer", padding: "7px 12px", borderRadius: "8px", font: "inherit", fontSize: "13px", color: "var(--dsw-alias-label-primary)" },
|
|
532
|
+
// 015 设置面板(settings.section 页面内容)。
|
|
533
|
+
settingsWrap: { display: "flex", flexDirection: "column", gap: "12px", padding: "16px", maxWidth: "480px" },
|
|
534
|
+
settingsGroup: { display: "flex", flexDirection: "column", gap: "14px", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "12px", padding: "14px", background: "var(--dsw-alias-bg-layer-3)" },
|
|
535
|
+
settingsGroupTitle: { fontSize: "13px", fontWeight: 600, color: "var(--dsw-alias-label-primary)", margin: "4px 0 0" },
|
|
536
|
+
settingsRow: { display: "flex", alignItems: "center", gap: "12px", fontSize: "13px", color: "var(--dsw-alias-label-primary)" },
|
|
537
|
+
settingsLabel: { flex: "1 1 auto", minWidth: 0, color: "var(--dsw-alias-label-secondary)" },
|
|
538
|
+
settingsInput: { width: "72px", padding: "4px 8px", borderRadius: "6px", border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-base)", color: "var(--dsw-alias-label-primary)", font: "inherit", fontSize: "13px" },
|
|
539
|
+
settingsHint: { color: "var(--dsw-alias-label-tertiary)", fontSize: "12px", lineHeight: 1.6, margin: "0" },
|
|
540
|
+
settingsNotice: { color: "var(--dsw-alias-state-success-primary, #1a7f37)", fontSize: "12px", margin: "0" },
|
|
541
|
+
settingsError: { color: "var(--dsw-alias-label-error)", fontSize: "12px", lineHeight: 1.6, margin: "0" },
|
|
542
|
+
// 分段选择控件(线型/卡片风格)
|
|
543
|
+
segmentRow: { display: "inline-flex", gap: "4px", padding: "3px", borderRadius: "8px", background: "var(--dsw-alias-bg-base)", border: "1px solid var(--dsw-alias-border-l2)" },
|
|
544
|
+
segmentBtn: { border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: "12px", padding: "3px 12px", borderRadius: "6px", color: "var(--dsw-alias-label-secondary)", lineHeight: "18px" },
|
|
545
|
+
segmentBtnActive: { background: "var(--dsw-alias-bg-layer-3)", color: "var(--dsw-alias-label-primary)", boxShadow: "0 1px 2px rgba(16,24,40,0.08)" },
|
|
546
|
+
// 颜色主题色板
|
|
547
|
+
swatchRow: { display: "flex", gap: "6px", flex: "1 1 auto", justifyContent: "flex-end" },
|
|
548
|
+
swatchBtn: { display: "inline-flex", alignItems: "center", gap: "6px", border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-base)", cursor: "pointer", font: "inherit", fontSize: "12px", padding: "3px 10px", borderRadius: "8px", color: "var(--dsw-alias-label-secondary)", lineHeight: "18px" },
|
|
549
|
+
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)" },
|
|
550
|
+
swatchDot: { width: "10px", height: "10px", borderRadius: "50%", flex: "none" },
|
|
499
551
|
row: { display: "flex", alignItems: "center", minWidth: 0 },
|
|
500
552
|
childrenColumn: { display: "flex", flexDirection: "column", gap: "8px", marginLeft: "40px", minWidth: 0 },
|
|
501
553
|
// 面板树连线层:正交折线(MarkGrove 的 orthogonalPath 风格),
|
|
@@ -508,6 +560,155 @@ window.__ModuleLoader__.load({
|
|
|
508
560
|
codeBox: { fontFamily: "Menlo, monospace", fontSize: "12px" },
|
|
509
561
|
};
|
|
510
562
|
|
|
563
|
+
/** 015 分段选择控件(线型/卡片风格)。 */
|
|
564
|
+
function Segmented(props) {
|
|
565
|
+
const { options, value, onChange, disabled } = props;
|
|
566
|
+
return (0, react_jsx_runtime.jsx)("div", { style: S.segmentRow, children: options.map((opt) => (0, react_jsx_runtime.jsx)("button", {
|
|
567
|
+
key: opt.value,
|
|
568
|
+
type: "button",
|
|
569
|
+
style: value === opt.value ? { ...S.segmentBtn, ...S.segmentBtnActive } : S.segmentBtn,
|
|
570
|
+
disabled,
|
|
571
|
+
onClick: () => onChange(opt.value),
|
|
572
|
+
children: opt.label,
|
|
573
|
+
}, opt.value)) });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* 015 设置面板(settings.section 页面,root scope):读写 host 的
|
|
578
|
+
* settings namespace "mindmap"。节点主题三件套(线/卡片/颜色)+ 面板宽度;
|
|
579
|
+
* requireApproval 按作者要求隐藏(功能保留,经 config/API 仍可设)。
|
|
580
|
+
*/
|
|
581
|
+
function SettingsPanel(props) {
|
|
582
|
+
const { mindmapFace } = props;
|
|
583
|
+
const [value, setValue] = react.useState(null);
|
|
584
|
+
const [saving, setSaving] = react.useState(false);
|
|
585
|
+
const [notice, setNotice] = react.useState("");
|
|
586
|
+
const [error, setError] = react.useState("");
|
|
587
|
+
|
|
588
|
+
react.useEffect(() => {
|
|
589
|
+
let alive = true;
|
|
590
|
+
(async () => {
|
|
591
|
+
try {
|
|
592
|
+
const v = mindmapFace && typeof mindmapFace.readSettings === "function" ? await mindmapFace.readSettings() : null;
|
|
593
|
+
if (!alive) return;
|
|
594
|
+
if (v === null) setError("设置服务不可用:settings namespace 未注册或 connection 缺失");
|
|
595
|
+
setValue({
|
|
596
|
+
lineStyle: v && v.lineStyle === "curve" ? "curve" : "elbow",
|
|
597
|
+
cardStyle: v && v.cardStyle === "square" ? "square" : "rounded",
|
|
598
|
+
colorTheme: v && COLOR_THEMES[v.colorTheme] ? v.colorTheme : "ocean",
|
|
599
|
+
defaultPanelWidth: v && typeof v.defaultPanelWidth === "number" ? v.defaultPanelWidth : 42,
|
|
600
|
+
});
|
|
601
|
+
} catch (err) {
|
|
602
|
+
if (alive) setError(String(err?.message ?? err));
|
|
603
|
+
}
|
|
604
|
+
})();
|
|
605
|
+
return () => {
|
|
606
|
+
alive = false;
|
|
607
|
+
};
|
|
608
|
+
}, [mindmapFace]);
|
|
609
|
+
|
|
610
|
+
async function save(patch) {
|
|
611
|
+
setSaving(true);
|
|
612
|
+
setError("");
|
|
613
|
+
setNotice("");
|
|
614
|
+
try {
|
|
615
|
+
if (!mindmapFace || typeof mindmapFace.updateSettings !== "function") throw new Error("settings service unavailable");
|
|
616
|
+
await mindmapFace.updateSettings(patch);
|
|
617
|
+
settingsBus.bump(); // 通知脑图面板重读主题(面板常驻,open 不变)
|
|
618
|
+
setNotice("已保存");
|
|
619
|
+
} catch (err) {
|
|
620
|
+
setError(String(err?.message ?? err));
|
|
621
|
+
} finally {
|
|
622
|
+
setSaving(false);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (value === null) {
|
|
627
|
+
return (0, react_jsx_runtime.jsx)("div", { style: S.settingsWrap, children: error
|
|
628
|
+
? (0, react_jsx_runtime.jsx)("p", { style: S.settingsError, children: error })
|
|
629
|
+
: (0, react_jsx_runtime.jsx)("p", { style: S.settingsHint, children: "正在读取设置…" }) });
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const setField = (patch) => {
|
|
633
|
+
setValue({ ...value, ...patch });
|
|
634
|
+
save(patch);
|
|
635
|
+
};
|
|
636
|
+
const changeWidth = (e) => {
|
|
637
|
+
const raw = Number(e.target.value);
|
|
638
|
+
const next = Number.isFinite(raw) ? Math.min(80, Math.max(20, Math.round(raw))) : value.defaultPanelWidth;
|
|
639
|
+
setValue({ ...value, defaultPanelWidth: next });
|
|
640
|
+
};
|
|
641
|
+
const commitWidth = () => {
|
|
642
|
+
save({ defaultPanelWidth: value.defaultPanelWidth });
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
return (0, react_jsx_runtime.jsxs)("div", { style: S.settingsWrap, children: [
|
|
646
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.settingsGroupTitle, children: "节点主题" }),
|
|
647
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsGroup, children: [
|
|
648
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsRow, children: [
|
|
649
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.settingsLabel, children: "线" }),
|
|
650
|
+
(0, react_jsx_runtime.jsx)(Segmented, {
|
|
651
|
+
options: [{ value: "elbow", label: "折线" }, { value: "curve", label: "曲线" }],
|
|
652
|
+
value: value.lineStyle,
|
|
653
|
+
disabled: saving,
|
|
654
|
+
onChange: (v) => setField({ lineStyle: v }),
|
|
655
|
+
}),
|
|
656
|
+
] }),
|
|
657
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsRow, children: [
|
|
658
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.settingsLabel, children: "卡片" }),
|
|
659
|
+
(0, react_jsx_runtime.jsx)(Segmented, {
|
|
660
|
+
options: [{ value: "rounded", label: "圆角" }, { value: "square", label: "直角" }],
|
|
661
|
+
value: value.cardStyle,
|
|
662
|
+
disabled: saving,
|
|
663
|
+
onChange: (v) => setField({ cardStyle: v }),
|
|
664
|
+
}),
|
|
665
|
+
] }),
|
|
666
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsRow, children: [
|
|
667
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.settingsLabel, children: "颜色" }),
|
|
668
|
+
(0, react_jsx_runtime.jsx)("div", { style: S.swatchRow, children: [
|
|
669
|
+
{ value: "ocean", label: "海洋蓝" },
|
|
670
|
+
{ value: "sunset", label: "落日橙" },
|
|
671
|
+
{ value: "forest", label: "森林绿" },
|
|
672
|
+
].map((t) => {
|
|
673
|
+
const tokens = colorThemeTokens(t.value);
|
|
674
|
+
return (0, react_jsx_runtime.jsxs)("button", {
|
|
675
|
+
key: t.value,
|
|
676
|
+
type: "button",
|
|
677
|
+
style: value.colorTheme === t.value ? { ...S.swatchBtn, ...S.swatchActive } : S.swatchBtn,
|
|
678
|
+
disabled: saving,
|
|
679
|
+
onClick: () => setField({ colorTheme: t.value }),
|
|
680
|
+
children: [
|
|
681
|
+
(0, react_jsx_runtime.jsx)("span", { style: { ...S.swatchDot, background: tokens.heading } }),
|
|
682
|
+
t.label,
|
|
683
|
+
],
|
|
684
|
+
}, t.value);
|
|
685
|
+
}) }),
|
|
686
|
+
] }),
|
|
687
|
+
] }),
|
|
688
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.settingsHint, children: "主题改动在脑图面板下次打开时生效;背景色始终跟随全局主题。" }),
|
|
689
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.settingsGroupTitle, children: "面板" }),
|
|
690
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsGroup, children: [
|
|
691
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsRow, children: [
|
|
692
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.settingsLabel, children: "默认宽度(%)" }),
|
|
693
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
694
|
+
type: "number",
|
|
695
|
+
min: 20,
|
|
696
|
+
max: 80,
|
|
697
|
+
value: value.defaultPanelWidth,
|
|
698
|
+
disabled: saving,
|
|
699
|
+
onChange: changeWidth,
|
|
700
|
+
onBlur: commitWidth,
|
|
701
|
+
style: S.settingsInput,
|
|
702
|
+
}),
|
|
703
|
+
] }),
|
|
704
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.settingsHint, children: "范围 20~80。拖拽面板后的宽度会记住(localStorage);清除本地记忆后回到这里配置的默认值。" }),
|
|
705
|
+
] }),
|
|
706
|
+
saving ? (0, react_jsx_runtime.jsx)("p", { style: S.settingsHint, children: "保存中…" }) : null,
|
|
707
|
+
notice ? (0, react_jsx_runtime.jsx)("p", { style: S.settingsNotice, children: notice }) : null,
|
|
708
|
+
error ? (0, react_jsx_runtime.jsx)("p", { style: S.settingsError, children: error }) : null,
|
|
709
|
+
] });
|
|
710
|
+
}
|
|
711
|
+
|
|
511
712
|
/**
|
|
512
713
|
* 「思维脑图」槽位组件(014):同一槽位渲染 M 按钮 + 悬浮面板宿主层。
|
|
513
714
|
* session scope 的 useSession/sessionId/inputActions 直给,经 props 传给
|
|
@@ -560,16 +761,19 @@ window.__ModuleLoader__.load({
|
|
|
560
761
|
}
|
|
561
762
|
|
|
562
763
|
function NodeBox(props) {
|
|
563
|
-
const { node } = props;
|
|
764
|
+
const { node, theme } = props;
|
|
765
|
+
// 015 节点主题:卡片圆角(圆角/直角)+ 颜色主题令牌(根盒/标题用主题色)。
|
|
766
|
+
const tokens = colorThemeTokens(theme && theme.colorTheme);
|
|
767
|
+
const radius = theme && theme.cardStyle === "square" ? 0 : 10;
|
|
564
768
|
const style = node.kind === "root"
|
|
565
|
-
? { ...S.box, ...S.rootBox }
|
|
769
|
+
? { ...S.box, ...S.rootBox, borderRadius: radius, borderColor: tokens.rootBorder, background: tokens.rootBg }
|
|
566
770
|
: node.kind === "heading"
|
|
567
|
-
? { ...S.box, ...S.headingBox }
|
|
771
|
+
? { ...S.box, ...S.headingBox, borderRadius: radius, color: tokens.heading }
|
|
568
772
|
: node.kind === "placeholder"
|
|
569
|
-
? { ...S.placeholderBox }
|
|
773
|
+
? { ...S.placeholderBox, borderRadius: radius }
|
|
570
774
|
: node.kind === "code"
|
|
571
|
-
? { ...S.box, ...S.codeBox }
|
|
572
|
-
: S.box;
|
|
775
|
+
? { ...S.box, ...S.codeBox, borderRadius: radius }
|
|
776
|
+
: { ...S.box, borderRadius: radius };
|
|
573
777
|
const title = node.data?.description
|
|
574
778
|
? `${node.topic}\n\n${node.data.description}`
|
|
575
779
|
: node.data?.code
|
|
@@ -578,21 +782,23 @@ window.__ModuleLoader__.load({
|
|
|
578
782
|
return (0, react_jsx_runtime.jsx)("div", { style, title, children: node.kind === "placeholder" ? "待填写" : node.topic });
|
|
579
783
|
}
|
|
580
784
|
|
|
581
|
-
/** 左→右递归树:节点盒 + 右侧子节点列 +
|
|
785
|
+
/** 左→右递归树:节点盒 + 右侧子节点列 + 连线层(015 支持折线/曲线两种线型)。 */
|
|
582
786
|
function TreeRow(props) {
|
|
583
|
-
const { node } = props;
|
|
787
|
+
const { node, theme } = props;
|
|
584
788
|
const rowRef = react.useRef(null);
|
|
585
789
|
const boxWrapRef = react.useRef(null);
|
|
586
790
|
const childRefs = react.useRef([]);
|
|
587
791
|
const [edges, setEdges] = react.useState([]);
|
|
588
792
|
const prevEdgesRef = react.useRef("");
|
|
589
793
|
|
|
590
|
-
//
|
|
591
|
-
//
|
|
794
|
+
// 测量父盒右缘与各子节点包裹块的几何位置,画连线
|
|
795
|
+
// (折线 = M x1 y1 H midX V y2 H x2;曲线 = 贝塞尔水平切出);
|
|
796
|
+
// 序列化比对防 setState 循环。
|
|
592
797
|
react.useLayoutEffect(() => {
|
|
593
798
|
const rowEl = rowRef.current;
|
|
594
799
|
const boxEl = boxWrapRef.current;
|
|
595
800
|
if (!rowEl || !boxEl) return;
|
|
801
|
+
const curve = theme && theme.lineStyle === "curve";
|
|
596
802
|
const measure = () => {
|
|
597
803
|
const rowRect = rowEl.getBoundingClientRect();
|
|
598
804
|
const boxRect = boxEl.getBoundingClientRect();
|
|
@@ -605,7 +811,9 @@ window.__ModuleLoader__.load({
|
|
|
605
811
|
const x2 = c.left - rowRect.left;
|
|
606
812
|
const y2 = c.top - rowRect.top + c.height / 2;
|
|
607
813
|
const midX = (x1 + x2) / 2;
|
|
608
|
-
next.push(
|
|
814
|
+
next.push(curve
|
|
815
|
+
? `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`
|
|
816
|
+
: `M ${x1} ${y1} H ${midX} V ${y2} H ${x2}`);
|
|
609
817
|
}
|
|
610
818
|
const key = next.join("|");
|
|
611
819
|
if (prevEdgesRef.current === key) return;
|
|
@@ -639,14 +847,14 @@ window.__ModuleLoader__.load({
|
|
|
639
847
|
}, i)),
|
|
640
848
|
})
|
|
641
849
|
: null,
|
|
642
|
-
(0, react_jsx_runtime.jsx)("div", { ref: boxWrapRef, style: { flex: "0 0 auto" }, children: (0, react_jsx_runtime.jsx)(NodeBox, { node }) }),
|
|
850
|
+
(0, react_jsx_runtime.jsx)("div", { ref: boxWrapRef, style: { flex: "0 0 auto" }, children: (0, react_jsx_runtime.jsx)(NodeBox, { node, theme }) }),
|
|
643
851
|
node.children && node.children.length > 0
|
|
644
852
|
? (0, react_jsx_runtime.jsx)("div", { style: S.childrenColumn, children: node.children.map((child, idx) => (0, react_jsx_runtime.jsx)("div", {
|
|
645
853
|
key: child.id,
|
|
646
854
|
ref: (el) => {
|
|
647
855
|
childRefs.current[idx] = el;
|
|
648
856
|
},
|
|
649
|
-
children: (0, react_jsx_runtime.jsx)(TreeRow, { node: child }),
|
|
857
|
+
children: (0, react_jsx_runtime.jsx)(TreeRow, { node: child, theme }),
|
|
650
858
|
}, child.id)) })
|
|
651
859
|
: null,
|
|
652
860
|
] });
|
|
@@ -678,6 +886,43 @@ window.__ModuleLoader__.load({
|
|
|
678
886
|
setPanelWidth(Math.round(window.innerWidth * 0.8));
|
|
679
887
|
}
|
|
680
888
|
}, []);
|
|
889
|
+
// 015 设置面板:没有本地拖拽记忆时,用 settings 里的默认宽度。
|
|
890
|
+
react.useEffect(() => {
|
|
891
|
+
let hasLocal = false;
|
|
892
|
+
try {
|
|
893
|
+
hasLocal = localStorage.getItem(WIDTH_KEY) !== null;
|
|
894
|
+
} catch {
|
|
895
|
+
// 忽略
|
|
896
|
+
}
|
|
897
|
+
if (hasLocal) return;
|
|
898
|
+
if (!mindmapFace || typeof mindmapFace.readSettings !== "function") return;
|
|
899
|
+
mindmapFace.readSettings().then((v) => {
|
|
900
|
+
const pct = v && typeof v.defaultPanelWidth === "number" ? Math.min(80, Math.max(20, v.defaultPanelWidth)) : 42;
|
|
901
|
+
const px = Math.round(window.innerWidth * pct / 100);
|
|
902
|
+
setPanelWidth((prev) => (Math.abs(prev - px) < 2 ? prev : px));
|
|
903
|
+
}).catch(() => {
|
|
904
|
+
// 读设置失败:保持 42% 默认
|
|
905
|
+
});
|
|
906
|
+
}, [mindmapFace]);
|
|
907
|
+
|
|
908
|
+
// 015 节点主题:面板每次打开、或设置总线 bump(设置页保存)时重读
|
|
909
|
+
// settings——面板常驻不卸载,光靠 open 变化会漏掉「开着面板改设置」。
|
|
910
|
+
const settingsStamp = react.useSyncExternalStore(settingsBus.subscribe, settingsBus.get);
|
|
911
|
+
const [theme, setTheme] = react.useState({ lineStyle: "elbow", cardStyle: "rounded", colorTheme: "ocean" });
|
|
912
|
+
react.useEffect(() => {
|
|
913
|
+
if (!open) return;
|
|
914
|
+
if (!mindmapFace || typeof mindmapFace.readSettings !== "function") return;
|
|
915
|
+
mindmapFace.readSettings().then((v) => {
|
|
916
|
+
if (!v) return;
|
|
917
|
+
setTheme({
|
|
918
|
+
lineStyle: v.lineStyle === "curve" ? "curve" : "elbow",
|
|
919
|
+
cardStyle: v.cardStyle === "square" ? "square" : "rounded",
|
|
920
|
+
colorTheme: COLOR_THEMES[v.colorTheme] ? v.colorTheme : "ocean",
|
|
921
|
+
});
|
|
922
|
+
}).catch(() => {
|
|
923
|
+
// 读设置失败:保持当前主题
|
|
924
|
+
});
|
|
925
|
+
}, [open, settingsStamp, mindmapFace]);
|
|
681
926
|
const dragStateRef = react.useRef(null);
|
|
682
927
|
function startResize(e) {
|
|
683
928
|
e.preventDefault();
|
|
@@ -1290,7 +1535,7 @@ window.__ModuleLoader__.load({
|
|
|
1290
1535
|
: (doc && doc.op === "local")
|
|
1291
1536
|
? renderLoading()
|
|
1292
1537
|
: tree
|
|
1293
|
-
? (0, react_jsx_runtime.jsx)(TreeRow, { node: tree })
|
|
1538
|
+
? (0, react_jsx_runtime.jsx)(TreeRow, { node: tree, theme })
|
|
1294
1539
|
: renderTree() }),
|
|
1295
1540
|
tabMenu ? (0, react_jsx_runtime.jsxs)("div", {
|
|
1296
1541
|
style: { ...S.treeMenu, left: tabMenu.x, top: tabMenu.y },
|
|
@@ -1317,16 +1562,17 @@ window.__ModuleLoader__.load({
|
|
|
1317
1562
|
|
|
1318
1563
|
// 014「布局让位」CSS(better-sidebar 同款机制):面板打开时给 #root 挂
|
|
1319
1564
|
// margin-right + 宽度挤压,把聊天区推到左边、面板占右侧腾出的空间,
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1565
|
+
// 互不遮挡。015 修复级联冲突:它家(dsh-better-sidebar)同样注入
|
|
1566
|
+
// #root 规则,后注入者胜导致我们的推挤被压掉——我们的规则加
|
|
1567
|
+
// !important 且把双方变量相加(它开面板时聊天同样让位),无论注入
|
|
1568
|
+
// 顺序如何都稳定生效。若它家未来也用 !important,需再评估(见 docs/014)。
|
|
1323
1569
|
if (typeof document !== "undefined") {
|
|
1324
1570
|
const style = document.createElement("style");
|
|
1325
1571
|
style.setAttribute("data-dsh-mindmap", "layout-push");
|
|
1326
1572
|
style.textContent = [
|
|
1327
1573
|
"#root{",
|
|
1328
|
-
"margin-right:var(--dsh-mindmap-width,0px);",
|
|
1329
|
-
"width:calc(100% - var(--dsh-mindmap-width,0px));",
|
|
1574
|
+
"margin-right:calc(var(--dsh-mindmap-width,0px) + var(--dsh-sidebar-width,0px))!important;",
|
|
1575
|
+
"width:calc(100% - var(--dsh-mindmap-width,0px) - var(--dsh-sidebar-width,0px))!important;",
|
|
1330
1576
|
"transition:margin-right var(--ds-transition-duration-slow) var(--ds-ease-in-out),width var(--ds-transition-duration-slow) var(--ds-ease-in-out);",
|
|
1331
1577
|
"}",
|
|
1332
1578
|
].join("");
|
|
@@ -1349,6 +1595,35 @@ window.__ModuleLoader__.load({
|
|
|
1349
1595
|
return parsed.value;
|
|
1350
1596
|
};
|
|
1351
1597
|
|
|
1598
|
+
// 015 设置面板:settings namespace(dsh-grafana 同款读写面)。
|
|
1599
|
+
// connection 走 ctx.get 可选查取(动态 ctx 契约);缺失时设置面板降级提示。
|
|
1600
|
+
const connection = ctx.get("connection");
|
|
1601
|
+
const settingsApi = connection && typeof connection.api === "object" ? connection.api : null;
|
|
1602
|
+
face.readSettings = async () => {
|
|
1603
|
+
if (!settingsApi || typeof settingsApi.settings?.describe !== "function") return null;
|
|
1604
|
+
const res = await settingsApi.settings.describe({});
|
|
1605
|
+
const namespaces = res?.result?.value?.namespaces ?? [];
|
|
1606
|
+
const ns = namespaces.find((n) => n?.ns === "mindmap");
|
|
1607
|
+
return ns?.value ?? null;
|
|
1608
|
+
};
|
|
1609
|
+
face.updateSettings = async (patch) => {
|
|
1610
|
+
if (!settingsApi || typeof settingsApi.settings?.update !== "function") {
|
|
1611
|
+
throw new Error("settings service unavailable");
|
|
1612
|
+
}
|
|
1613
|
+
await settingsApi.settings.update({ ns: "mindmap", patch });
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1616
|
+
// 015 设置面板:settings.section(list 槽、root scope)——设置页左栏
|
|
1617
|
+
// 新增「思维脑图」导航项(better-sidebar 同款入口;dsh-grafana 的
|
|
1618
|
+
// settings.plugin.item 卡片是另一条路,未采用)。
|
|
1619
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1620
|
+
name: "settings.section",
|
|
1621
|
+
id: "dsh-mindmap",
|
|
1622
|
+
order: 100,
|
|
1623
|
+
label: "思维脑图",
|
|
1624
|
+
inject: () => ({ mindmapFace: face }),
|
|
1625
|
+
}, SettingsPanel));
|
|
1626
|
+
|
|
1352
1627
|
// 014 overlay 形态(作者拍板,见 docs/014):面板宿主层(position:fixed)
|
|
1353
1628
|
// 与 M 按钮一起渲染在 conversation.session.header.actions 槽位里——
|
|
1354
1629
|
// better-sidebar 同款「fixed 宿主层自举」思路(它的宿主层挂在
|
|
@@ -1375,6 +1650,7 @@ window.__ModuleLoader__.load({
|
|
|
1375
1650
|
createIdFactory,
|
|
1376
1651
|
relPathWithin,
|
|
1377
1652
|
visibleTreeRows,
|
|
1653
|
+
colorThemeTokens,
|
|
1378
1654
|
TOOL_NAMES,
|
|
1379
1655
|
OPENING_OPS,
|
|
1380
1656
|
});
|
package/index.js
CHANGED
|
@@ -10,16 +10,29 @@
|
|
|
10
10
|
// - 结果 JSON {ok, op, path, rootTitle, content, renamedFrom?}:content 全文
|
|
11
11
|
// 供模型续编辑,client 用同一份重放面板(工具结果即实时通道,002 第二节)。
|
|
12
12
|
// - requireApproval 配置(决策 6):默认 false 免审批;置 true 时 mindmap_update
|
|
13
|
-
// 走原生 ask(tools/pre-execute,照 dsh-grafana
|
|
14
|
-
//
|
|
15
|
-
// -
|
|
16
|
-
//
|
|
13
|
+
// 走原生 ask(tools/pre-execute,照 dsh-grafana 的钩子模式)。015 起经 settings
|
|
14
|
+
// namespace 可在设置面板运行时切换(见 SETTINGS_NAMESPACE/Config)。
|
|
15
|
+
// - 依赖:仅 @deepseek-ai/schemastery(settings schema;发布包正常解析,
|
|
16
|
+
// link 开发需先 npm i)。工具参数 schema 仍手写 JSON Schema(003 偏差 1)。
|
|
17
17
|
import { access, opendir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
18
18
|
import { dirname, isAbsolute, join, relative, resolve as resolvePath } from 'node:path'
|
|
19
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
19
20
|
|
|
20
21
|
export const name = 'mindmap'
|
|
21
22
|
export const inject = ['tools', 'systemPrompt', 'webServer', 'sessions']
|
|
22
23
|
|
|
24
|
+
// 015 设置面板:settings namespace(dsh-grafana 同款模式)。
|
|
25
|
+
// requireApproval 在 pre-execute 时读当前值(运行时切换即时生效);
|
|
26
|
+
// defaultPanelWidth 供客户端面板取默认宽度(20-80 钳制由客户端执行)。
|
|
27
|
+
export const SETTINGS_NAMESPACE = 'mindmap'
|
|
28
|
+
export const Config = Schema.object({
|
|
29
|
+
requireApproval: Schema.boolean().default(false).description('Require native DSH approval for every mindmap_update (including renameRoot). Files are git-managed, so it defaults to off. Hidden from the settings panel; still honored at runtime.'),
|
|
30
|
+
defaultPanelWidth: Schema.number().default(42).description('Default floating-panel width as a percentage of the viewport (clamped 20-80 on the client).'),
|
|
31
|
+
lineStyle: Schema.union(['curve', 'elbow']).default('elbow').description('Connector line style between nodes: curve (bezier) or elbow (orthogonal).'),
|
|
32
|
+
cardStyle: Schema.union(['rounded', 'square']).default('rounded').description('Node card corner style.'),
|
|
33
|
+
colorTheme: Schema.union(['ocean', 'sunset', 'forest']).default('ocean').description('Node color theme.'),
|
|
34
|
+
})
|
|
35
|
+
|
|
23
36
|
const MAX_CONTENT_BYTES = 2 * 1024 * 1024
|
|
24
37
|
const MAX_NAME_CHARS = 80
|
|
25
38
|
const TOOL_TIMEOUT_MS = 15_000
|
|
@@ -224,25 +237,38 @@ function defineTool(spec) {
|
|
|
224
237
|
}
|
|
225
238
|
|
|
226
239
|
export function apply(ctx, config = {}) {
|
|
227
|
-
const
|
|
228
|
-
ctx.systemPrompt.section({ name: 'tool:mindmap', order: 106, text: GUIDANCE })
|
|
240
|
+
const entryConfig = { requireApproval: config.requireApproval === true, defaultPanelWidth: 42 }
|
|
229
241
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
kind: 'ask',
|
|
242
|
-
reason: `Write mindmap ${JSON.stringify(String(args.path ?? '?'))} (${bytes} bytes${renameNote}). dsh-mindmap is configured with requireApproval.`,
|
|
243
|
-
}
|
|
242
|
+
// 015 设置面板:settings 服务可用时以命名空间解析值为准
|
|
243
|
+
// (schema 默认 → 组合层 base → 用户设置层),否则回退入口配置
|
|
244
|
+
// (dsh-grafana 同款模式;link 环境缺 schemastery 时见 003 偏差 1 的
|
|
245
|
+
// 依赖说明——发布包正常安装依赖)。
|
|
246
|
+
let activeConfig = () => entryConfig
|
|
247
|
+
ctx.inject(['settings'], (sctx) => {
|
|
248
|
+
const scope = sctx.settings.register(SETTINGS_NAMESPACE, Config, { base: entryConfig })
|
|
249
|
+
activeConfig = () => scope.get()
|
|
250
|
+
sctx.effect(() => () => {
|
|
251
|
+
activeConfig = () => entryConfig
|
|
244
252
|
})
|
|
245
|
-
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
ctx.systemPrompt.section({ name: 'tool:mindmap', order: 106, text: GUIDANCE })
|
|
256
|
+
|
|
257
|
+
// 后悔药开关(决策 6):钩子常驻注册,运行时读 activeConfig().requireApproval
|
|
258
|
+
// ——设置面板切换立即生效;关闭时直接放行(默认 false 免审批)。
|
|
259
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
260
|
+
const decision = await next()
|
|
261
|
+
if (decision.kind !== 'allow') return decision
|
|
262
|
+
if (!activeConfig().requireApproval) return decision
|
|
263
|
+
if (exec.name !== 'mindmap_update') return decision
|
|
264
|
+
const args = exec.arguments ?? {}
|
|
265
|
+
const renameNote = typeof args.renameRoot === 'string' && args.renameRoot ? `, rename root to "${args.renameRoot}"` : ''
|
|
266
|
+
const bytes = typeof args.content === 'string' ? byteLength(args.content) : 0
|
|
267
|
+
return {
|
|
268
|
+
kind: 'ask',
|
|
269
|
+
reason: `Write mindmap ${JSON.stringify(String(args.path ?? '?'))} (${bytes} bytes${renameNote}). dsh-mindmap is configured with requireApproval.`,
|
|
270
|
+
}
|
|
271
|
+
})
|
|
246
272
|
|
|
247
273
|
ctx.tools.register(defineTool({
|
|
248
274
|
name: 'mindmap_create',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mindmap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Mindmap plugin for DeepSeek Harness: a plain markdown file in the working directory IS the mindmap; the chat edits it step by step and the right-side panel follows live.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"README.zh-CN.md",
|
|
18
18
|
"CHANGELOG.md"
|
|
19
19
|
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@deepseek-ai/schemastery": "^3.18.0"
|
|
22
|
+
},
|
|
20
23
|
"dsh": {
|
|
21
24
|
"bundle": {
|
|
22
25
|
"patch": "./cordis.patch.yml"
|