dsh-mindmap 0.1.0 → 0.3.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 +10 -0
- package/README.md +6 -2
- package/README.zh-CN.md +6 -2
- package/client.js +418 -64
- package/index.js +48 -21
- package/package.json +19 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to this project are documented here. Release-specific notes
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- The browser client is now maintained as focused source fragments under `src/client/` and assembled into the single runtime `client.js` entry by `npm run build:client`.
|
|
10
|
+
- AI create/open/view intent now restores and switches the target mindmap panel smoothly, including repeated opens and open-then-update tool sequences.
|
|
11
|
+
- Added release metadata, a `prepack` verification hook, and an npm package dry-run check to the documented development flow.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
|
|
7
17
|
## [0.1.0] - 2026-08-23
|
|
8
18
|
|
|
9
19
|
First release of dsh-mindmap: a plain Markdown file in the session working directory becomes a live mindmap.
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A DeepSeek Harness plugin that turns a plain Markdown file into a live mindmap.
|
|
|
17
17
|
|
|
18
18
|
- **Four tools** (`mindmap_create` / `mindmap_open` / `mindmap_get` / `mindmap_update`) — plain Markdown files in the session working directory; the root node title is the filename and stays in sync both ways (`renameRoot` renames the file, collisions are rejected).
|
|
19
19
|
- **Live panel with zero extra channels** — the panel consumes the session snapshot (`mindmap_*` tool results), so every AI edit re-renders immediately.
|
|
20
|
-
- **Floating right panel** — toggled by the 思维脑图 button in the session header;
|
|
20
|
+
- **Floating right panel** — toggled by the 思维脑图 button in the session header; AI create/open/view intents also expand it and switch to the target document, including when the panel is currently closed or the same document is opened again. Opening the panel pushes the chat to the left (layout-push, the chat is never covered); width is drag-resizable (280px ~ 80% viewport) and persisted.
|
|
21
21
|
- **Directory tree tab** — a persistent tree of the session working directory (served by a plugin-owned read-only route), lazy-loaded per directory; right-click to create a mindmap (at the root or inside a directory); left-click a `.md` to open it instantly and hand it to the AI for editing.
|
|
22
22
|
- **Single-mindmap mode** — two tabs only: 目录 (tree) and 脑图 (the current mindmap); opening another `.md` replaces the previous one.
|
|
23
23
|
- **"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.
|
|
@@ -58,9 +58,13 @@ dsh plugin --profile <profile> add <pkg>#v<version>
|
|
|
58
58
|
## Development
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
|
-
npm run
|
|
61
|
+
npm run build:client # assemble the runtime client.js from src/client fragments
|
|
62
|
+
npm run verify # rebuild + syntax check + node --test
|
|
63
|
+
npm pack --dry-run # inspect the files that will enter the npm package
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
The browser implementation is maintained under `src/client/` and assembled into the single `client.js` entry required by DeepSeek Harness. Edit the source fragments, then run `npm run build:client`; do not hand-edit the generated entry.
|
|
67
|
+
|
|
64
68
|
## License
|
|
65
69
|
|
|
66
70
|
Not yet chosen — contact the author before redistributing.
|
package/README.zh-CN.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
- **四个工具**(`mindmap_create` / `mindmap_open` / `mindmap_get` / `mindmap_update`)——会话工作目录里的普通 `.md` 文件;根节点标题 = 文件名,双向同步(`renameRoot` 触发文件重命名,撞名报错不覆盖)。
|
|
19
19
|
- **零通道实时面板**——面板直接消费会话快照里的 `mindmap_*` 工具结果,AI 每改一步面板即渲染一次。
|
|
20
|
-
- **右侧悬浮面板**——会话头部「思维脑图」按钮开合;AI
|
|
20
|
+
- **右侧悬浮面板**——会话头部「思维脑图」按钮开合;AI 创建、打开或查看脑图时会自动展开并切换到目标文档,即使面板当前收起或重复打开同一文档也能接续跟随。打开时**聊天区向左让位**(布局推挤,互不遮挡);宽度可拖(280px ~ 80% 视口)并持久化。
|
|
21
21
|
- **常驻目录树 tab**——工作目录结构懒加载树(插件自建只读路由);空白处/目录右键新建脑图;左键点 `.md` 秒开 tab 并自动交给 AI 打开,随后直接对话继续编辑。
|
|
22
22
|
- **单脑图模式**——面板只有「目录」和「脑图」两个 tab,打开新脑图替换旧的那颗。
|
|
23
23
|
- **「所见即所编」焦点同步**——可见脑图与 AI 工作文档不一致时,面板自动让 AI 打开它,聊天焦点始终跟随你的眼睛。
|
|
@@ -58,9 +58,13 @@ dsh plugin --profile <profile> add <pkg>#v<version>
|
|
|
58
58
|
## 开发
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
|
-
npm run
|
|
61
|
+
npm run build:client # 从 src/client 片段组装运行时 client.js
|
|
62
|
+
npm run verify # 重建 + 语法检查 + node --test
|
|
63
|
+
npm pack --dry-run # 检查将进入 npm 包的文件
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
浏览器端实现维护在 `src/client/` 下,构建后仍输出 DeepSeek Harness 要求的单一 `client.js` 入口。请修改源码片段后运行 `npm run build:client`,不要直接手改生成入口。
|
|
67
|
+
|
|
64
68
|
## License
|
|
65
69
|
|
|
66
70
|
尚未确定——再分发前请先联系作者。
|
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) {
|
|
@@ -248,41 +281,97 @@ window.__ModuleLoader__.load({
|
|
|
248
281
|
/**
|
|
249
282
|
* 重放会话快照里的 mindmap_* 工具结果,得到每个脑图文档的最新状态。
|
|
250
283
|
* nodes: ConversationSnapshot.nodes(ToolResultNode 含 call.name 与渲染后的
|
|
251
|
-
* content 文本块——host 的工具结果 JSON 就写在其中)。
|
|
252
|
-
*
|
|
284
|
+
* content 文本块——host 的工具结果 JSON 就写在其中)。Code 等工具的结果
|
|
285
|
+
* 还可能把真实的 mindmap ToolResultNode 放在 subCalls 中,因此这里按事件
|
|
286
|
+
* 顺序递归重放整棵调用树。
|
|
287
|
+
* 返回文档集及最近一次 create/open 意图,用于驱动面板自动切换目标。
|
|
253
288
|
*/
|
|
254
289
|
function reduceDocuments(nodes) {
|
|
255
290
|
const byPath = Object.create(null);
|
|
256
291
|
let order = [];
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
292
|
+
let latestOpeningPath = null;
|
|
293
|
+
let latestOpeningEventKey = null;
|
|
294
|
+
const visited = new WeakSet();
|
|
295
|
+
const MAX_SUBCALL_DEPTH = 100;
|
|
296
|
+
|
|
297
|
+
function replayNode(node, eventPath, depth) {
|
|
298
|
+
if (!node || typeof node !== "object" || visited.has(node)) return;
|
|
299
|
+
visited.add(node);
|
|
300
|
+
|
|
301
|
+
if (node.kind === "tool-result" && !node.isError) {
|
|
302
|
+
const name = node.call?.name;
|
|
303
|
+
if (typeof name === "string" && TOOL_NAMES.has(name)) {
|
|
304
|
+
let parsed;
|
|
305
|
+
try {
|
|
306
|
+
parsed = JSON.parse(resultTextOfBlocks(node.content));
|
|
307
|
+
} catch {
|
|
308
|
+
parsed = null;
|
|
309
|
+
}
|
|
310
|
+
if (parsed && parsed.ok === true && typeof parsed.path === "string" && parsed.path) {
|
|
311
|
+
const op = typeof parsed.op === "string" ? parsed.op : name;
|
|
312
|
+
// callId 是实际调用的事件身份;eventPath 是无 callId 时按会话
|
|
313
|
+
// 遍历顺序生成的稳定兜底,避免重复 open 被合并成一个事件。
|
|
314
|
+
const callId = node.callId != null && String(node.callId)
|
|
315
|
+
? node.callId
|
|
316
|
+
: node.call?.callId;
|
|
317
|
+
const eventKey = callId != null && String(callId)
|
|
318
|
+
? `call:${String(callId)}`
|
|
319
|
+
: `node:${eventPath}`;
|
|
320
|
+
const renamedFrom = typeof parsed.renamedFrom === "string" ? parsed.renamedFrom : null;
|
|
321
|
+
const previous = byPath[parsed.path];
|
|
322
|
+
const renamedDocument = renamedFrom && renamedFrom !== parsed.path
|
|
323
|
+
? byPath[renamedFrom]
|
|
324
|
+
: null;
|
|
325
|
+
const inheritedOpeningEventKey = renamedDocument?.openingEventKey
|
|
326
|
+
?? previous?.openingEventKey
|
|
327
|
+
?? (renamedFrom && latestOpeningPath === renamedFrom ? latestOpeningEventKey : null);
|
|
328
|
+
|
|
329
|
+
// 根节点改名会删除旧路径键;若旧路径正是最近一次打开意图,
|
|
330
|
+
// 同时迁移路径并保留原 openingEventKey,确保自动切换仍生效。
|
|
331
|
+
if (renamedFrom && renamedFrom !== parsed.path && latestOpeningPath === renamedFrom) {
|
|
332
|
+
latestOpeningPath = parsed.path;
|
|
333
|
+
if (latestOpeningEventKey == null) latestOpeningEventKey = inheritedOpeningEventKey;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const openingEventKey = OPENING_OPS.has(op)
|
|
337
|
+
? eventKey
|
|
338
|
+
: inheritedOpeningEventKey;
|
|
339
|
+
if (OPENING_OPS.has(op)) {
|
|
340
|
+
latestOpeningPath = parsed.path;
|
|
341
|
+
latestOpeningEventKey = eventKey;
|
|
342
|
+
}
|
|
343
|
+
if (renamedFrom && renamedDocument && renamedFrom !== parsed.path) {
|
|
344
|
+
delete byPath[renamedFrom];
|
|
345
|
+
order = order.filter((p) => p !== renamedFrom);
|
|
346
|
+
}
|
|
347
|
+
if (!byPath[parsed.path]) order.push(parsed.path);
|
|
348
|
+
byPath[parsed.path] = {
|
|
349
|
+
path: parsed.path,
|
|
350
|
+
rootTitle: typeof parsed.rootTitle === "string" && parsed.rootTitle ? parsed.rootTitle : stemOf(parsed.path),
|
|
351
|
+
content: typeof parsed.content === "string" ? parsed.content : "",
|
|
352
|
+
op,
|
|
353
|
+
callId,
|
|
354
|
+
eventKey,
|
|
355
|
+
openingEventKey,
|
|
356
|
+
// 013:rename 迁移后保留旧路径,供本地直读 tab 清理(mergeDocuments)。
|
|
357
|
+
renamedFrom,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
266
361
|
}
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
362
|
+
|
|
363
|
+
// 父级非 mindmap 工具不参与文档解析,但其 subCalls 仍是会话事件,
|
|
364
|
+
// 必须继续深入;深度上限与 WeakSet 一起防止异常结构卡死。
|
|
365
|
+
if (depth >= MAX_SUBCALL_DEPTH || !Array.isArray(node.subCalls)) return;
|
|
366
|
+
for (const [subCallIndex, subCall] of node.subCalls.entries()) {
|
|
367
|
+
replayNode(subCall, `${eventPath}.${subCallIndex}`, depth + 1);
|
|
273
368
|
}
|
|
274
|
-
if (!byPath[parsed.path]) order.push(parsed.path);
|
|
275
|
-
byPath[parsed.path] = {
|
|
276
|
-
path: parsed.path,
|
|
277
|
-
rootTitle: typeof parsed.rootTitle === "string" && parsed.rootTitle ? parsed.rootTitle : stemOf(parsed.path),
|
|
278
|
-
content: typeof parsed.content === "string" ? parsed.content : "",
|
|
279
|
-
op: typeof parsed.op === "string" ? parsed.op : name,
|
|
280
|
-
callId: node.callId,
|
|
281
|
-
// 013:rename 迁移后保留旧路径,供本地直读 tab 清理(mergeDocuments)。
|
|
282
|
-
renamedFrom: typeof parsed.renamedFrom === "string" ? parsed.renamedFrom : null,
|
|
283
|
-
};
|
|
284
369
|
}
|
|
285
|
-
|
|
370
|
+
|
|
371
|
+
for (const [nodeIndex, node] of (Array.isArray(nodes) ? nodes : []).entries()) {
|
|
372
|
+
replayNode(node, String(nodeIndex), 0);
|
|
373
|
+
}
|
|
374
|
+
return { order, byPath, latestOpeningPath, latestOpeningEventKey };
|
|
286
375
|
}
|
|
287
376
|
|
|
288
377
|
/**
|
|
@@ -304,7 +393,36 @@ window.__ModuleLoader__.load({
|
|
|
304
393
|
for (const p of Object.keys(localDocs)) {
|
|
305
394
|
if (!snapshot.byPath[p] && !dropped.has(p)) order.push(p);
|
|
306
395
|
}
|
|
307
|
-
return {
|
|
396
|
+
return {
|
|
397
|
+
order,
|
|
398
|
+
byPath,
|
|
399
|
+
latestOpeningPath: snapshot.latestOpeningPath ?? null,
|
|
400
|
+
latestOpeningEventKey: snapshot.latestOpeningEventKey ?? null,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 找到本次快照需要自动展示的脑图路径。
|
|
406
|
+
* seenKeys 为 null 表示首次挂载:恢复历史会话最近一次 create/open;
|
|
407
|
+
* 否则只响应尚未消费的打开事件。
|
|
408
|
+
*/
|
|
409
|
+
function autoOpenTarget(snapshot, seenKeys) {
|
|
410
|
+
if (!snapshot || !snapshot.latestOpeningPath || !snapshot.byPath?.[snapshot.latestOpeningPath]) return null;
|
|
411
|
+
if (seenKeys === null) return snapshot.latestOpeningPath;
|
|
412
|
+
const latest = snapshot.byPath[snapshot.latestOpeningPath];
|
|
413
|
+
if (latest.openingEventKey && !seenKeys.has(latest.openingEventKey)) return snapshot.latestOpeningPath;
|
|
414
|
+
let target = null;
|
|
415
|
+
for (const p of snapshot.order ?? []) {
|
|
416
|
+
const doc = snapshot.byPath[p];
|
|
417
|
+
if (doc?.openingEventKey && !seenKeys.has(doc.openingEventKey)) target = p;
|
|
418
|
+
}
|
|
419
|
+
return target;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function openingEventKeys(snapshot) {
|
|
423
|
+
return new Set((snapshot?.order ?? [])
|
|
424
|
+
.map((p) => snapshot.byPath[p]?.openingEventKey)
|
|
425
|
+
.filter((key) => key != null));
|
|
308
426
|
}
|
|
309
427
|
//#endregion
|
|
310
428
|
|
|
@@ -496,6 +614,25 @@ window.__ModuleLoader__.load({
|
|
|
496
614
|
treeError: { color: "var(--dsw-alias-label-error)", fontSize: "12px", lineHeight: 1.6, margin: "0" },
|
|
497
615
|
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
616
|
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)" },
|
|
617
|
+
// 015 设置面板(settings.section 页面内容)。
|
|
618
|
+
settingsWrap: { display: "flex", flexDirection: "column", gap: "12px", padding: "16px", maxWidth: "480px" },
|
|
619
|
+
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)" },
|
|
620
|
+
settingsGroupTitle: { fontSize: "13px", fontWeight: 600, color: "var(--dsw-alias-label-primary)", margin: "4px 0 0" },
|
|
621
|
+
settingsRow: { display: "flex", alignItems: "center", gap: "12px", fontSize: "13px", color: "var(--dsw-alias-label-primary)" },
|
|
622
|
+
settingsLabel: { flex: "1 1 auto", minWidth: 0, color: "var(--dsw-alias-label-secondary)" },
|
|
623
|
+
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" },
|
|
624
|
+
settingsHint: { color: "var(--dsw-alias-label-tertiary)", fontSize: "12px", lineHeight: 1.6, margin: "0" },
|
|
625
|
+
settingsNotice: { color: "var(--dsw-alias-state-success-primary, #1a7f37)", fontSize: "12px", margin: "0" },
|
|
626
|
+
settingsError: { color: "var(--dsw-alias-label-error)", fontSize: "12px", lineHeight: 1.6, margin: "0" },
|
|
627
|
+
// 分段选择控件(线型/卡片风格)
|
|
628
|
+
segmentRow: { display: "inline-flex", gap: "4px", padding: "3px", borderRadius: "8px", background: "var(--dsw-alias-bg-base)", border: "1px solid var(--dsw-alias-border-l2)" },
|
|
629
|
+
segmentBtn: { border: "none", background: "none", cursor: "pointer", font: "inherit", fontSize: "12px", padding: "3px 12px", borderRadius: "6px", color: "var(--dsw-alias-label-secondary)", lineHeight: "18px" },
|
|
630
|
+
segmentBtnActive: { background: "var(--dsw-alias-bg-layer-3)", color: "var(--dsw-alias-label-primary)", boxShadow: "0 1px 2px rgba(16,24,40,0.08)" },
|
|
631
|
+
// 颜色主题色板
|
|
632
|
+
swatchRow: { display: "flex", gap: "6px", flex: "1 1 auto", justifyContent: "flex-end" },
|
|
633
|
+
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" },
|
|
634
|
+
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)" },
|
|
635
|
+
swatchDot: { width: "10px", height: "10px", borderRadius: "50%", flex: "none" },
|
|
499
636
|
row: { display: "flex", alignItems: "center", minWidth: 0 },
|
|
500
637
|
childrenColumn: { display: "flex", flexDirection: "column", gap: "8px", marginLeft: "40px", minWidth: 0 },
|
|
501
638
|
// 面板树连线层:正交折线(MarkGrove 的 orthogonalPath 风格),
|
|
@@ -508,6 +645,155 @@ window.__ModuleLoader__.load({
|
|
|
508
645
|
codeBox: { fontFamily: "Menlo, monospace", fontSize: "12px" },
|
|
509
646
|
};
|
|
510
647
|
|
|
648
|
+
/** 015 分段选择控件(线型/卡片风格)。 */
|
|
649
|
+
function Segmented(props) {
|
|
650
|
+
const { options, value, onChange, disabled } = props;
|
|
651
|
+
return (0, react_jsx_runtime.jsx)("div", { style: S.segmentRow, children: options.map((opt) => (0, react_jsx_runtime.jsx)("button", {
|
|
652
|
+
key: opt.value,
|
|
653
|
+
type: "button",
|
|
654
|
+
style: value === opt.value ? { ...S.segmentBtn, ...S.segmentBtnActive } : S.segmentBtn,
|
|
655
|
+
disabled,
|
|
656
|
+
onClick: () => onChange(opt.value),
|
|
657
|
+
children: opt.label,
|
|
658
|
+
}, opt.value)) });
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* 015 设置面板(settings.section 页面,root scope):读写 host 的
|
|
663
|
+
* settings namespace "mindmap"。节点主题三件套(线/卡片/颜色)+ 面板宽度;
|
|
664
|
+
* requireApproval 按作者要求隐藏(功能保留,经 config/API 仍可设)。
|
|
665
|
+
*/
|
|
666
|
+
function SettingsPanel(props) {
|
|
667
|
+
const { mindmapFace } = props;
|
|
668
|
+
const [value, setValue] = react.useState(null);
|
|
669
|
+
const [saving, setSaving] = react.useState(false);
|
|
670
|
+
const [notice, setNotice] = react.useState("");
|
|
671
|
+
const [error, setError] = react.useState("");
|
|
672
|
+
|
|
673
|
+
react.useEffect(() => {
|
|
674
|
+
let alive = true;
|
|
675
|
+
(async () => {
|
|
676
|
+
try {
|
|
677
|
+
const v = mindmapFace && typeof mindmapFace.readSettings === "function" ? await mindmapFace.readSettings() : null;
|
|
678
|
+
if (!alive) return;
|
|
679
|
+
if (v === null) setError("设置服务不可用:settings namespace 未注册或 connection 缺失");
|
|
680
|
+
setValue({
|
|
681
|
+
lineStyle: v && v.lineStyle === "curve" ? "curve" : "elbow",
|
|
682
|
+
cardStyle: v && v.cardStyle === "square" ? "square" : "rounded",
|
|
683
|
+
colorTheme: v && COLOR_THEMES[v.colorTheme] ? v.colorTheme : "ocean",
|
|
684
|
+
defaultPanelWidth: v && typeof v.defaultPanelWidth === "number" ? v.defaultPanelWidth : 42,
|
|
685
|
+
});
|
|
686
|
+
} catch (err) {
|
|
687
|
+
if (alive) setError(String(err?.message ?? err));
|
|
688
|
+
}
|
|
689
|
+
})();
|
|
690
|
+
return () => {
|
|
691
|
+
alive = false;
|
|
692
|
+
};
|
|
693
|
+
}, [mindmapFace]);
|
|
694
|
+
|
|
695
|
+
async function save(patch) {
|
|
696
|
+
setSaving(true);
|
|
697
|
+
setError("");
|
|
698
|
+
setNotice("");
|
|
699
|
+
try {
|
|
700
|
+
if (!mindmapFace || typeof mindmapFace.updateSettings !== "function") throw new Error("settings service unavailable");
|
|
701
|
+
await mindmapFace.updateSettings(patch);
|
|
702
|
+
settingsBus.bump(); // 通知脑图面板重读主题(面板常驻,open 不变)
|
|
703
|
+
setNotice("已保存");
|
|
704
|
+
} catch (err) {
|
|
705
|
+
setError(String(err?.message ?? err));
|
|
706
|
+
} finally {
|
|
707
|
+
setSaving(false);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (value === null) {
|
|
712
|
+
return (0, react_jsx_runtime.jsx)("div", { style: S.settingsWrap, children: error
|
|
713
|
+
? (0, react_jsx_runtime.jsx)("p", { style: S.settingsError, children: error })
|
|
714
|
+
: (0, react_jsx_runtime.jsx)("p", { style: S.settingsHint, children: "正在读取设置…" }) });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const setField = (patch) => {
|
|
718
|
+
setValue({ ...value, ...patch });
|
|
719
|
+
save(patch);
|
|
720
|
+
};
|
|
721
|
+
const changeWidth = (e) => {
|
|
722
|
+
const raw = Number(e.target.value);
|
|
723
|
+
const next = Number.isFinite(raw) ? Math.min(80, Math.max(20, Math.round(raw))) : value.defaultPanelWidth;
|
|
724
|
+
setValue({ ...value, defaultPanelWidth: next });
|
|
725
|
+
};
|
|
726
|
+
const commitWidth = () => {
|
|
727
|
+
save({ defaultPanelWidth: value.defaultPanelWidth });
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
return (0, react_jsx_runtime.jsxs)("div", { style: S.settingsWrap, children: [
|
|
731
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.settingsGroupTitle, children: "节点主题" }),
|
|
732
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsGroup, children: [
|
|
733
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsRow, children: [
|
|
734
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.settingsLabel, children: "线" }),
|
|
735
|
+
(0, react_jsx_runtime.jsx)(Segmented, {
|
|
736
|
+
options: [{ value: "elbow", label: "折线" }, { value: "curve", label: "曲线" }],
|
|
737
|
+
value: value.lineStyle,
|
|
738
|
+
disabled: saving,
|
|
739
|
+
onChange: (v) => setField({ lineStyle: v }),
|
|
740
|
+
}),
|
|
741
|
+
] }),
|
|
742
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsRow, children: [
|
|
743
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.settingsLabel, children: "卡片" }),
|
|
744
|
+
(0, react_jsx_runtime.jsx)(Segmented, {
|
|
745
|
+
options: [{ value: "rounded", label: "圆角" }, { value: "square", label: "直角" }],
|
|
746
|
+
value: value.cardStyle,
|
|
747
|
+
disabled: saving,
|
|
748
|
+
onChange: (v) => setField({ cardStyle: v }),
|
|
749
|
+
}),
|
|
750
|
+
] }),
|
|
751
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsRow, children: [
|
|
752
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.settingsLabel, children: "颜色" }),
|
|
753
|
+
(0, react_jsx_runtime.jsx)("div", { style: S.swatchRow, children: [
|
|
754
|
+
{ value: "ocean", label: "海洋蓝" },
|
|
755
|
+
{ value: "sunset", label: "落日橙" },
|
|
756
|
+
{ value: "forest", label: "森林绿" },
|
|
757
|
+
].map((t) => {
|
|
758
|
+
const tokens = colorThemeTokens(t.value);
|
|
759
|
+
return (0, react_jsx_runtime.jsxs)("button", {
|
|
760
|
+
key: t.value,
|
|
761
|
+
type: "button",
|
|
762
|
+
style: value.colorTheme === t.value ? { ...S.swatchBtn, ...S.swatchActive } : S.swatchBtn,
|
|
763
|
+
disabled: saving,
|
|
764
|
+
onClick: () => setField({ colorTheme: t.value }),
|
|
765
|
+
children: [
|
|
766
|
+
(0, react_jsx_runtime.jsx)("span", { style: { ...S.swatchDot, background: tokens.heading } }),
|
|
767
|
+
t.label,
|
|
768
|
+
],
|
|
769
|
+
}, t.value);
|
|
770
|
+
}) }),
|
|
771
|
+
] }),
|
|
772
|
+
] }),
|
|
773
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.settingsHint, children: "主题改动在脑图面板下次打开时生效;背景色始终跟随全局主题。" }),
|
|
774
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.settingsGroupTitle, children: "面板" }),
|
|
775
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsGroup, children: [
|
|
776
|
+
(0, react_jsx_runtime.jsxs)("div", { style: S.settingsRow, children: [
|
|
777
|
+
(0, react_jsx_runtime.jsx)("span", { style: S.settingsLabel, children: "默认宽度(%)" }),
|
|
778
|
+
(0, react_jsx_runtime.jsx)("input", {
|
|
779
|
+
type: "number",
|
|
780
|
+
min: 20,
|
|
781
|
+
max: 80,
|
|
782
|
+
value: value.defaultPanelWidth,
|
|
783
|
+
disabled: saving,
|
|
784
|
+
onChange: changeWidth,
|
|
785
|
+
onBlur: commitWidth,
|
|
786
|
+
style: S.settingsInput,
|
|
787
|
+
}),
|
|
788
|
+
] }),
|
|
789
|
+
(0, react_jsx_runtime.jsx)("p", { style: S.settingsHint, children: "范围 20~80。拖拽面板后的宽度会记住(localStorage);清除本地记忆后回到这里配置的默认值。" }),
|
|
790
|
+
] }),
|
|
791
|
+
saving ? (0, react_jsx_runtime.jsx)("p", { style: S.settingsHint, children: "保存中…" }) : null,
|
|
792
|
+
notice ? (0, react_jsx_runtime.jsx)("p", { style: S.settingsNotice, children: notice }) : null,
|
|
793
|
+
error ? (0, react_jsx_runtime.jsx)("p", { style: S.settingsError, children: error }) : null,
|
|
794
|
+
] });
|
|
795
|
+
}
|
|
796
|
+
|
|
511
797
|
/**
|
|
512
798
|
* 「思维脑图」槽位组件(014):同一槽位渲染 M 按钮 + 悬浮面板宿主层。
|
|
513
799
|
* session scope 的 useSession/sessionId/inputActions 直给,经 props 传给
|
|
@@ -560,16 +846,19 @@ window.__ModuleLoader__.load({
|
|
|
560
846
|
}
|
|
561
847
|
|
|
562
848
|
function NodeBox(props) {
|
|
563
|
-
const { node } = props;
|
|
849
|
+
const { node, theme } = props;
|
|
850
|
+
// 015 节点主题:卡片圆角(圆角/直角)+ 颜色主题令牌(根盒/标题用主题色)。
|
|
851
|
+
const tokens = colorThemeTokens(theme && theme.colorTheme);
|
|
852
|
+
const radius = theme && theme.cardStyle === "square" ? 0 : 10;
|
|
564
853
|
const style = node.kind === "root"
|
|
565
|
-
? { ...S.box, ...S.rootBox }
|
|
854
|
+
? { ...S.box, ...S.rootBox, borderRadius: radius, borderColor: tokens.rootBorder, background: tokens.rootBg }
|
|
566
855
|
: node.kind === "heading"
|
|
567
|
-
? { ...S.box, ...S.headingBox }
|
|
856
|
+
? { ...S.box, ...S.headingBox, borderRadius: radius, color: tokens.heading }
|
|
568
857
|
: node.kind === "placeholder"
|
|
569
|
-
? { ...S.placeholderBox }
|
|
858
|
+
? { ...S.placeholderBox, borderRadius: radius }
|
|
570
859
|
: node.kind === "code"
|
|
571
|
-
? { ...S.box, ...S.codeBox }
|
|
572
|
-
: S.box;
|
|
860
|
+
? { ...S.box, ...S.codeBox, borderRadius: radius }
|
|
861
|
+
: { ...S.box, borderRadius: radius };
|
|
573
862
|
const title = node.data?.description
|
|
574
863
|
? `${node.topic}\n\n${node.data.description}`
|
|
575
864
|
: node.data?.code
|
|
@@ -578,21 +867,23 @@ window.__ModuleLoader__.load({
|
|
|
578
867
|
return (0, react_jsx_runtime.jsx)("div", { style, title, children: node.kind === "placeholder" ? "待填写" : node.topic });
|
|
579
868
|
}
|
|
580
869
|
|
|
581
|
-
/** 左→右递归树:节点盒 + 右侧子节点列 +
|
|
870
|
+
/** 左→右递归树:节点盒 + 右侧子节点列 + 连线层(015 支持折线/曲线两种线型)。 */
|
|
582
871
|
function TreeRow(props) {
|
|
583
|
-
const { node } = props;
|
|
872
|
+
const { node, theme } = props;
|
|
584
873
|
const rowRef = react.useRef(null);
|
|
585
874
|
const boxWrapRef = react.useRef(null);
|
|
586
875
|
const childRefs = react.useRef([]);
|
|
587
876
|
const [edges, setEdges] = react.useState([]);
|
|
588
877
|
const prevEdgesRef = react.useRef("");
|
|
589
878
|
|
|
590
|
-
//
|
|
591
|
-
//
|
|
879
|
+
// 测量父盒右缘与各子节点包裹块的几何位置,画连线
|
|
880
|
+
// (折线 = M x1 y1 H midX V y2 H x2;曲线 = 贝塞尔水平切出);
|
|
881
|
+
// 序列化比对防 setState 循环。
|
|
592
882
|
react.useLayoutEffect(() => {
|
|
593
883
|
const rowEl = rowRef.current;
|
|
594
884
|
const boxEl = boxWrapRef.current;
|
|
595
885
|
if (!rowEl || !boxEl) return;
|
|
886
|
+
const curve = theme && theme.lineStyle === "curve";
|
|
596
887
|
const measure = () => {
|
|
597
888
|
const rowRect = rowEl.getBoundingClientRect();
|
|
598
889
|
const boxRect = boxEl.getBoundingClientRect();
|
|
@@ -605,7 +896,9 @@ window.__ModuleLoader__.load({
|
|
|
605
896
|
const x2 = c.left - rowRect.left;
|
|
606
897
|
const y2 = c.top - rowRect.top + c.height / 2;
|
|
607
898
|
const midX = (x1 + x2) / 2;
|
|
608
|
-
next.push(
|
|
899
|
+
next.push(curve
|
|
900
|
+
? `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`
|
|
901
|
+
: `M ${x1} ${y1} H ${midX} V ${y2} H ${x2}`);
|
|
609
902
|
}
|
|
610
903
|
const key = next.join("|");
|
|
611
904
|
if (prevEdgesRef.current === key) return;
|
|
@@ -639,14 +932,14 @@ window.__ModuleLoader__.load({
|
|
|
639
932
|
}, i)),
|
|
640
933
|
})
|
|
641
934
|
: null,
|
|
642
|
-
(0, react_jsx_runtime.jsx)("div", { ref: boxWrapRef, style: { flex: "0 0 auto" }, children: (0, react_jsx_runtime.jsx)(NodeBox, { node }) }),
|
|
935
|
+
(0, react_jsx_runtime.jsx)("div", { ref: boxWrapRef, style: { flex: "0 0 auto" }, children: (0, react_jsx_runtime.jsx)(NodeBox, { node, theme }) }),
|
|
643
936
|
node.children && node.children.length > 0
|
|
644
937
|
? (0, react_jsx_runtime.jsx)("div", { style: S.childrenColumn, children: node.children.map((child, idx) => (0, react_jsx_runtime.jsx)("div", {
|
|
645
938
|
key: child.id,
|
|
646
939
|
ref: (el) => {
|
|
647
940
|
childRefs.current[idx] = el;
|
|
648
941
|
},
|
|
649
|
-
children: (0, react_jsx_runtime.jsx)(TreeRow, { node: child }),
|
|
942
|
+
children: (0, react_jsx_runtime.jsx)(TreeRow, { node: child, theme }),
|
|
650
943
|
}, child.id)) })
|
|
651
944
|
: null,
|
|
652
945
|
] });
|
|
@@ -678,6 +971,43 @@ window.__ModuleLoader__.load({
|
|
|
678
971
|
setPanelWidth(Math.round(window.innerWidth * 0.8));
|
|
679
972
|
}
|
|
680
973
|
}, []);
|
|
974
|
+
// 015 设置面板:没有本地拖拽记忆时,用 settings 里的默认宽度。
|
|
975
|
+
react.useEffect(() => {
|
|
976
|
+
let hasLocal = false;
|
|
977
|
+
try {
|
|
978
|
+
hasLocal = localStorage.getItem(WIDTH_KEY) !== null;
|
|
979
|
+
} catch {
|
|
980
|
+
// 忽略
|
|
981
|
+
}
|
|
982
|
+
if (hasLocal) return;
|
|
983
|
+
if (!mindmapFace || typeof mindmapFace.readSettings !== "function") return;
|
|
984
|
+
mindmapFace.readSettings().then((v) => {
|
|
985
|
+
const pct = v && typeof v.defaultPanelWidth === "number" ? Math.min(80, Math.max(20, v.defaultPanelWidth)) : 42;
|
|
986
|
+
const px = Math.round(window.innerWidth * pct / 100);
|
|
987
|
+
setPanelWidth((prev) => (Math.abs(prev - px) < 2 ? prev : px));
|
|
988
|
+
}).catch(() => {
|
|
989
|
+
// 读设置失败:保持 42% 默认
|
|
990
|
+
});
|
|
991
|
+
}, [mindmapFace]);
|
|
992
|
+
|
|
993
|
+
// 015 节点主题:面板每次打开、或设置总线 bump(设置页保存)时重读
|
|
994
|
+
// settings——面板常驻不卸载,光靠 open 变化会漏掉「开着面板改设置」。
|
|
995
|
+
const settingsStamp = react.useSyncExternalStore(settingsBus.subscribe, settingsBus.get);
|
|
996
|
+
const [theme, setTheme] = react.useState({ lineStyle: "elbow", cardStyle: "rounded", colorTheme: "ocean" });
|
|
997
|
+
react.useEffect(() => {
|
|
998
|
+
if (!open) return;
|
|
999
|
+
if (!mindmapFace || typeof mindmapFace.readSettings !== "function") return;
|
|
1000
|
+
mindmapFace.readSettings().then((v) => {
|
|
1001
|
+
if (!v) return;
|
|
1002
|
+
setTheme({
|
|
1003
|
+
lineStyle: v.lineStyle === "curve" ? "curve" : "elbow",
|
|
1004
|
+
cardStyle: v.cardStyle === "square" ? "square" : "rounded",
|
|
1005
|
+
colorTheme: COLOR_THEMES[v.colorTheme] ? v.colorTheme : "ocean",
|
|
1006
|
+
});
|
|
1007
|
+
}).catch(() => {
|
|
1008
|
+
// 读设置失败:保持当前主题
|
|
1009
|
+
});
|
|
1010
|
+
}, [open, settingsStamp, mindmapFace]);
|
|
681
1011
|
const dragStateRef = react.useRef(null);
|
|
682
1012
|
function startResize(e) {
|
|
683
1013
|
e.preventDefault();
|
|
@@ -783,26 +1113,17 @@ window.__ModuleLoader__.load({
|
|
|
783
1113
|
[doc && doc.content, doc && doc.rootTitle],
|
|
784
1114
|
);
|
|
785
1115
|
|
|
786
|
-
// AI
|
|
787
|
-
//
|
|
1116
|
+
// AI 自动打开:create/open 代表用户明确的「创建 / 打开 / 查看」意图。
|
|
1117
|
+
// 无论面板当前是否收起,都展开并切到这次意图对应的文档;首次挂载的
|
|
1118
|
+
// 历史快照也照常显示最近一次打开的脑图,避免出现「AI 说已打开但面板没了」。
|
|
788
1119
|
const seen = react.useRef(null);
|
|
789
1120
|
react.useEffect(() => {
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
let shouldOpen = false;
|
|
796
|
-
for (const p of merged.order) {
|
|
797
|
-
const d = merged.byPath[p];
|
|
798
|
-
if (OPENING_OPS.has(d.op) && !seen.current.has(d.callId)) shouldOpen = true;
|
|
799
|
-
}
|
|
800
|
-
seen.current = ids;
|
|
801
|
-
if (shouldOpen) {
|
|
1121
|
+
const targetPath = autoOpenTarget(merged, seen.current);
|
|
1122
|
+
seen.current = openingEventKeys(merged);
|
|
1123
|
+
if (targetPath) {
|
|
802
1124
|
onOpen();
|
|
803
|
-
// 单脑图模式:新打开的脑图顶替旧视图(001 场景 1「一句开脑图」)。
|
|
804
1125
|
setHiddenPath(null);
|
|
805
|
-
setCurrentPath(
|
|
1126
|
+
setCurrentPath(targetPath);
|
|
806
1127
|
setView("mindmap");
|
|
807
1128
|
}
|
|
808
1129
|
}, [merged]);
|
|
@@ -1290,7 +1611,7 @@ window.__ModuleLoader__.load({
|
|
|
1290
1611
|
: (doc && doc.op === "local")
|
|
1291
1612
|
? renderLoading()
|
|
1292
1613
|
: tree
|
|
1293
|
-
? (0, react_jsx_runtime.jsx)(TreeRow, { node: tree })
|
|
1614
|
+
? (0, react_jsx_runtime.jsx)(TreeRow, { node: tree, theme })
|
|
1294
1615
|
: renderTree() }),
|
|
1295
1616
|
tabMenu ? (0, react_jsx_runtime.jsxs)("div", {
|
|
1296
1617
|
style: { ...S.treeMenu, left: tabMenu.x, top: tabMenu.y },
|
|
@@ -1317,16 +1638,17 @@ window.__ModuleLoader__.load({
|
|
|
1317
1638
|
|
|
1318
1639
|
// 014「布局让位」CSS(better-sidebar 同款机制):面板打开时给 #root 挂
|
|
1319
1640
|
// margin-right + 宽度挤压,把聊天区推到左边、面板占右侧腾出的空间,
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1641
|
+
// 互不遮挡。015 修复级联冲突:它家(dsh-better-sidebar)同样注入
|
|
1642
|
+
// #root 规则,后注入者胜导致我们的推挤被压掉——我们的规则加
|
|
1643
|
+
// !important 且把双方变量相加(它开面板时聊天同样让位),无论注入
|
|
1644
|
+
// 顺序如何都稳定生效。若它家未来也用 !important,需再评估(见 docs/014)。
|
|
1323
1645
|
if (typeof document !== "undefined") {
|
|
1324
1646
|
const style = document.createElement("style");
|
|
1325
1647
|
style.setAttribute("data-dsh-mindmap", "layout-push");
|
|
1326
1648
|
style.textContent = [
|
|
1327
1649
|
"#root{",
|
|
1328
|
-
"margin-right:var(--dsh-mindmap-width,0px);",
|
|
1329
|
-
"width:calc(100% - var(--dsh-mindmap-width,0px));",
|
|
1650
|
+
"margin-right:calc(var(--dsh-mindmap-width,0px) + var(--dsh-sidebar-width,0px))!important;",
|
|
1651
|
+
"width:calc(100% - var(--dsh-mindmap-width,0px) - var(--dsh-sidebar-width,0px))!important;",
|
|
1330
1652
|
"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
1653
|
"}",
|
|
1332
1654
|
].join("");
|
|
@@ -1349,6 +1671,35 @@ window.__ModuleLoader__.load({
|
|
|
1349
1671
|
return parsed.value;
|
|
1350
1672
|
};
|
|
1351
1673
|
|
|
1674
|
+
// 015 设置面板:settings namespace(dsh-grafana 同款读写面)。
|
|
1675
|
+
// connection 走 ctx.get 可选查取(动态 ctx 契约);缺失时设置面板降级提示。
|
|
1676
|
+
const connection = ctx.get("connection");
|
|
1677
|
+
const settingsApi = connection && typeof connection.api === "object" ? connection.api : null;
|
|
1678
|
+
face.readSettings = async () => {
|
|
1679
|
+
if (!settingsApi || typeof settingsApi.settings?.describe !== "function") return null;
|
|
1680
|
+
const res = await settingsApi.settings.describe({});
|
|
1681
|
+
const namespaces = res?.result?.value?.namespaces ?? [];
|
|
1682
|
+
const ns = namespaces.find((n) => n?.ns === "mindmap");
|
|
1683
|
+
return ns?.value ?? null;
|
|
1684
|
+
};
|
|
1685
|
+
face.updateSettings = async (patch) => {
|
|
1686
|
+
if (!settingsApi || typeof settingsApi.settings?.update !== "function") {
|
|
1687
|
+
throw new Error("settings service unavailable");
|
|
1688
|
+
}
|
|
1689
|
+
await settingsApi.settings.update({ ns: "mindmap", patch });
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
// 015 设置面板:settings.section(list 槽、root scope)——设置页左栏
|
|
1693
|
+
// 新增「思维脑图」导航项(better-sidebar 同款入口;dsh-grafana 的
|
|
1694
|
+
// settings.plugin.item 卡片是另一条路,未采用)。
|
|
1695
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1696
|
+
name: "settings.section",
|
|
1697
|
+
id: "dsh-mindmap",
|
|
1698
|
+
order: 100,
|
|
1699
|
+
label: "思维脑图",
|
|
1700
|
+
inject: () => ({ mindmapFace: face }),
|
|
1701
|
+
}, SettingsPanel));
|
|
1702
|
+
|
|
1352
1703
|
// 014 overlay 形态(作者拍板,见 docs/014):面板宿主层(position:fixed)
|
|
1353
1704
|
// 与 M 按钮一起渲染在 conversation.session.header.actions 槽位里——
|
|
1354
1705
|
// better-sidebar 同款「fixed 宿主层自举」思路(它的宿主层挂在
|
|
@@ -1369,12 +1720,15 @@ window.__ModuleLoader__.load({
|
|
|
1369
1720
|
parseMarkdownToTree,
|
|
1370
1721
|
reduceDocuments,
|
|
1371
1722
|
mergeDocuments,
|
|
1723
|
+
autoOpenTarget,
|
|
1724
|
+
openingEventKeys,
|
|
1372
1725
|
resultTextOfBlocks,
|
|
1373
1726
|
stemOf,
|
|
1374
1727
|
buildExportSvg,
|
|
1375
1728
|
createIdFactory,
|
|
1376
1729
|
relPathWithin,
|
|
1377
1730
|
visibleTreeRows,
|
|
1731
|
+
colorThemeTokens,
|
|
1378
1732
|
TOOL_NAMES,
|
|
1379
1733
|
OPENING_OPS,
|
|
1380
1734
|
});
|
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
|
|
@@ -39,6 +52,7 @@ Tools:
|
|
|
39
52
|
Markdown mapping (the panel's parser): headings nest by level (H1 are root children, H2 under the previous H1, ...); list items are child nodes nested by 2-space indentation; an EMPTY list item ("- " followed by nothing) renders as a placeholder node — use placeholders for planned-but-unwritten nodes; a fenced code block becomes a leaf node titled "[lang] first line"; plain paragraphs become the note text of the nearest heading.
|
|
40
53
|
|
|
41
54
|
Behavior rules:
|
|
55
|
+
- When the user asks to create a mindmap, call mindmap_create. When the user asks to open, view, show, or switch to an existing mindmap, call mindmap_open (do not use mindmap_get alone). Both operations bring that document to the visible mindmap panel automatically.
|
|
42
56
|
- Always mindmap_get before editing, then send the complete updated document to mindmap_update. One tool call per step so the panel follows along live.
|
|
43
57
|
- Never delete the whole document or restructure it without an explicit user request. Make the smallest change that answers the request.
|
|
44
58
|
- When the user steps away or pauses (e.g. "我去买咖啡"), stop all mindmap edits immediately and wait — never continue autonomously.
|
|
@@ -224,25 +238,38 @@ function defineTool(spec) {
|
|
|
224
238
|
}
|
|
225
239
|
|
|
226
240
|
export function apply(ctx, config = {}) {
|
|
227
|
-
const
|
|
228
|
-
ctx.systemPrompt.section({ name: 'tool:mindmap', order: 106, text: GUIDANCE })
|
|
241
|
+
const entryConfig = { requireApproval: config.requireApproval === true, defaultPanelWidth: 42 }
|
|
229
242
|
|
|
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
|
-
}
|
|
243
|
+
// 015 设置面板:settings 服务可用时以命名空间解析值为准
|
|
244
|
+
// (schema 默认 → 组合层 base → 用户设置层),否则回退入口配置
|
|
245
|
+
// (dsh-grafana 同款模式;link 环境缺 schemastery 时见 003 偏差 1 的
|
|
246
|
+
// 依赖说明——发布包正常安装依赖)。
|
|
247
|
+
let activeConfig = () => entryConfig
|
|
248
|
+
ctx.inject(['settings'], (sctx) => {
|
|
249
|
+
const scope = sctx.settings.register(SETTINGS_NAMESPACE, Config, { base: entryConfig })
|
|
250
|
+
activeConfig = () => scope.get()
|
|
251
|
+
sctx.effect(() => () => {
|
|
252
|
+
activeConfig = () => entryConfig
|
|
244
253
|
})
|
|
245
|
-
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
ctx.systemPrompt.section({ name: 'tool:mindmap', order: 106, text: GUIDANCE })
|
|
257
|
+
|
|
258
|
+
// 后悔药开关(决策 6):钩子常驻注册,运行时读 activeConfig().requireApproval
|
|
259
|
+
// ——设置面板切换立即生效;关闭时直接放行(默认 false 免审批)。
|
|
260
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
261
|
+
const decision = await next()
|
|
262
|
+
if (decision.kind !== 'allow') return decision
|
|
263
|
+
if (!activeConfig().requireApproval) return decision
|
|
264
|
+
if (exec.name !== 'mindmap_update') return decision
|
|
265
|
+
const args = exec.arguments ?? {}
|
|
266
|
+
const renameNote = typeof args.renameRoot === 'string' && args.renameRoot ? `, rename root to "${args.renameRoot}"` : ''
|
|
267
|
+
const bytes = typeof args.content === 'string' ? byteLength(args.content) : 0
|
|
268
|
+
return {
|
|
269
|
+
kind: 'ask',
|
|
270
|
+
reason: `Write mindmap ${JSON.stringify(String(args.path ?? '?'))} (${bytes} bytes${renameNote}). dsh-mindmap is configured with requireApproval.`,
|
|
271
|
+
}
|
|
272
|
+
})
|
|
246
273
|
|
|
247
274
|
ctx.tools.register(defineTool({
|
|
248
275
|
name: 'mindmap_create',
|
package/package.json
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mindmap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/guhanfei-ai/dsh-mindmap.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/guhanfei-ai/dsh-mindmap#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/guhanfei-ai/dsh-mindmap/issues"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
5
16
|
"type": "module",
|
|
6
17
|
"main": "index.js",
|
|
7
18
|
"scripts": {
|
|
8
|
-
"
|
|
19
|
+
"build:client": "node scripts/build-client.mjs",
|
|
20
|
+
"check": "npm run build:client && node --check index.js && node --check client.js && node --check scripts/build-client.mjs && node --check test/index.test.js && node --check test/client.test.js",
|
|
9
21
|
"test": "node --test",
|
|
10
|
-
"verify": "npm run check && npm test"
|
|
22
|
+
"verify": "npm run check && npm test",
|
|
23
|
+
"prepack": "npm run verify"
|
|
11
24
|
},
|
|
12
25
|
"files": [
|
|
13
26
|
"index.js",
|
|
@@ -17,6 +30,9 @@
|
|
|
17
30
|
"README.zh-CN.md",
|
|
18
31
|
"CHANGELOG.md"
|
|
19
32
|
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@deepseek-ai/schemastery": "^3.18.0"
|
|
35
|
+
},
|
|
20
36
|
"dsh": {
|
|
21
37
|
"bundle": {
|
|
22
38
|
"patch": "./cordis.patch.yml"
|