dsh-mindmap 0.2.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 +6 -0
- package/README.md +6 -2
- package/README.zh-CN.md +6 -2
- package/client.js +123 -45
- package/index.js +1 -0
- package/package.json +16 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ 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
|
+
|
|
7
13
|
### Added
|
|
8
14
|
|
|
9
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.
|
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
|
@@ -281,41 +281,97 @@ window.__ModuleLoader__.load({
|
|
|
281
281
|
/**
|
|
282
282
|
* 重放会话快照里的 mindmap_* 工具结果,得到每个脑图文档的最新状态。
|
|
283
283
|
* nodes: ConversationSnapshot.nodes(ToolResultNode 含 call.name 与渲染后的
|
|
284
|
-
* content 文本块——host 的工具结果 JSON 就写在其中)。
|
|
285
|
-
*
|
|
284
|
+
* content 文本块——host 的工具结果 JSON 就写在其中)。Code 等工具的结果
|
|
285
|
+
* 还可能把真实的 mindmap ToolResultNode 放在 subCalls 中,因此这里按事件
|
|
286
|
+
* 顺序递归重放整棵调用树。
|
|
287
|
+
* 返回文档集及最近一次 create/open 意图,用于驱动面板自动切换目标。
|
|
286
288
|
*/
|
|
287
289
|
function reduceDocuments(nodes) {
|
|
288
290
|
const byPath = Object.create(null);
|
|
289
291
|
let order = [];
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
+
}
|
|
299
361
|
}
|
|
300
|
-
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
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);
|
|
306
368
|
}
|
|
307
|
-
if (!byPath[parsed.path]) order.push(parsed.path);
|
|
308
|
-
byPath[parsed.path] = {
|
|
309
|
-
path: parsed.path,
|
|
310
|
-
rootTitle: typeof parsed.rootTitle === "string" && parsed.rootTitle ? parsed.rootTitle : stemOf(parsed.path),
|
|
311
|
-
content: typeof parsed.content === "string" ? parsed.content : "",
|
|
312
|
-
op: typeof parsed.op === "string" ? parsed.op : name,
|
|
313
|
-
callId: node.callId,
|
|
314
|
-
// 013:rename 迁移后保留旧路径,供本地直读 tab 清理(mergeDocuments)。
|
|
315
|
-
renamedFrom: typeof parsed.renamedFrom === "string" ? parsed.renamedFrom : null,
|
|
316
|
-
};
|
|
317
369
|
}
|
|
318
|
-
|
|
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 };
|
|
319
375
|
}
|
|
320
376
|
|
|
321
377
|
/**
|
|
@@ -337,7 +393,36 @@ window.__ModuleLoader__.load({
|
|
|
337
393
|
for (const p of Object.keys(localDocs)) {
|
|
338
394
|
if (!snapshot.byPath[p] && !dropped.has(p)) order.push(p);
|
|
339
395
|
}
|
|
340
|
-
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));
|
|
341
426
|
}
|
|
342
427
|
//#endregion
|
|
343
428
|
|
|
@@ -1028,26 +1113,17 @@ window.__ModuleLoader__.load({
|
|
|
1028
1113
|
[doc && doc.content, doc && doc.rootTitle],
|
|
1029
1114
|
);
|
|
1030
1115
|
|
|
1031
|
-
// AI
|
|
1032
|
-
//
|
|
1116
|
+
// AI 自动打开:create/open 代表用户明确的「创建 / 打开 / 查看」意图。
|
|
1117
|
+
// 无论面板当前是否收起,都展开并切到这次意图对应的文档;首次挂载的
|
|
1118
|
+
// 历史快照也照常显示最近一次打开的脑图,避免出现「AI 说已打开但面板没了」。
|
|
1033
1119
|
const seen = react.useRef(null);
|
|
1034
1120
|
react.useEffect(() => {
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
let shouldOpen = false;
|
|
1041
|
-
for (const p of merged.order) {
|
|
1042
|
-
const d = merged.byPath[p];
|
|
1043
|
-
if (OPENING_OPS.has(d.op) && !seen.current.has(d.callId)) shouldOpen = true;
|
|
1044
|
-
}
|
|
1045
|
-
seen.current = ids;
|
|
1046
|
-
if (shouldOpen) {
|
|
1121
|
+
const targetPath = autoOpenTarget(merged, seen.current);
|
|
1122
|
+
seen.current = openingEventKeys(merged);
|
|
1123
|
+
if (targetPath) {
|
|
1047
1124
|
onOpen();
|
|
1048
|
-
// 单脑图模式:新打开的脑图顶替旧视图(001 场景 1「一句开脑图」)。
|
|
1049
1125
|
setHiddenPath(null);
|
|
1050
|
-
setCurrentPath(
|
|
1126
|
+
setCurrentPath(targetPath);
|
|
1051
1127
|
setView("mindmap");
|
|
1052
1128
|
}
|
|
1053
1129
|
}, [merged]);
|
|
@@ -1644,6 +1720,8 @@ window.__ModuleLoader__.load({
|
|
|
1644
1720
|
parseMarkdownToTree,
|
|
1645
1721
|
reduceDocuments,
|
|
1646
1722
|
mergeDocuments,
|
|
1723
|
+
autoOpenTarget,
|
|
1724
|
+
openingEventKeys,
|
|
1647
1725
|
resultTextOfBlocks,
|
|
1648
1726
|
stemOf,
|
|
1649
1727
|
buildExportSvg,
|
package/index.js
CHANGED
|
@@ -52,6 +52,7 @@ Tools:
|
|
|
52
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.
|
|
53
53
|
|
|
54
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.
|
|
55
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.
|
|
56
57
|
- Never delete the whole document or restructure it without an explicit user request. Make the smallest change that answers the request.
|
|
57
58
|
- When the user steps away or pauses (e.g. "我去买咖啡"), stop all mindmap edits immediately and wait — never continue autonomously.
|
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",
|