dsh-mindmap 0.9.0 → 0.10.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 +6 -0
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/client.js +160 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,14 @@ All notable changes to this project are documented here. Release-specific notes
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Collapsible subtrees: every node with children shows a small toggle on its connector. Collapsing hides the subtree and reports how many nodes are hidden, which keeps large maps navigable. The state is view-only — the markdown file is untouched and image export still covers the full subtree. Collapsed nodes reset when the document changes, and stale ids are pruned after the AI rewrites the tree.
|
|
10
|
+
|
|
7
11
|
### Fixed
|
|
8
12
|
|
|
13
|
+
- 画布拖拽不再吞掉折叠开关等控件的点击。
|
|
14
|
+
- 目录打开/焦点同步:宿主草稿 API 不可读时不再静默放弃;有非空草稿时指令转入剪贴板。
|
|
9
15
|
- Settings now wait for a late-arriving client connection instead of permanently capturing its initial absence. The client also supports direct settings descriptors and the current positional update API, while retaining the legacy envelopes.
|
|
10
16
|
- Canvas dragging remains available horizontally and vertically when the map fits inside the viewport. Native scrolling is used first; bounded offset compensation continues the drag at an edge, and Fit or node focus returns the map to center.
|
|
11
17
|
- Mindmap file access now fails closed when the session working directory is unavailable. Reads are limited during streaming rather than trusting a pre-read size check, and updates use a same-directory temporary file plus atomic replacement to avoid partial writes.
|
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ A DeepSeek Harness plugin that turns a plain Markdown file into a live mindmap.
|
|
|
24
24
|
- **"What you see is what the AI edits"** — when the visible mindmap differs from the AI's working document, the panel automatically asks the AI to open it, keeping the chat focus in sync.
|
|
25
25
|
- **MarkGrove-style mapping** — heading hierarchy, nested lists (empty items become placeholder nodes), code blocks as leaf nodes, paragraphs promoted to their own nodes (019 block concept), stable structural IDs, and orthogonal connector lines between nodes.
|
|
26
26
|
- **Centered canvas with zoom and pan** — the mindmap opens centered in the canvas (scrollable without edge clipping when larger); a floating zoom bar at the canvas top-right (zoom out / percent / zoom in / fit) applies auto fit-to-view on open (small maps stay at 100%), steps through 25%–300% with a stable view center, and keeps re-fitting as the AI edits — until you zoom manually. Click any node to zoom in on it and its whole subtree, with the node pinned at the left-center of the canvas. The canvas also pans by drag: the **middle button** anywhere (even over a node), the **left button on blank canvas** (the Mac trackpad「click and drag」path), or **Space + left button** when the drag must start on a card. Panning works in both directions even when content does not overflow; content follows the pointer 1:1, blank space shows a grab hand, and a 4px threshold separates drag from click — so clicking blank space still clears the selection and clicking a node still focuses it, while a real drag never wipes the selection ring.
|
|
27
|
+
- **Collapsible subtrees** — every node with children carries a small toggle on its connector: collapsing hides the whole subtree and reports how many nodes are hidden, so large maps stay navigable. It is view state only — the markdown file is untouched, image export still covers the full subtree, and switching documents expands everything again.
|
|
27
28
|
- **PNG export** — one click on 导出图片 exports the current mindmap.
|
|
28
29
|
- **Safety** — `mindmap_create` / `mindmap_update` require native approval by default. Automation can explicitly opt out with `requireApproval: false`; the client has **no write path** to the filesystem — every edit goes through the AI tools.
|
|
29
30
|
|
package/README.zh-CN.md
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
- **「所见即所编」焦点同步**——可见脑图与 AI 工作文档不一致时,面板自动让 AI 打开它,聊天焦点始终跟随你的眼睛。
|
|
25
25
|
- **MarkGrove 同款映射与连线**——标题层级挂树、列表缩进(空项 = 占位节点)、代码块叶节点、段落升格为独立节点(019 块概念)、稳定结构 ID、节点间直角折线。
|
|
26
26
|
- **画布居中、缩放与平移**——脑图打开后居中呈现(超出画布时可滚动、无边缘裁剪);画布右上角浮动缩放条(缩小 / 比例 / 放大 / 适配),打开时自动适配合适比例(小图保持 100%),25%–300% 逐级缩放且视图中心不跳变,AI 编辑后持续自动再适配,直到你手动缩放。点击任意节点即可聚焦:节点滚到画布左侧居中,视图放大到它和整棵子树完整可见(上限 100%)。画布还能拖着走:**中键**随处拖(含节点上)、**空白处左键拖**(Mac 触控板「点按并拖移」)、或 **Space + 左键拖**(必须从节点上起手时);无论内容是否溢出都可横纵双向拖动,内容跟手 1:1,空白处呈抓手光标,4px 阈值把拖与点分开——点空白仍取消选中、点节点仍聚焦,真拖不会误清选中圈。
|
|
27
|
+
- **子树折叠**——有子节点的节点在连线起点上带一个小开关:折叠后隐藏整棵子树并提示隐藏了多少节点,超大脑图也能逐层看。折叠只是视图状态,不改 markdown 文件,导出图片仍是完整子树;切换文档自动全部展开。
|
|
27
28
|
- **PNG 导出**——面板右上角「导出图片」一键导出当前脑图。
|
|
28
29
|
- **安全**——`mindmap_create`/`mindmap_update` 默认进入原生审批;可通过 `requireApproval: false` 为自动化场景明确关闭。客户端**没有任何写文件通道**,一切编辑都经 AI 工具。
|
|
29
30
|
|
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
|
// 布局契约不变——叶子自上而下占行、父节点垂直居中于其子块。
|
|
@@ -1316,7 +1385,11 @@ window.__ModuleLoader__.load({
|
|
|
1316
1385
|
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
1386
|
swatchDot: { width: "10px", height: "10px", borderRadius: "50%", flex: "none" },
|
|
1318
1387
|
row: { display: "flex", alignItems: "center", minWidth: 0 },
|
|
1319
|
-
|
|
1388
|
+
// 025:子列左距 16 + 折叠开关(16 宽 + 左右各 4 外距)= 折叠前的 40,
|
|
1389
|
+
// 连线长度与既有版式保持一致。
|
|
1390
|
+
childrenColumn: { display: "flex", flexDirection: "column", gap: "8px", marginLeft: "16px", minWidth: 0 },
|
|
1391
|
+
// 025 折叠开关:压在连线起点上的小圆钮,叶子节点不渲染。
|
|
1392
|
+
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
1393
|
// 面板树连线层:正交折线(MarkGrove 的 orthogonalPath 风格),
|
|
1321
1394
|
// 覆盖整行、点击穿透、置于节点盒之下。
|
|
1322
1395
|
// 016:去掉 CSS width/height 百分比——在 auto-height 的 flex 行内,
|
|
@@ -1784,7 +1857,10 @@ window.__ModuleLoader__.load({
|
|
|
1784
1857
|
|
|
1785
1858
|
/** 左→右递归树:节点盒 + 右侧子节点列 + 连线层(015 支持折线/曲线两种线型)。 */
|
|
1786
1859
|
function TreeRow(props) {
|
|
1787
|
-
const { node, theme, onNodeContextMenu, reveal, selectedId, onCodePanel } = props;
|
|
1860
|
+
const { node, theme, onNodeContextMenu, reveal, selectedId, onCodePanel, collapsed, onToggleCollapse } = props;
|
|
1861
|
+
// 025 折叠:纯视图态——markdown 资产不变,导出仍取完整子树。
|
|
1862
|
+
const hasChildren = Boolean(node.children && node.children.length > 0);
|
|
1863
|
+
const isCollapsed = hasChildren && Boolean(collapsed && collapsed.has(node.id));
|
|
1788
1864
|
// 018 生长动画:本节点渐显延迟(新节点盒)与本行连线渐显延迟(有新子节点)。
|
|
1789
1865
|
const revealDelay = reveal && reveal.nodes ? reveal.nodes.get(node.id) : undefined;
|
|
1790
1866
|
const edgeRevealDelay = reveal && reveal.edges ? reveal.edges.get(node.id) : undefined;
|
|
@@ -1821,7 +1897,8 @@ window.__ModuleLoader__.load({
|
|
|
1821
1897
|
const rowRect = rowEl.getBoundingClientRect();
|
|
1822
1898
|
const boxRect = boxEl.getBoundingClientRect();
|
|
1823
1899
|
const next = [];
|
|
1824
|
-
|
|
1900
|
+
// 折叠时子列未挂载:不量、不画线(ref 回调已置空,这里再兜一层)。
|
|
1901
|
+
for (const ref of isCollapsed ? [] : childRefs.current) {
|
|
1825
1902
|
if (!ref) continue;
|
|
1826
1903
|
const c = ref.getBoundingClientRect();
|
|
1827
1904
|
// 视觉像素 → 行本地坐标(SVG 用户空间 = 本地空间)。
|
|
@@ -1899,13 +1976,30 @@ window.__ModuleLoader__.load({
|
|
|
1899
1976
|
} : undefined,
|
|
1900
1977
|
children: (0, react_jsx_runtime.jsx)(NodeBox, { node, theme, revealDelay, selectedId, onCodePanel }),
|
|
1901
1978
|
}),
|
|
1902
|
-
|
|
1903
|
-
|
|
1979
|
+
// 025 折叠开关:坐在盒与子列之间的连线起点上(有子节点才出现)。
|
|
1980
|
+
// stopPropagation 保证点它不触发画布的「点节点聚焦 / 点空白取消选中」。
|
|
1981
|
+
hasChildren
|
|
1982
|
+
? (0, react_jsx_runtime.jsx)("button", {
|
|
1983
|
+
type: "button",
|
|
1984
|
+
"data-mindmap-collapse": "",
|
|
1985
|
+
style: S.collapseToggle,
|
|
1986
|
+
title: isCollapsed ? `展开子树(已隐藏 ${countDescendants(node)} 个节点)` : "折叠子树",
|
|
1987
|
+
"aria-expanded": isCollapsed ? "false" : "true",
|
|
1988
|
+
onClick: (e) => {
|
|
1989
|
+
e.preventDefault();
|
|
1990
|
+
e.stopPropagation();
|
|
1991
|
+
if (onToggleCollapse) onToggleCollapse(node.id);
|
|
1992
|
+
},
|
|
1993
|
+
children: isCollapsed ? "+" : "−",
|
|
1994
|
+
})
|
|
1995
|
+
: null,
|
|
1996
|
+
hasChildren && !isCollapsed
|
|
1997
|
+
? (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
1998
|
key: child.id,
|
|
1905
1999
|
ref: (el) => {
|
|
1906
2000
|
childRefs.current[idx] = el;
|
|
1907
2001
|
},
|
|
1908
|
-
children: (0, react_jsx_runtime.jsx)(TreeRow, { node: child, theme, onNodeContextMenu, reveal, selectedId, onCodePanel }),
|
|
2002
|
+
children: (0, react_jsx_runtime.jsx)(TreeRow, { node: child, theme, onNodeContextMenu, reveal, selectedId, onCodePanel, collapsed, onToggleCollapse }),
|
|
1909
2003
|
}, child.id)) })
|
|
1910
2004
|
: null,
|
|
1911
2005
|
] });
|
|
@@ -1987,6 +2081,12 @@ window.__ModuleLoader__.load({
|
|
|
1987
2081
|
};
|
|
1988
2082
|
}
|
|
1989
2083
|
|
|
2084
|
+
/** 按下点是否落在画布内的交互控件上(这类按下不启动平移,留给控件自己)。 */
|
|
2085
|
+
function isCanvasControl(el) {
|
|
2086
|
+
if (!el || typeof el.closest !== "function") return false;
|
|
2087
|
+
return Boolean(el.closest("button, a[href], input, textarea, select, [role='button']"));
|
|
2088
|
+
}
|
|
2089
|
+
|
|
1990
2090
|
/** 空格键是否落在可输入元素里(聊天框/输入框与面板同 document,不能抢空格)。 */
|
|
1991
2091
|
function isTextEntry(el) {
|
|
1992
2092
|
if (!el || typeof el.tagName !== "string") return false;
|
|
@@ -2045,6 +2145,9 @@ window.__ModuleLoader__.load({
|
|
|
2045
2145
|
const nodeMenuRef = react.useRef(null);
|
|
2046
2146
|
// 019 选中态:点击聚焦的节点下选选中环(002 §6 状态体系)。
|
|
2047
2147
|
const [selectedId, setSelectedId] = react.useState(null);
|
|
2148
|
+
// 025 折叠子树:纯视图态(不写回 markdown,导出仍取完整子树)。
|
|
2149
|
+
// 切换文档时全部展开;AI 改写后清掉已消失节点的折叠标记。
|
|
2150
|
+
const [collapsed, setCollapsed] = react.useState(() => new Set());
|
|
2048
2151
|
// 019 代码块悬停浮层:{node, anchor};null = 关闭。延迟关闭(150ms
|
|
2049
2152
|
// 宽限)让鼠标能从节点盒移到面板上滚动全文,不闪灭。
|
|
2050
2153
|
const [codePanel, setCodePanel] = react.useState(null);
|
|
@@ -2108,6 +2211,7 @@ window.__ModuleLoader__.load({
|
|
|
2108
2211
|
userZoomedRef.current = false;
|
|
2109
2212
|
lastNaturalRef.current = null;
|
|
2110
2213
|
fitStampRef.current = [];
|
|
2214
|
+
setCollapsed((prev) => (prev.size > 0 ? new Set() : prev));
|
|
2111
2215
|
const id = requestAnimationFrame(applyFit);
|
|
2112
2216
|
return () => cancelAnimationFrame(id);
|
|
2113
2217
|
}, [fitKey]);
|
|
@@ -2204,6 +2308,10 @@ window.__ModuleLoader__.load({
|
|
|
2204
2308
|
const scroller = scrollRef.current;
|
|
2205
2309
|
if (!scroller || panRef.current) return;
|
|
2206
2310
|
const target = e.target;
|
|
2311
|
+
// 025:画布内的控件(折叠开关等)必须先于平移拿到这次按下。
|
|
2312
|
+
// 否则 setPointerCapture 会把随后的 click 改派到滚动区,按钮
|
|
2313
|
+
// 永远收不到点击——「折叠按钮点了没反应」的根因。
|
|
2314
|
+
if (isCanvasControl(target)) return;
|
|
2207
2315
|
const onNode = Boolean(target && typeof target.closest === "function" && target.closest("[data-mindmap-node]"));
|
|
2208
2316
|
if (!shouldStartPan(e.button, { onNode, spaceHeld: spaceRef.current, touch: e.pointerType === "touch" })) return;
|
|
2209
2317
|
// 中键:掐掉浏览器自动滚动;左键:掐掉拖选文本。
|
|
@@ -2395,8 +2503,14 @@ window.__ModuleLoader__.load({
|
|
|
2395
2503
|
setNodeMenu(null);
|
|
2396
2504
|
// 019:文档内容变化时同步收掉代码浮层(节点对象已失效)。
|
|
2397
2505
|
setCodePanel(null);
|
|
2506
|
+
// 025:树重解析后丢弃已消失节点的折叠标记(无变化时保持原引用)。
|
|
2507
|
+
setCollapsed((prev) => pruneCollapsed(prev, node));
|
|
2398
2508
|
}, [node]);
|
|
2399
2509
|
|
|
2510
|
+
function toggleCollapse(id) {
|
|
2511
|
+
setCollapsed((prev) => toggleCollapsed(prev, id));
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2400
2514
|
// 017 右键节点:记录菜单锚点与目标子树(清掉上次的忙碌/错误态)。
|
|
2401
2515
|
function onNodeContextMenu(e, target) {
|
|
2402
2516
|
setNodeMenuBusy(null);
|
|
@@ -2461,7 +2575,7 @@ window.__ModuleLoader__.load({
|
|
|
2461
2575
|
children:
|
|
2462
2576
|
(0, react_jsx_runtime.jsx)("div", { style: S.canvasCenter, children:
|
|
2463
2577
|
(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 })
|
|
2578
|
+
(0, react_jsx_runtime.jsx)(TreeRow, { node, theme, onNodeContextMenu, reveal, selectedId, onCodePanel: handleCodePanel, collapsed, onToggleCollapse: toggleCollapse })
|
|
2465
2579
|
})
|
|
2466
2580
|
})
|
|
2467
2581
|
}),
|
|
@@ -2842,7 +2956,7 @@ window.__ModuleLoader__.load({
|
|
|
2842
2956
|
if (focusPath === active) return;
|
|
2843
2957
|
if (focusSentRef.current === active) return; // 已发过,等 AI 结果追平
|
|
2844
2958
|
const rel = fsTree.cwd ? relPathWithin(fsTree.cwd, active, stemOf(active)) : active;
|
|
2845
|
-
if (
|
|
2959
|
+
if (submitChatCommand(`用 mindmap_open 打开 ${rel}`)) {
|
|
2846
2960
|
focusSentRef.current = active;
|
|
2847
2961
|
}
|
|
2848
2962
|
}, [active, focusPath, fsTree.cwd, docs, open]);
|
|
@@ -2861,19 +2975,15 @@ window.__ModuleLoader__.load({
|
|
|
2861
2975
|
}
|
|
2862
2976
|
|
|
2863
2977
|
//#region 013 目录树 tab:懒加载树 + 把指令填进聊天输入框
|
|
2864
|
-
//
|
|
2865
|
-
//
|
|
2866
|
-
function
|
|
2867
|
-
|
|
2868
|
-
try {
|
|
2869
|
-
return !String(inputActions.getDraft() ?? "").trim();
|
|
2870
|
-
} catch {
|
|
2871
|
-
return false;
|
|
2872
|
-
}
|
|
2978
|
+
// 草稿保护:确实读到非空草稿才让路(改走剪贴板),读不到就按既有
|
|
2979
|
+
// 行为直填直发——宿主不暴露草稿时不能把功能整个卡死。
|
|
2980
|
+
function draftBlocked() {
|
|
2981
|
+
return draftBlocksAutoSend(inputActions);
|
|
2873
2982
|
}
|
|
2874
2983
|
|
|
2875
|
-
function
|
|
2876
|
-
if (
|
|
2984
|
+
function submitChatCommand(text) {
|
|
2985
|
+
if (draftBlocked()) return false;
|
|
2986
|
+
if (!inputActions || typeof inputActions.setDraft !== "function" || typeof inputActions.submit !== "function") return false;
|
|
2877
2987
|
try {
|
|
2878
2988
|
inputActions.setDraft(text);
|
|
2879
2989
|
inputActions.submit();
|
|
@@ -2883,10 +2993,10 @@ window.__ModuleLoader__.load({
|
|
|
2883
2993
|
}
|
|
2884
2994
|
}
|
|
2885
2995
|
|
|
2886
|
-
//
|
|
2996
|
+
// 手动新建指令同样不覆盖已有草稿;否则填入输入框。
|
|
2887
2997
|
function fillDraft(text) {
|
|
2888
2998
|
try {
|
|
2889
|
-
if (
|
|
2999
|
+
if (!draftBlocked() && inputActions && typeof inputActions.setDraft === "function") {
|
|
2890
3000
|
inputActions.setDraft(text);
|
|
2891
3001
|
setFilledHint("指令已填入聊天输入框");
|
|
2892
3002
|
return;
|
|
@@ -2920,8 +3030,14 @@ window.__ModuleLoader__.load({
|
|
|
2920
3030
|
// 说即可继续编辑(002 数据流不变:内容只来自 AI 工具结果)。
|
|
2921
3031
|
function openMindmap(entry) {
|
|
2922
3032
|
const text = `用 mindmap_open 打开 ${relPathWithin(fsTree.cwd, entry.path, entry.name)}`;
|
|
2923
|
-
|
|
2924
|
-
|
|
3033
|
+
// 有未发送草稿:不建占位、不抢输入框,改把指令交给用户自己发。
|
|
3034
|
+
if (draftBlocked()) {
|
|
3035
|
+
if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
3036
|
+
navigator.clipboard.writeText(text).catch(() => {});
|
|
3037
|
+
setFilledHint("检测到未发送的草稿,已保留;打开指令已复制到剪贴板,粘贴发送即可");
|
|
3038
|
+
} else {
|
|
3039
|
+
setFilledHint(`检测到未发送的草稿,已保留。请手动发送:${text}`);
|
|
3040
|
+
}
|
|
2925
3041
|
return;
|
|
2926
3042
|
}
|
|
2927
3043
|
// 016:记录点击时刻的错误基线(errorByPath 与 latestError 的全部
|
|
@@ -2943,9 +3059,19 @@ window.__ModuleLoader__.load({
|
|
|
2943
3059
|
renamedFrom: null,
|
|
2944
3060
|
},
|
|
2945
3061
|
}));
|
|
2946
|
-
//
|
|
2947
|
-
|
|
2948
|
-
|
|
3062
|
+
// ② AI 就位:填指令并直接提交(失败降级剪贴板)。
|
|
3063
|
+
if (submitChatCommand(text)) {
|
|
3064
|
+
// 标记已发,避免焦点同步 effect 对同一路径重复发送。
|
|
3065
|
+
focusSentRef.current = entry.path;
|
|
3066
|
+
setFilledHint(`已让 AI 打开「${entry.name}」,在聊天里继续说就能继续编辑`);
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
3070
|
+
navigator.clipboard.writeText(text).catch(() => {});
|
|
3071
|
+
setFilledHint("已复制指令到剪贴板,请粘贴到聊天输入框");
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
setFilledHint(text);
|
|
2949
3075
|
}
|
|
2950
3076
|
|
|
2951
3077
|
/** 016 加载态恢复:错误/超时后重试——重发打开指令并重启看门狗。 */
|
|
@@ -3544,6 +3670,14 @@ window.__ModuleLoader__.load({
|
|
|
3544
3670
|
planGrowthReveal,
|
|
3545
3671
|
relPathWithin,
|
|
3546
3672
|
visibleTreeRows,
|
|
3673
|
+
// 025 草稿保护:宿主草稿读取面的能力探测(供测试)。
|
|
3674
|
+
readDraftText,
|
|
3675
|
+
draftBlocksAutoSend,
|
|
3676
|
+
// 025 子树折叠:视图态纯函数 + 节点行组件(供测试)。
|
|
3677
|
+
toggleCollapsed,
|
|
3678
|
+
countDescendants,
|
|
3679
|
+
pruneCollapsed,
|
|
3680
|
+
TreeRow,
|
|
3547
3681
|
// 019 皮肤层与血肉层纯函数(供测试)。
|
|
3548
3682
|
resolveToken,
|
|
3549
3683
|
resolveNodeStyle,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mindmap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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
|
"repository": {
|
|
6
6
|
"type": "git",
|