dsh-mindmap 0.6.0 → 0.8.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 +11 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/client.js +134 -57
- package/index.js +74 -22
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -23,8 +23,19 @@ All notable changes to this project are documented here. Release-specific notes
|
|
|
23
23
|
|
|
24
24
|
### Fixed
|
|
25
25
|
|
|
26
|
+
- Compatibility with dsh 0.1.2-rc.1 (statically verified, docs/023), while keeping 0.1.1-rc.2 working — all fixes are dual-path with the old path first:
|
|
27
|
+
- **Host tools**: `Agent` no longer carries a live `session` (it is reduced to `{ id }`), so `sessionCwd` now falls back from `exec.agent.session.header.cwd` (dsh ≤0.1.1) to looking the session up via `ctx.sessions.get(exec.agent.id).header.cwd` (0.1.2-rc.1). Without this, every tool failed with "the session has no working directory" on 0.1.2-rc.1.
|
|
28
|
+
- **Live panel data**: the session snapshot no longer carries a flat `nodes` array (conversation content moved to the Chat view), so the panel now reads nodes via `useChat → ChatSnapshot.legacy.nodes` (0.1.2-rc.1+) first and falls back to `useSession → SessionSnapshot.nodes` (dsh ≤0.1.1). `ToolResultNode` field names are unchanged across both, so document replay, auto-open, and error surfacing work as before.
|
|
29
|
+
- **Settings panel**: `settings.describe` over the client connection now returns the descriptor array directly (0.1.2-rc.1+) instead of an aggregate `result.value.namespaces` (dsh ≤0.1.1); both envelope shapes are parsed.
|
|
30
|
+
- **Hygiene**: `dsh.client.inject` additionally lists `@deepseek-ai/dsh-cordis-client-runner` (the 0.1.2-rc.1 browser runtime module id; `@deepseek-ai/dsh-client-runtime` is kept for 0.1.1-rc.2 — a missing id is silently skipped by the loader, so both coexist safely).
|
|
26
31
|
- The panel now reliably auto-opens when the AI completes `mindmap_open` / `mindmap_create`. A structural fingerprint of the session nodes (`nodesFingerprint`) feeds a second `useSession` selector; its value comparison bypasses the reference-equality short-circuit that starved the auto-open effect whenever the host store mutated the nodes array in place.
|
|
27
32
|
- The "AI 正在打开脑图…" loading state is no longer a dead end. Snapshot documents whose path differs from the tree-click key only by letter case (macOS case-insensitive filesystem) now merge automatically; errored mindmap tool results (`isError` or `ok !== true`) surface as an inline error; a ~30s watchdog switches to a timeout state. Both failure states offer a one-click retry that re-sends the open request.
|
|
33
|
+
- A code-review sweep (`_issues/002`) landed 11 fixes, each with a failing regression test first:
|
|
34
|
+
- **Markdown parsing**: an unclosed `---` frontmatter fence at the top no longer swallows the whole document into an empty tree; the root-title echo merge (`firstTopH1Seen`) is set only by a top-level H1 so an earlier H2 or a heading inside a quote block no longer suppresses it; GFM table column count is pinned to the **separator row** (short rows padded, extra cells dropped) instead of the header; fenced code blocks close only on a matching fence type (`~~~` is no longer ended by a ``` line) and `parseTableRow` honors an escaped `\|` preceded by an escaped backslash (`\\|`).
|
|
35
|
+
- **Document snapshots**: `mergeDocuments` only deletes a dropped local entry when no live snapshot owns the same path, so a recreated file that a snapshot also references keeps its panel openable.
|
|
36
|
+
- **Link rendering**: the bare-URL pattern no longer swallows CJK punctuation/full-width characters (`https://a.com,然后…` stops at the comma); `[text](url)` and bare links render as plain text unless the scheme is http/https/mailto (blocking `javascript:`/`data:`), and `openLink` calls `preventDefault` only when `window.open` actually succeeds, so host-blocked popups fall back to default navigation instead of a dead click.
|
|
37
|
+
- **Host safety & approval**: the tree route and all mindmap path resolutions now reject symlink escapes out of the working directory via a `realpath` walk-up (`resolvesInsideBase`); `requireApproval` gates `mindmap_create` as well as `mindmap_update`; and `listDirectoryLevel` stops iterating once it hits the entry cap (perf) while still flagging truncation.
|
|
38
|
+
- **PNG export**: wide tables measure their box height using the same clamped per-column width the renderer uses, so wrapped cell text no longer overflows the measured box bottom.
|
|
28
39
|
|
|
29
40
|
## [0.4.1] - 2026-08-24
|
|
30
41
|
|
package/README.md
CHANGED
|
@@ -23,9 +23,9 @@ A DeepSeek Harness plugin that turns a plain Markdown file into a live mindmap.
|
|
|
23
23
|
- **Single-mindmap mode** — two tabs only: 目录 (tree) and 脑图 (the current mindmap); opening another `.md` replaces the previous one.
|
|
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 as node notes, stable structural IDs, and orthogonal connector lines between nodes.
|
|
26
|
-
- **Centered canvas with zoom** — 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.
|
|
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. 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
27
|
- **PNG export** — one click on 导出图片 exports the current mindmap.
|
|
28
|
-
- **Safety** — `mindmap_update`
|
|
28
|
+
- **Safety** — `mindmap_create` / `mindmap_update` are approval-free by default (files are git-managed) with a `requireApproval` switch as an escape hatch (when on, both write tools ask); the client has **no write path** to the filesystem — every edit goes through the AI tools.
|
|
29
29
|
|
|
30
30
|
## Requirements
|
|
31
31
|
|
package/README.zh-CN.md
CHANGED
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
- **单脑图模式**——面板只有「目录」和「脑图」两个 tab,打开新脑图替换旧的那颗。
|
|
24
24
|
- **「所见即所编」焦点同步**——可见脑图与 AI 工作文档不一致时,面板自动让 AI 打开它,聊天焦点始终跟随你的眼睛。
|
|
25
25
|
- **MarkGrove 同款映射与连线**——标题层级挂树、列表缩进(空项 = 占位节点)、代码块叶节点、段落挂节点说明、稳定结构 ID、节点间直角折线。
|
|
26
|
-
-
|
|
26
|
+
- **画布居中、缩放与平移**——脑图打开后居中呈现(超出画布时可滚动、无边缘裁剪);画布右上角浮动缩放条(缩小 / 比例 / 放大 / 适配),打开时自动适配合适比例(小图保持 100%),25%–300% 逐级缩放且视图中心不跳变,AI 编辑后持续自动再适配,直到你手动缩放。点击任意节点即可聚焦:节点滚到画布左侧居中,视图放大到它和整棵子树完整可见(上限 100%)。画布还能拖着走:**中键**随处拖(含节点上)、**空白处左键拖**(Mac 触控板「点按并拖移」)、或 **Space + 左键拖**(必须从节点上起手时);内容跟手 1:1,空白处呈抓手光标,4px 阈值把拖与点分开——点空白仍取消选中、点节点仍聚焦,真拖不会误清选中圈。
|
|
27
27
|
- **PNG 导出**——面板右上角「导出图片」一键导出当前脑图。
|
|
28
|
-
- **安全**——`mindmap_update` 默认免原生审批(文件受 git 管控),留 `requireApproval`
|
|
28
|
+
- **安全**——`mindmap_create`/`mindmap_update` 默认免原生审批(文件受 git 管控),留 `requireApproval` 开关作后悔药(开启后两个写工具都走审批);客户端**没有任何写文件通道**,一切编辑都经 AI 工具。
|
|
29
29
|
|
|
30
30
|
## 环境要求
|
|
31
31
|
|
package/client.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
// - 脑图面板:014 起注册在 shell.overlay(list 槽、root scope、点击穿透层),
|
|
8
8
|
// 右缘贴边全高悬浮、左缘拖拽调宽(280~80% 视口,localStorage 持久化);
|
|
9
9
|
// details 槽已归还官方(原生「工具详情」栏恢复,003 的顶替方案退役)。
|
|
10
|
-
// -
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// - 实时数据通路:消费会话快照里 mindmap_* 工具的 ToolResultNode,重放出各文档
|
|
11
|
+
// 的最新内容并渲染(002/003:无自定义事件通道,工具调用本身就是事件流)。
|
|
12
|
+
// 023 双代:useChat → ChatSnapshot.legacy.nodes(dsh 0.1.2-rc.1+)优先,
|
|
13
|
+
// useSession → SessionSnapshot.nodes(dsh ≤0.1.1)兜底。
|
|
13
14
|
// - markdown→脑图树:本文件内置零依赖解析器(MarkGrove mdastConverter 的映射
|
|
14
15
|
// 语法移植:标题栈→树、列表→子节点、空列表项=占位节点、代码块→首行摘要叶
|
|
15
16
|
// 节点、段落→挂标题的正文说明、结构路径稳定 ID)。
|
|
@@ -287,11 +288,12 @@ window.__ModuleLoader__.load({
|
|
|
287
288
|
}
|
|
288
289
|
|
|
289
290
|
/** 表格行 → 单元格数组(去首尾空段,保留中间空单元格)。
|
|
290
|
-
* 支持 GFM 转义:`\|`
|
|
291
|
+
* 支持 GFM 转义:`\|` 是字面竖线(占位符避位,剥标记后还原),不切单元格;
|
|
292
|
+
* `\\|` 则是已转义的反斜杠 + 真分隔符,照常切开。 */
|
|
291
293
|
function parseTableRow(line) {
|
|
292
294
|
return String(line ?? "").trim()
|
|
293
295
|
.replace(/^\|/, "").replace(/\|$/, "")
|
|
294
|
-
.replace(
|
|
296
|
+
.replace(/(?<!\\)((?:\\\\)*)\\\|/g, "$1\u0000")
|
|
295
297
|
.split("|")
|
|
296
298
|
.map((c) => c.replace(/\u0000/g, "|").trim());
|
|
297
299
|
}
|
|
@@ -333,13 +335,16 @@ window.__ModuleLoader__.load({
|
|
|
333
335
|
data: {},
|
|
334
336
|
};
|
|
335
337
|
// 根标题回声标记:记录文档常以文件名作首行 H1(如 "# 002-spike结论.md"),
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
338
|
+
// 而根节点标题就是文件名——首个顶层 H1 与根标题一致(或仅多 .md 后缀)
|
|
339
|
+
// 时并入根节点,避免标题显示两次。只有顶层 H1 参与回声:H2 等低级标题
|
|
340
|
+
// 不消耗名额,引用块递归(echoRoot=false)也不触碰本标记。
|
|
341
|
+
let firstTopH1Seen = false;
|
|
339
342
|
const lines = String(markdown ?? "").split(/\r?\n/);
|
|
340
343
|
|
|
341
344
|
let start = 0;
|
|
342
|
-
// 跳过 YAML frontmatter(--- ...
|
|
345
|
+
// 跳过 YAML frontmatter(--- ... ---):找不到闭合行说明不是
|
|
346
|
+
// frontmatter(如以水平线开头的合法文档),回退普通解析,
|
|
347
|
+
// 否则整篇会被吞成空树。
|
|
343
348
|
if (lines.length > 0 && /^\s*---\s*$/.test(lines[0])) {
|
|
344
349
|
for (start = 1; start < lines.length; start++) {
|
|
345
350
|
if (/^\s*---\s*$/.test(lines[start])) {
|
|
@@ -347,6 +352,7 @@ window.__ModuleLoader__.load({
|
|
|
347
352
|
break;
|
|
348
353
|
}
|
|
349
354
|
}
|
|
355
|
+
if (start >= lines.length) start = 0;
|
|
350
356
|
}
|
|
351
357
|
|
|
352
358
|
/**
|
|
@@ -393,12 +399,16 @@ window.__ModuleLoader__.load({
|
|
|
393
399
|
|
|
394
400
|
// 围栏代码块:整块成为一个叶节点,标题 = [语言] 首行摘要(盒内紧凑,
|
|
395
401
|
// 悬停浮层看全文——003 §5.3;data.code 全量保存)。
|
|
396
|
-
|
|
402
|
+
// GFM:闭合围栏须同字符且不少于开启长度——``` 块里的 ~~~、
|
|
403
|
+
// ```` 块里的 ``` 都是代码内容,不是围栏。
|
|
404
|
+
const fence = /^\s*(`{3,}|~{3,})/.exec(line);
|
|
405
|
+
if (fence) {
|
|
397
406
|
flushParagraph();
|
|
398
407
|
listStack = [];
|
|
399
|
-
const lang = line.trim().slice(
|
|
408
|
+
const lang = line.trim().slice(fence[1].length).trim();
|
|
409
|
+
const fenceClose = new RegExp(`^\\s*${fence[1][0]}{${fence[1].length},}\\s*$`);
|
|
400
410
|
const buf = [];
|
|
401
|
-
for (i += 1; i < lineList.length &&
|
|
411
|
+
for (i += 1; i < lineList.length && !fenceClose.test(lineList[i]); i++) buf.push(lineList[i]);
|
|
402
412
|
const code = buf.join("\n");
|
|
403
413
|
const firstLine = (code.split("\n")[0] || "").trim();
|
|
404
414
|
const summary = firstLine.length > 40 ? `${firstLine.slice(0, 40)}…` : firstLine;
|
|
@@ -419,12 +429,15 @@ window.__ModuleLoader__.load({
|
|
|
419
429
|
listStack = [];
|
|
420
430
|
const level = heading[1].length;
|
|
421
431
|
const text = heading[2].trim() || "(无标题)";
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
432
|
+
// 首个顶层 H1 与根标题一致(或仅多 .md 后缀)→ 并入根节点,不另建节点。
|
|
433
|
+
// 名额只属于顶层 H1:低级标题先行、引用块内出现标题都不影响回声。
|
|
434
|
+
if (echoRoot && level === 1) {
|
|
435
|
+
if (!firstTopH1Seen && (text === root.topic || text === `${root.topic}.md`)) {
|
|
436
|
+
firstTopH1Seen = true;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
firstTopH1Seen = true;
|
|
426
440
|
}
|
|
427
|
-
firstHeadingSeen = true;
|
|
428
441
|
while (headingStack.length > 0 && headingStack[headingStack.length - 1].level >= level) headingStack.pop();
|
|
429
442
|
const basePath = parentRec() ? parentRec().path : "";
|
|
430
443
|
const node = {
|
|
@@ -475,19 +488,20 @@ window.__ModuleLoader__.load({
|
|
|
475
488
|
flushParagraph();
|
|
476
489
|
listStack = [];
|
|
477
490
|
i = j - 1;
|
|
478
|
-
|
|
479
|
-
//
|
|
480
|
-
|
|
491
|
+
// GFM 对齐契约:列数钉死在分隔行。表头与数据行同等待遇:
|
|
492
|
+
// 少列补空、多列截断——未转义竖线切碎的行顶多内容错位,网格永不参差。
|
|
493
|
+
const cols = parseTableRow(rows[1]).length;
|
|
481
494
|
const toCols = (cells) => {
|
|
482
|
-
if (cells.length >=
|
|
483
|
-
return cells.concat(new Array(
|
|
495
|
+
if (cells.length >= cols) return cells.slice(0, cols);
|
|
496
|
+
return cells.concat(new Array(cols - cells.length).fill(""));
|
|
484
497
|
};
|
|
498
|
+
const header = toCols(parseTableRow(rows[0]));
|
|
485
499
|
const body = rows.slice(2).map((row) => toCols(parseTableRow(row)));
|
|
486
500
|
const tableRows = [header].concat(body);
|
|
487
501
|
appendNode({
|
|
488
502
|
id: idOf("table", tableRows.map((r) => r.join("\u0001")).join("\u0002"), parentPathOf()),
|
|
489
503
|
kind: "table",
|
|
490
|
-
topic: `${tableRows.length}×${
|
|
504
|
+
topic: `${tableRows.length}×${cols} 表格`,
|
|
491
505
|
children: [],
|
|
492
506
|
data: { rows: tableRows },
|
|
493
507
|
});
|
|
@@ -807,7 +821,9 @@ window.__ModuleLoader__.load({
|
|
|
807
821
|
const lower = p.toLowerCase();
|
|
808
822
|
if (snapPaths.some((sp) => sp.toLowerCase() === lower)) dropped.add(p);
|
|
809
823
|
}
|
|
810
|
-
|
|
824
|
+
// 只删本地条目;若该路径同时是存活快照文档
|
|
825
|
+
// (改名后又重建),快照保留,面板照常打开。
|
|
826
|
+
for (const p of dropped) if (!snapByPath[p]) delete byPath[p];
|
|
811
827
|
const order = [...(snapshot?.order ?? [])];
|
|
812
828
|
for (const p of Object.keys(localDocs)) {
|
|
813
829
|
if (!snapByPath[p] && !dropped.has(p)) order.push(p);
|
|
@@ -997,14 +1013,16 @@ window.__ModuleLoader__.load({
|
|
|
997
1013
|
return { text: stripInlineForExport(node.topic), lines: null };
|
|
998
1014
|
}
|
|
999
1015
|
|
|
1000
|
-
/** 019 盒尺寸估算:文本按折行行数生长;表格按行列数算网格尺寸。
|
|
1016
|
+
/** 019 盒尺寸估算:文本按折行行数生长;表格按行列数算网格尺寸。
|
|
1017
|
+
* 022:测量与渲染必须共用钳制后的列宽——先算盒宽再按 w/cols 折行,
|
|
1018
|
+
* 否则宽表(钳到 tableMaxW)按 110px 估行、渲染按更窄列宽折行,盒高不足。 */
|
|
1001
1019
|
function measureExportBox(node) {
|
|
1002
1020
|
if (node.kind === "table") {
|
|
1003
1021
|
const rows = (node.data && node.data.rows) || [];
|
|
1004
1022
|
const cols = rows.reduce((mx, row) => Math.max(mx, row.length), 0) || 1;
|
|
1005
|
-
const cellInner = EXPORT.tableCellW - EXPORT.tableCellPad * 2;
|
|
1006
|
-
const rowLines = rows.map((row) => row.reduce((mx, cell) => Math.max(mx, wrapExportText(stripInlineForExport(cell), cellInner, EXPORT.fontSize - 1).length), 1));
|
|
1007
1023
|
const w = Math.min(EXPORT.tableMaxW, Math.max(EXPORT.tableMinW, cols * EXPORT.tableCellW));
|
|
1024
|
+
const cellInner = w / cols - EXPORT.tableCellPad * 2;
|
|
1025
|
+
const rowLines = rows.map((row) => row.reduce((mx, cell) => Math.max(mx, wrapExportText(stripInlineForExport(cell), cellInner, EXPORT.fontSize - 1).length), 1));
|
|
1008
1026
|
const h = Math.max(EXPORT.lineHeight, rowLines.reduce((a, b) => a + b, 0) * EXPORT.lineHeight);
|
|
1009
1027
|
return { w, h };
|
|
1010
1028
|
}
|
|
@@ -1481,20 +1499,35 @@ window.__ModuleLoader__.load({
|
|
|
1481
1499
|
] });
|
|
1482
1500
|
}
|
|
1483
1501
|
|
|
1502
|
+
/**
|
|
1503
|
+
* 会话内容节点的双代快照选择(023):dsh ≤0.1.1 的 useSession 快照带
|
|
1504
|
+
* 平铺 nodes;0.1.2-rc.1 起 SessionSnapshot 拆成纯控制状态,会话内容
|
|
1505
|
+
* 迁入 useChat 的 ChatSnapshot.legacy.nodes(官方兼容面,ToolResultNode
|
|
1506
|
+
* 字段同名)。legacy 优先、旧 nodes 兜底,两代通吃。
|
|
1507
|
+
*/
|
|
1508
|
+
function conversationNodesOf(s) {
|
|
1509
|
+
if (!s) return EMPTY_NODES;
|
|
1510
|
+
const legacy = s.legacy;
|
|
1511
|
+
if (legacy && Array.isArray(legacy.nodes)) return legacy.nodes;
|
|
1512
|
+
return Array.isArray(s.nodes) ? s.nodes : EMPTY_NODES;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1484
1515
|
/**
|
|
1485
1516
|
* 「思维脑图」槽位组件(014):同一槽位渲染 M 按钮 + 悬浮面板宿主层。
|
|
1486
1517
|
* session scope 的 useSession/sessionId/inputActions 直给,经 props 传给
|
|
1487
1518
|
* MindmapDetailsPanel(无桥、无 useSyncExternalStore——shell.overlay 跨槽
|
|
1488
|
-
* 方案实测未渲染,弃用后顺手把桥也删了)。
|
|
1519
|
+
* 方案实测未渲染,弃用后顺手把桥也删了)。023:内容钩子改为
|
|
1520
|
+
* useChat(0.1.2-rc.1+)优先、useSession(≤0.1.1)兜底。
|
|
1489
1521
|
*/
|
|
1490
1522
|
function MindmapSlot(props) {
|
|
1491
|
-
const { useSession, sessionId, inputActions, mindmapFace } = props;
|
|
1492
|
-
const
|
|
1523
|
+
const { useSession, useChat, sessionId, inputActions, mindmapFace } = props;
|
|
1524
|
+
const nodesHook = useChat ?? useSession;
|
|
1525
|
+
const nodes = nodesHook ? nodesHook(conversationNodesOf) : EMPTY_NODES;
|
|
1493
1526
|
// 016 可靠性加固:结构指纹作第二 selector。store 原地改数组(引用
|
|
1494
1527
|
// 不变)时,nodes prop 不换、memo 命中缓存、auto-open effect 永不
|
|
1495
1528
|
// 重跑——「AI 打开了脑图但面板不展开」的根因。指纹是原始值字符串,
|
|
1496
|
-
//
|
|
1497
|
-
const nodesVersion =
|
|
1529
|
+
// 值比较天然绕过引用相等短路;内容钩子不可用时回退空串。
|
|
1530
|
+
const nodesVersion = nodesHook ? nodesHook((s) => nodesFingerprint(conversationNodesOf(s))) : "";
|
|
1498
1531
|
const [open, setOpen] = react.useState(false);
|
|
1499
1532
|
return (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
|
|
1500
1533
|
(0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -1541,18 +1574,25 @@ window.__ModuleLoader__.load({
|
|
|
1541
1574
|
//#region 019 血肉渲染:行内格式 + 大一统链接 + 表格块(规范源:003)
|
|
1542
1575
|
// 行内格式统一扫描序:图片/链接 → 行内代码 → 粗体 → 删除线 → 斜体 → 裸链接。
|
|
1543
1576
|
// 先命中先生效,裸链接放最后,避免吞掉已被 [文字](url) 消费的 URL。
|
|
1544
|
-
|
|
1577
|
+
// 裸链接字符类排除 CJK 标点与全角符号(,。、;()……),
|
|
1578
|
+
// 否则中文句读被吞进 URL;ASCII 括号放行,由配平裁剪兜底。
|
|
1579
|
+
const INLINE_PATTERN = /(!?\[[^\]]*\]\([^)]*\))|(`[^`]+`)|(\*\*[^*]+\*\*)|(~~[^~]+~~)|(\*[^*\s][^*]*\*)|(https?:\/\/[^\s\u3000-\u303f\uff00-\uffef]+)/g;
|
|
1545
1580
|
|
|
1546
1581
|
/** 大一统链接点击:在机器浏览器打开(新标签页),不触发画布聚焦缩放。 */
|
|
1547
1582
|
function openLink(event, url) {
|
|
1548
|
-
event.preventDefault();
|
|
1549
1583
|
event.stopPropagation();
|
|
1584
|
+
// 只在 window.open 成功后 preventDefault:宿主拦截(返回 null 或
|
|
1585
|
+
// 抛错)时不拦,锚点自带的 target=_blank 原生导航接管——链接永远可达。
|
|
1586
|
+
// (旧版 catch 里给只读属性 defaultPrevented 赋值是死代码,拦了默认
|
|
1587
|
+
// 行为又开不了窗,链接彻底点不开。)
|
|
1588
|
+
let opened = null;
|
|
1550
1589
|
try {
|
|
1551
|
-
window.open(url, "_blank", "noopener");
|
|
1590
|
+
opened = window.open(url, "_blank", "noopener");
|
|
1552
1591
|
} catch {
|
|
1553
|
-
|
|
1554
|
-
event.defaultPrevented = false;
|
|
1592
|
+
opened = null;
|
|
1555
1593
|
}
|
|
1594
|
+
if (!opened) return;
|
|
1595
|
+
event.preventDefault();
|
|
1556
1596
|
}
|
|
1557
1597
|
|
|
1558
1598
|
/**
|
|
@@ -1575,20 +1615,26 @@ window.__ModuleLoader__.load({
|
|
|
1575
1615
|
// [文字](url) 或 。图片块暂缓(003 §9):图语法退化为
|
|
1576
1616
|
// 指向原图的链接,同时把 alt 与原图地址都完整呈现(不缩减)。
|
|
1577
1617
|
const parsed = /^(!?)\[([^\]]*)\]\(([^)]*)\)$/.exec(token);
|
|
1578
|
-
//
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1618
|
+
// scheme 白名单:只放行 http/https/mailto。javascript:/data:
|
|
1619
|
+
// 等不进 href,整串原样退化为纯文本(不缩减,也不可执行)。
|
|
1620
|
+
if (!/^\s*(https?:|mailto:)/i.test(parsed[3])) {
|
|
1621
|
+
out.push(token);
|
|
1622
|
+
} else {
|
|
1623
|
+
// 普通链接标签取文字(无文字显地址);图语法带 alt 时两者都完整呈现。
|
|
1624
|
+
const label = parsed[1]
|
|
1625
|
+
? (parsed[2] ? `${parsed[2]} (${parsed[3]})` : parsed[3])
|
|
1626
|
+
: (parsed[2] || parsed[3]);
|
|
1627
|
+
out.push((0, react_jsx_runtime.jsx)("a", {
|
|
1628
|
+
key,
|
|
1629
|
+
href: parsed[3],
|
|
1630
|
+
target: "_blank",
|
|
1631
|
+
rel: "noopener noreferrer",
|
|
1632
|
+
style: S.inlineLink,
|
|
1633
|
+
title: parsed[3],
|
|
1634
|
+
onClick: (e) => openLink(e, parsed[3]),
|
|
1635
|
+
children: label,
|
|
1636
|
+
}, key));
|
|
1637
|
+
}
|
|
1592
1638
|
} else if (m[2]) {
|
|
1593
1639
|
out.push((0, react_jsx_runtime.jsx)("code", { key, style: S.inlineCode, children: token.slice(1, -1) }, key));
|
|
1594
1640
|
} else if (m[3]) {
|
|
@@ -1598,17 +1644,30 @@ window.__ModuleLoader__.load({
|
|
|
1598
1644
|
} else if (m[5]) {
|
|
1599
1645
|
out.push((0, react_jsx_runtime.jsx)("em", { key, children: token.slice(1, -1) }, key));
|
|
1600
1646
|
} else {
|
|
1601
|
-
//
|
|
1647
|
+
// 裸链接:完整显示、可点击。维基式配平括号属于 URL;未配平的
|
|
1648
|
+
// 尾 ) 退回正文当纯文本——href 干净,可见文本不丢字符。
|
|
1649
|
+
let url = token;
|
|
1650
|
+
let opens = 0;
|
|
1651
|
+
let closes = 0;
|
|
1652
|
+
for (const ch of url) {
|
|
1653
|
+
if (ch === "(") opens += 1;
|
|
1654
|
+
else if (ch === ")") closes += 1;
|
|
1655
|
+
}
|
|
1656
|
+
while (closes > opens && url.endsWith(")")) {
|
|
1657
|
+
url = url.slice(0, -1);
|
|
1658
|
+
closes -= 1;
|
|
1659
|
+
}
|
|
1602
1660
|
out.push((0, react_jsx_runtime.jsx)("a", {
|
|
1603
1661
|
key,
|
|
1604
|
-
href:
|
|
1662
|
+
href: url,
|
|
1605
1663
|
target: "_blank",
|
|
1606
1664
|
rel: "noopener noreferrer",
|
|
1607
1665
|
style: S.inlineLink,
|
|
1608
|
-
title:
|
|
1609
|
-
onClick: (e) => openLink(e,
|
|
1610
|
-
children:
|
|
1666
|
+
title: url,
|
|
1667
|
+
onClick: (e) => openLink(e, url),
|
|
1668
|
+
children: url,
|
|
1611
1669
|
}, key));
|
|
1670
|
+
if (url.length < token.length) out.push(token.slice(url.length));
|
|
1612
1671
|
}
|
|
1613
1672
|
last = m.index + token.length;
|
|
1614
1673
|
}
|
|
@@ -3227,6 +3286,19 @@ window.__ModuleLoader__.load({
|
|
|
3227
3286
|
}
|
|
3228
3287
|
//#endregion
|
|
3229
3288
|
|
|
3289
|
+
/**
|
|
3290
|
+
* settings describe 应答的双代信封解析(023):dsh ≤0.1.1 的远端把
|
|
3291
|
+
* 描述符聚合在 result.value.namespaces[];0.1.2-rc.1 起直接返回描述符
|
|
3292
|
+
* 数组(每项 {ns, schema, value, …},字段两代同名)。数组优先、
|
|
3293
|
+
* namespaces 兜底,两代通吃。
|
|
3294
|
+
*/
|
|
3295
|
+
function settingsNamespacesOf(res) {
|
|
3296
|
+
const value = res?.result?.value;
|
|
3297
|
+
if (Array.isArray(value)) return value;
|
|
3298
|
+
const list = value?.namespaces;
|
|
3299
|
+
return Array.isArray(list) ? list : [];
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3230
3302
|
function apply(ctx) {
|
|
3231
3303
|
const face = {};
|
|
3232
3304
|
|
|
@@ -3288,7 +3360,7 @@ window.__ModuleLoader__.load({
|
|
|
3288
3360
|
face.readSettings = async () => {
|
|
3289
3361
|
if (!settingsApi || typeof settingsApi.settings?.describe !== "function") return null;
|
|
3290
3362
|
const res = await settingsApi.settings.describe({});
|
|
3291
|
-
const namespaces = res
|
|
3363
|
+
const namespaces = settingsNamespacesOf(res);
|
|
3292
3364
|
const ns = namespaces.find((n) => n?.ns === "mindmap");
|
|
3293
3365
|
return ns?.value ?? null;
|
|
3294
3366
|
};
|
|
@@ -3352,6 +3424,8 @@ window.__ModuleLoader__.load({
|
|
|
3352
3424
|
parseTableRow,
|
|
3353
3425
|
nodeFullText,
|
|
3354
3426
|
renderInline,
|
|
3427
|
+
// 链接点击:供测试验证开窗成功才拦默认行为(宿主拦截时退回原生导航)。
|
|
3428
|
+
openLink,
|
|
3355
3429
|
stripInlineForExport,
|
|
3356
3430
|
wrapExportText,
|
|
3357
3431
|
COLOR_THEMES,
|
|
@@ -3368,6 +3442,9 @@ window.__ModuleLoader__.load({
|
|
|
3368
3442
|
isActivatable,
|
|
3369
3443
|
TOOL_NAMES,
|
|
3370
3444
|
OPENING_OPS,
|
|
3445
|
+
// 023 双代兼容纯函数(供测试):会话内容节点 / settings 信封。
|
|
3446
|
+
conversationNodesOf,
|
|
3447
|
+
settingsNamespacesOf,
|
|
3371
3448
|
// 021 画布组件:仅供测试驱动平移手势(不参与运行时契约)。
|
|
3372
3449
|
MindmapCanvas,
|
|
3373
3450
|
});
|
package/index.js
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
// - 四工具都带 path/name 参数(决策 3:多脑图并存,作用于指定那颗)。
|
|
10
10
|
// - 结果 JSON {ok, op, path, rootTitle, content, renamedFrom?}:content 全文
|
|
11
11
|
// 供模型续编辑,client 用同一份重放面板(工具结果即实时通道,002 第二节)。
|
|
12
|
-
// - requireApproval 配置(决策 6):默认 false 免审批;置 true 时
|
|
13
|
-
// 走原生 ask(tools/pre-execute,照 dsh-grafana 的钩子模式)。
|
|
14
|
-
// namespace 可在设置面板运行时切换(见 SETTINGS_NAMESPACE/Config)。
|
|
12
|
+
// - requireApproval 配置(决策 6):默认 false 免审批;置 true 时 mindmap_create
|
|
13
|
+
// 与 mindmap_update 走原生 ask(tools/pre-execute,照 dsh-grafana 的钩子模式)。
|
|
14
|
+
// 015 起经 settings namespace 可在设置面板运行时切换(见 SETTINGS_NAMESPACE/Config)。
|
|
15
15
|
// - 依赖:仅 @deepseek-ai/schemastery(settings schema;发布包正常解析,
|
|
16
16
|
// link 开发需先 npm i)。工具参数 schema 仍手写 JSON Schema(003 偏差 1)。
|
|
17
|
-
import { access, opendir, readFile, rename, stat, writeFile } from 'node:fs/promises'
|
|
17
|
+
import { access, opendir, readFile, realpath, rename, stat, writeFile } from 'node:fs/promises'
|
|
18
18
|
import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from 'node:path'
|
|
19
19
|
import Schema from '@deepseek-ai/schemastery'
|
|
20
20
|
|
|
@@ -26,7 +26,7 @@ export const inject = ['tools', 'systemPrompt', 'webServer', 'sessions']
|
|
|
26
26
|
// defaultPanelWidth 供客户端面板取默认宽度(20-80 钳制由客户端执行)。
|
|
27
27
|
export const SETTINGS_NAMESPACE = 'mindmap'
|
|
28
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.'),
|
|
29
|
+
requireApproval: Schema.boolean().default(false).description('Require native DSH approval for every mindmap_create and mindmap_update (including renameRoot). Files are git-managed, so it defaults to off. Hidden from the settings panel; still honored at runtime.'),
|
|
30
30
|
defaultPanelWidth: Schema.number().default(42).description('Default floating-panel width as a percentage of the viewport (clamped 20-80 on the client).'),
|
|
31
31
|
lineStyle: Schema.union(['curve', 'elbow']).default('elbow').description('Connector line style between nodes: curve (bezier) or elbow (orthogonal).'),
|
|
32
32
|
cardStyle: Schema.union(['rounded', 'square']).default('rounded').description('Node card corner style.'),
|
|
@@ -65,9 +65,21 @@ function textOut(value) {
|
|
|
65
65
|
return [{ type: 'text', text: String(value) }]
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
/**
|
|
69
|
+
* 会话工作目录:工具执行的 agent → session → header.cwd(dsh-session 契约)。
|
|
70
|
+
* 023 双路径:dsh ≤0.1.1 的 Agent 直挂 live session(agent.session.header.cwd);
|
|
71
|
+
* 0.1.2-rc.1 起 Agent 只剩 { id },改经 sessions 服务按 id 查 header.cwd
|
|
72
|
+
* (SessionStore.get / SessionHeader.cwd 两代同名)。旧链优先,新链兜底。
|
|
73
|
+
*/
|
|
74
|
+
function sessionCwd(exec, sessions) {
|
|
75
|
+
const direct = exec?.agent?.session?.header?.cwd
|
|
76
|
+
if (direct) return direct
|
|
77
|
+
const id = exec?.agent?.id
|
|
78
|
+
if (id && sessions?.get) {
|
|
79
|
+
const cwd = sessions.get(id)?.header?.cwd
|
|
80
|
+
if (cwd) return cwd
|
|
81
|
+
}
|
|
82
|
+
return undefined
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
//#region 013 目录树 API(host 自建只读 HTTP 路由;dsh-better-sidebar 同款机制)
|
|
@@ -113,15 +125,44 @@ function escapesBase(rel) {
|
|
|
113
125
|
return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)
|
|
114
126
|
}
|
|
115
127
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
128
|
+
/**
|
|
129
|
+
* realpath 包含性校验(#5):字符串规范化只挡字面 `..`,cwd 内指向外部
|
|
130
|
+
* 的符号链接能骗过它。尾部不存在的段(待建文件)向上走最近的存在祖先
|
|
131
|
+
* 逐个 realpath——符号链接只能藏在已存在的段里。解析后仍在 base 内返回
|
|
132
|
+
* true;base 自身不存在或越界返回 false。
|
|
133
|
+
*/
|
|
134
|
+
async function resolvesInsideBase(resolved, base) {
|
|
135
|
+
let realBase
|
|
136
|
+
try {
|
|
137
|
+
realBase = await realpath(base)
|
|
138
|
+
} catch {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
let probe = resolved
|
|
142
|
+
for (;;) {
|
|
143
|
+
try {
|
|
144
|
+
const real = await realpath(probe)
|
|
145
|
+
if (real === realBase) return true
|
|
146
|
+
return !escapesBase(relative(realBase, real))
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error
|
|
149
|
+
const parent = dirname(probe)
|
|
150
|
+
if (parent === probe) return false
|
|
151
|
+
probe = parent
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 请求路径校验:缺省 = 根 cwd;显式路径必须绝对、落在 cwd 内,
|
|
157
|
+
* 且解析符号链接后仍在内(#5)。 */
|
|
158
|
+
async function resolveTreePath(cwd, input) {
|
|
118
159
|
if (!cwd) throw httpError(400, 'no-cwd', 'session has no working directory')
|
|
119
160
|
if (input === undefined || input === null || String(input).trim() === '') return cwd
|
|
120
161
|
const p = String(input).trim()
|
|
121
162
|
if (!isAbsolute(p)) throw httpError(400, 'bad-request', `path must be absolute: ${JSON.stringify(p)}`)
|
|
122
163
|
const resolved = resolvePath(p)
|
|
123
164
|
const rel = relative(cwd, resolved)
|
|
124
|
-
if (escapesBase(rel)) {
|
|
165
|
+
if (escapesBase(rel) || !(await resolvesInsideBase(resolved, cwd))) {
|
|
125
166
|
throw httpError(400, 'bad-request', `path must stay inside the session working directory (${cwd})`)
|
|
126
167
|
}
|
|
127
168
|
return resolved
|
|
@@ -140,8 +181,10 @@ async function listDirectoryLevel(path, maxEntries = MAX_TREE_ENTRIES) {
|
|
|
140
181
|
try {
|
|
141
182
|
for await (const dirent of dir) {
|
|
142
183
|
if (rows.length >= maxEntries) {
|
|
143
|
-
|
|
144
|
-
|
|
184
|
+
// 022:到达上限即停(旧实现 continue 会把巨型目录整个遍历一遍)。
|
|
185
|
+
// truncated 只取布尔语义,无需精确计数剩余条目。
|
|
186
|
+
overflow = 1
|
|
187
|
+
break
|
|
145
188
|
}
|
|
146
189
|
rows.push({
|
|
147
190
|
name: dirent.name,
|
|
@@ -204,7 +247,7 @@ function sanitizeStem(input) {
|
|
|
204
247
|
* 且必须以 .md 结尾。cwd 缺失时仅接受绝对路径。
|
|
205
248
|
* @returns 绝对规范化路径。
|
|
206
249
|
*/
|
|
207
|
-
function resolveMindmapPath(cwd, input) {
|
|
250
|
+
async function resolveMindmapPath(cwd, input) {
|
|
208
251
|
if (typeof input !== 'string' || !input.trim()) throw new Error('path is required.')
|
|
209
252
|
const p = input.trim()
|
|
210
253
|
if (!/\.md$/i.test(p)) throw new Error(`mindmap path must end with .md: ${JSON.stringify(p)}.`)
|
|
@@ -214,7 +257,8 @@ function resolveMindmapPath(cwd, input) {
|
|
|
214
257
|
}
|
|
215
258
|
const resolved = resolvePath(cwd, p)
|
|
216
259
|
const rel = relative(cwd, resolved)
|
|
217
|
-
|
|
260
|
+
// realpath 兜底(#5):写路径经符号链接越狱是安全敏感操作。
|
|
261
|
+
if (rel === '' || escapesBase(rel) || !(await resolvesInsideBase(resolved, cwd))) {
|
|
218
262
|
throw new Error(`mindmap path must stay inside the session working directory (${cwd}).`)
|
|
219
263
|
}
|
|
220
264
|
return resolved
|
|
@@ -288,10 +332,18 @@ export function apply(ctx, config = {}) {
|
|
|
288
332
|
|
|
289
333
|
// 后悔药开关(决策 6):钩子常驻注册,运行时读 activeConfig().requireApproval
|
|
290
334
|
// ——设置面板切换立即生效;关闭时直接放行(默认 false 免审批)。
|
|
335
|
+
// 022:create 同为写路径,一并纳入审批(CONTRIBUTING:所有写路径安全敏感)。
|
|
291
336
|
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
292
337
|
const decision = await next()
|
|
293
338
|
if (decision.kind !== 'allow') return decision
|
|
294
339
|
if (!activeConfig().requireApproval) return decision
|
|
340
|
+
if (exec.name === 'mindmap_create') {
|
|
341
|
+
const args = exec.arguments ?? {}
|
|
342
|
+
return {
|
|
343
|
+
kind: 'ask',
|
|
344
|
+
reason: `Create mindmap ${JSON.stringify(String(args.name ?? '?'))}. dsh-mindmap is configured with requireApproval.`,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
295
347
|
if (exec.name !== 'mindmap_update') return decision
|
|
296
348
|
const args = exec.arguments ?? {}
|
|
297
349
|
const renameNote = typeof args.renameRoot === 'string' && args.renameRoot ? `, rename root to "${args.renameRoot}"` : ''
|
|
@@ -315,10 +367,10 @@ export function apply(ctx, config = {}) {
|
|
|
315
367
|
output: { schema: { type: 'string' }, render: (_args, value) => textOut(value) },
|
|
316
368
|
timeoutMs: TOOL_TIMEOUT_MS,
|
|
317
369
|
async execute(args, exec) {
|
|
318
|
-
const cwd = sessionCwd(exec)
|
|
370
|
+
const cwd = sessionCwd(exec, ctx.sessions)
|
|
319
371
|
if (!cwd) throw new Error('The session has no working directory; cannot create a mindmap.')
|
|
320
372
|
const stem = sanitizeStem(args?.name)
|
|
321
|
-
const path = resolveMindmapPath(cwd, `${stem}.md`)
|
|
373
|
+
const path = await resolveMindmapPath(cwd, `${stem}.md`)
|
|
322
374
|
// wx = 不存在才创建:原子拒绝已存在(含并发竞态)与同名目录,无 TOCTOU 窗口。
|
|
323
375
|
try {
|
|
324
376
|
await writeFile(path, '', { encoding: 'utf8', flag: 'wx' })
|
|
@@ -345,7 +397,7 @@ export function apply(ctx, config = {}) {
|
|
|
345
397
|
output: { schema: { type: 'string' }, render: (_args, value) => textOut(value) },
|
|
346
398
|
timeoutMs: TOOL_TIMEOUT_MS,
|
|
347
399
|
async execute(args, exec) {
|
|
348
|
-
const path = resolveMindmapPath(sessionCwd(exec), args?.path)
|
|
400
|
+
const path = await resolveMindmapPath(sessionCwd(exec, ctx.sessions), args?.path)
|
|
349
401
|
const content = await readFile(path, 'utf8')
|
|
350
402
|
return buildResult('open', path, { content })
|
|
351
403
|
},
|
|
@@ -364,7 +416,7 @@ export function apply(ctx, config = {}) {
|
|
|
364
416
|
output: { schema: { type: 'string' }, render: (_args, value) => textOut(value) },
|
|
365
417
|
timeoutMs: TOOL_TIMEOUT_MS,
|
|
366
418
|
async execute(args, exec) {
|
|
367
|
-
const path = resolveMindmapPath(sessionCwd(exec), args?.path)
|
|
419
|
+
const path = await resolveMindmapPath(sessionCwd(exec, ctx.sessions), args?.path)
|
|
368
420
|
const content = await readFile(path, 'utf8')
|
|
369
421
|
return buildResult('get', path, { content })
|
|
370
422
|
},
|
|
@@ -385,8 +437,8 @@ export function apply(ctx, config = {}) {
|
|
|
385
437
|
output: { schema: { type: 'string' }, render: (_args, value) => textOut(value) },
|
|
386
438
|
timeoutMs: TOOL_TIMEOUT_MS,
|
|
387
439
|
async execute(args, exec) {
|
|
388
|
-
const cwd = sessionCwd(exec)
|
|
389
|
-
const path = resolveMindmapPath(cwd, args?.path)
|
|
440
|
+
const cwd = sessionCwd(exec, ctx.sessions)
|
|
441
|
+
const path = await resolveMindmapPath(cwd, args?.path)
|
|
390
442
|
const hasContent = typeof args?.content === 'string'
|
|
391
443
|
if (!hasContent && typeof args?.renameRoot !== 'string') {
|
|
392
444
|
throw new Error('mindmap_update requires content (or renameRoot alone for a pure rename).')
|
|
@@ -453,7 +505,7 @@ export function apply(ctx, config = {}) {
|
|
|
453
505
|
sendJson(res, 400, { ok: false, error: { code: 'no-cwd', message: 'session has no working directory' } })
|
|
454
506
|
return
|
|
455
507
|
}
|
|
456
|
-
const dir = resolveTreePath(cwd, payload.path)
|
|
508
|
+
const dir = await resolveTreePath(cwd, payload.path)
|
|
457
509
|
const listing = await listDirectoryLevel(dir)
|
|
458
510
|
sendJson(res, 200, { ok: true, value: { ...listing, cwd } })
|
|
459
511
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mindmap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"client": {
|
|
43
43
|
"inject": [
|
|
44
44
|
"@deepseek-ai/dsh-client-runtime",
|
|
45
|
+
"@deepseek-ai/dsh-cordis-client-runner",
|
|
45
46
|
"@deepseek-ai/dsh-client-ui-layout"
|
|
46
47
|
],
|
|
47
48
|
"platform": "web",
|