mellos-mapping 0.17.0 → 0.19.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/README.md +21 -3
- package/README.zh-CN.md +17 -2
- package/dist/server.mjs +90 -2
- package/dist/watch.mjs +106 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,9 +130,12 @@ To watch the live pane beside a Codex session on Windows, run
|
|
|
130
130
|
terminal window hosting the session (or falls back to a dedicated
|
|
131
131
|
"mellos-mapping" window; `--window` picks that on purpose). Add
|
|
132
132
|
`--page <slug>` to open on a particular page — and with a pane already open,
|
|
133
|
-
rerunning with `--page` retargets it instead of opening another.
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
rerunning with `--page` retargets it instead of opening another. The pane
|
|
134
|
+
auto-follows the page being written — the map the agent is operating on
|
|
135
|
+
right now; press `f` to toggle that (a manual page switch also turns it
|
|
136
|
+
off), or start with `--no-follow`. Elsewhere run
|
|
137
|
+
`node <plugin root>/dist/watch.mjs` (same `--page` / `--no-follow` flags)
|
|
138
|
+
from the project directory in a second terminal (or any terminal split).
|
|
136
139
|
|
|
137
140
|
## Any MCP client
|
|
138
141
|
|
|
@@ -262,6 +265,21 @@ When a hidden sub-map changes in the background, the footer says so.
|
|
|
262
265
|
| `mmap_update` | Record progress: `planned → in-progress → done` (+evidence), `regressed`, group/lane membership, node kind |
|
|
263
266
|
| `mmap_remove` | Revise: drop edges, nodes, groups, lanes, empty bands |
|
|
264
267
|
| `mmap_view` | Render the current map as text inline (optional `zoom`) |
|
|
268
|
+
| `mmap_setup` | Get/set the project's mapping policy — when maps open |
|
|
269
|
+
|
|
270
|
+
### Setup: choose when maps open
|
|
271
|
+
|
|
272
|
+
Each project chooses how eager mapping is, once, via `/mmap setup` (or the
|
|
273
|
+
first time the assistant declares a map — the reply nudges it to ask you):
|
|
274
|
+
|
|
275
|
+
- `always` — map every structured task: workflows, designs, architecture,
|
|
276
|
+
technical dependencies.
|
|
277
|
+
- `complex` — map only medium or complex tasks (the default until configured).
|
|
278
|
+
- `on-request` — map only when you explicitly ask.
|
|
279
|
+
|
|
280
|
+
The choice is stored in `.claude/mellos-mapping.config.json` and guides the
|
|
281
|
+
assistant; it never blocks the tools, and asking for a map explicitly always
|
|
282
|
+
works under any policy.
|
|
265
283
|
|
|
266
284
|
Structural invariants enforced by the tools: layers form a total order by
|
|
267
285
|
rank; every node lives in exactly one layer; edges point **strictly
|
package/README.zh-CN.md
CHANGED
|
@@ -120,8 +120,10 @@ node ~/.codex/plugins/cache/mellos-mapping/mellos-mapping/<版本>/scripts/codex
|
|
|
120
120
|
终端窗口里分屏(识别不到就确定性地开到专属的 "mellos-mapping" 窗口;
|
|
121
121
|
`--window` 则是主动选择专属窗口)。加 `--page <slug>` 指定打开哪一页;
|
|
122
122
|
面板已经开着时,带 `--page` 重跑一次不会再开新面板,而是让现有面板
|
|
123
|
-
|
|
124
|
-
`
|
|
123
|
+
切到那一页。面板默认**自动跟随**正在被写入的页——AI 此刻操作哪张图,
|
|
124
|
+
就看哪张图;按 `f` 开关(手动切页也会关掉),或用 `--no-follow` 启动。
|
|
125
|
+
其他环境在项目目录下的第二个终端(或任意分屏)运行
|
|
126
|
+
`node <插件根>/dist/watch.mjs`(同样支持 `--page` / `--no-follow`)。
|
|
125
127
|
|
|
126
128
|
## 任意 MCP 客户端
|
|
127
129
|
|
|
@@ -240,6 +242,19 @@ npx -y -p mellos-mapping mellos-mapping-watch
|
|
|
240
242
|
| `mmap_update` | 记录进度:`planned → in-progress → done`(附证据)、`regressed`、分组/泳道归属、节点 kind |
|
|
241
243
|
| `mmap_remove` | 修订:删除边、节点、分组、泳道、空层 |
|
|
242
244
|
| `mmap_view` | 把当前地图渲染成文本,直接在对话里看(可选 `zoom` 参数) |
|
|
245
|
+
| `mmap_setup` | 查/设本项目的建图策略——什么时候开地图 |
|
|
246
|
+
|
|
247
|
+
### Setup:选择什么时候建图
|
|
248
|
+
|
|
249
|
+
每个项目选一次建图的积极程度,用 `/mmap setup`(或 AI 第一次 declare 时,
|
|
250
|
+
工具响应会引导它来问你):
|
|
251
|
+
|
|
252
|
+
- `always` —— 任何有结构的任务都建图:流程、设计、架构、技术依赖。
|
|
253
|
+
- `complex` —— 只在中等或复杂任务时建图(未配置时的默认行为)。
|
|
254
|
+
- `on-request` —— 只在你明确要求时建图。
|
|
255
|
+
|
|
256
|
+
选择保存在 `.claude/mellos-mapping.config.json`,用于引导 AI;它从不阻止
|
|
257
|
+
工具本身——无论什么策略,明确要求建图永远有效。
|
|
243
258
|
|
|
244
259
|
工具强制的结构不变量:层按 rank 构成全序;每个节点恰好属于一层;边**严格
|
|
245
260
|
向下**——因此图从构造上就是无环的;节点不能依赖同层兄弟(如果 A 需要
|
package/dist/server.mjs
CHANGED
|
@@ -22141,6 +22141,57 @@ var PAGES_DIR_NAME = "mellos-mapping.pages";
|
|
|
22141
22141
|
function pageFilePath(defaultFile, page) {
|
|
22142
22142
|
return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
|
|
22143
22143
|
}
|
|
22144
|
+
var CONFIG_FILE_NAME = "mellos-mapping.config.json";
|
|
22145
|
+
var CONFIG_FILE_VERSION = 1;
|
|
22146
|
+
function configFilePath(defaultFile) {
|
|
22147
|
+
return join(dirname(defaultFile), CONFIG_FILE_NAME);
|
|
22148
|
+
}
|
|
22149
|
+
var MAPPING_POLICIES = ["always", "complex", "on-request"];
|
|
22150
|
+
function makeMappingPolicy(raw) {
|
|
22151
|
+
return MAPPING_POLICIES.includes(raw) ? ok(raw) : err({ kind: "invalid-policy", raw, allowed: MAPPING_POLICIES });
|
|
22152
|
+
}
|
|
22153
|
+
function describeMappingPolicy(policy) {
|
|
22154
|
+
switch (policy) {
|
|
22155
|
+
case "always":
|
|
22156
|
+
return "map every structured task \u2014 workflows, designs, architecture, technical dependencies";
|
|
22157
|
+
case "complex":
|
|
22158
|
+
return "map only medium or complex tasks \u2014 several modules, a new subsystem, roughly an hour or more";
|
|
22159
|
+
case "on-request":
|
|
22160
|
+
return "map only when the user explicitly asks";
|
|
22161
|
+
}
|
|
22162
|
+
}
|
|
22163
|
+
function loadMappingPolicy(defaultFile) {
|
|
22164
|
+
const path = configFilePath(defaultFile);
|
|
22165
|
+
let text2;
|
|
22166
|
+
try {
|
|
22167
|
+
text2 = readFileSync(path, "utf8");
|
|
22168
|
+
} catch (e) {
|
|
22169
|
+
if (e.code === "ENOENT") return ok(void 0);
|
|
22170
|
+
throw e;
|
|
22171
|
+
}
|
|
22172
|
+
let raw;
|
|
22173
|
+
try {
|
|
22174
|
+
raw = JSON.parse(text2);
|
|
22175
|
+
} catch (e) {
|
|
22176
|
+
return err({ kind: "malformed-json", path, detail: e.message });
|
|
22177
|
+
}
|
|
22178
|
+
if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
|
|
22179
|
+
if (raw["version"] !== CONFIG_FILE_VERSION) {
|
|
22180
|
+
return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${CONFIG_FILE_VERSION}` });
|
|
22181
|
+
}
|
|
22182
|
+
const rawPolicy = raw["policy"];
|
|
22183
|
+
if (rawPolicy === void 0) return ok(void 0);
|
|
22184
|
+
if (typeof rawPolicy !== "string") return err({ kind: "bad-shape", path, detail: "policy is not a string" });
|
|
22185
|
+
const policy = makeMappingPolicy(rawPolicy);
|
|
22186
|
+
return policy.ok ? ok(policy.value) : err({ kind: "bad-shape", path, detail: `policy is "${rawPolicy}", expected one of: ${MAPPING_POLICIES.join(" | ")}` });
|
|
22187
|
+
}
|
|
22188
|
+
function saveMappingPolicy(defaultFile, policy) {
|
|
22189
|
+
const path = configFilePath(defaultFile);
|
|
22190
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
22191
|
+
const tmp = path + ".tmp";
|
|
22192
|
+
writeFileSync(tmp, JSON.stringify({ version: CONFIG_FILE_VERSION, policy }, null, 2) + "\n", "utf8");
|
|
22193
|
+
renameSync(tmp, path);
|
|
22194
|
+
}
|
|
22144
22195
|
function describeStoreError(e) {
|
|
22145
22196
|
switch (e.kind) {
|
|
22146
22197
|
case "not-found":
|
|
@@ -22515,7 +22566,7 @@ function summarize(map) {
|
|
|
22515
22566
|
|
|
22516
22567
|
// src/server/server.ts
|
|
22517
22568
|
var SERVER_NAME = "mellos-mapping";
|
|
22518
|
-
var SERVER_VERSION = "0.
|
|
22569
|
+
var SERVER_VERSION = "0.19.0";
|
|
22519
22570
|
var ID = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "lowercase letters, digits and dashes, 1-64 chars").describe("stable kebab-case identifier");
|
|
22520
22571
|
var PAGE = ID.optional().describe(
|
|
22521
22572
|
"page (parallel map) this call targets; omit for the default page. One effort = one page: start a NEW effort on its own page named after the effort, so concurrent sessions never write over each other and the pane can switch between pages."
|
|
@@ -22552,6 +22603,13 @@ function buildServer(stateFile) {
|
|
|
22552
22603
|
saveMapFile(file, applied.value);
|
|
22553
22604
|
return text(summarize(applied.value) + (page !== void 0 ? ` [page: ${page}]` : ""));
|
|
22554
22605
|
};
|
|
22606
|
+
const setupNudge = () => {
|
|
22607
|
+
const policy = loadMappingPolicy(stateFile);
|
|
22608
|
+
if (!policy.ok) return `
|
|
22609
|
+
note: ${describeStoreError(policy.error)} \u2014 fix it or rerun setup (mmap_setup).`;
|
|
22610
|
+
if (policy.value !== void 0) return "";
|
|
22611
|
+
return "\nnote: mapping policy not set for this project. Ask the user when maps should open \u2014 " + MAPPING_POLICIES.map((p) => `${p} (${describeMappingPolicy(p)})`).join("; ") + " \u2014 then record the answer with mmap_setup.";
|
|
22612
|
+
};
|
|
22555
22613
|
server.registerTool(
|
|
22556
22614
|
"mmap_declare",
|
|
22557
22615
|
{
|
|
@@ -22599,7 +22657,12 @@ function buildServer(stateFile) {
|
|
|
22599
22657
|
edges: external_exports.array(EDGE.extend({ label: external_exports.string().max(80).optional().describe("what flows along the edge") })).optional()
|
|
22600
22658
|
}
|
|
22601
22659
|
},
|
|
22602
|
-
(input) =>
|
|
22660
|
+
(input) => {
|
|
22661
|
+
const result = mutate(input.page, (map) => applyDeclare(map, input));
|
|
22662
|
+
if (result.isError === true) return result;
|
|
22663
|
+
const nudge = setupNudge();
|
|
22664
|
+
return nudge === "" ? result : text((result.content[0]?.text ?? "") + nudge);
|
|
22665
|
+
}
|
|
22603
22666
|
);
|
|
22604
22667
|
server.registerTool(
|
|
22605
22668
|
"mmap_update",
|
|
@@ -22641,6 +22704,31 @@ function buildServer(stateFile) {
|
|
|
22641
22704
|
},
|
|
22642
22705
|
(input) => mutate(input.page, (map) => applyRemove(map, input))
|
|
22643
22706
|
);
|
|
22707
|
+
server.registerTool(
|
|
22708
|
+
"mmap_setup",
|
|
22709
|
+
{
|
|
22710
|
+
title: "Configure when maps open",
|
|
22711
|
+
description: `Get or set this project's mapping policy \u2014 WHEN the assistant opens a Mellos map. Call with no arguments to read it. If it reports "not set", ask the USER to choose (never pick for them): always = ` + describeMappingPolicy("always") + "; complex = " + describeMappingPolicy("complex") + "; on-request = " + describeMappingPolicy("on-request") + ". Then call again with their choice to persist it. The policy guides you; it never blocks the tools, and an explicit user request for a map always wins.",
|
|
22712
|
+
inputSchema: {
|
|
22713
|
+
policy: external_exports.enum(MAPPING_POLICIES).optional().describe("the user's choice to persist; omit to read the current policy")
|
|
22714
|
+
}
|
|
22715
|
+
},
|
|
22716
|
+
(input) => {
|
|
22717
|
+
if (input.policy !== void 0) {
|
|
22718
|
+
const policy = input.policy;
|
|
22719
|
+
saveMappingPolicy(stateFile, policy);
|
|
22720
|
+
return text(`mapping policy set: ${policy} \u2014 ${describeMappingPolicy(policy)} [${configFilePath(stateFile)}]`);
|
|
22721
|
+
}
|
|
22722
|
+
const loaded = loadMappingPolicy(stateFile);
|
|
22723
|
+
if (!loaded.ok) return text(describeStoreError(loaded.error), true);
|
|
22724
|
+
if (loaded.value === void 0) {
|
|
22725
|
+
return text(
|
|
22726
|
+
"mapping policy not set. Ask the user to choose one of: " + MAPPING_POLICIES.map((p) => `${p} (${describeMappingPolicy(p)})`).join("; ") + " \u2014 then call mmap_setup with their choice. Until then act as complex."
|
|
22727
|
+
);
|
|
22728
|
+
}
|
|
22729
|
+
return text(`mapping policy: ${loaded.value} \u2014 ${describeMappingPolicy(loaded.value)}`);
|
|
22730
|
+
}
|
|
22731
|
+
);
|
|
22644
22732
|
server.registerTool(
|
|
22645
22733
|
"mmap_view",
|
|
22646
22734
|
{
|
package/dist/watch.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module'; const require = createRequire(impor
|
|
|
3
3
|
|
|
4
4
|
// src/watch/watch.ts
|
|
5
5
|
import { realpathSync, statSync } from "node:fs";
|
|
6
|
-
import { join as join2 } from "node:path";
|
|
6
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
8
|
|
|
9
9
|
// src/domain/types.ts
|
|
@@ -1259,6 +1259,7 @@ function parseInput(chunk) {
|
|
|
1259
1259
|
else if (ch === "-") events.push({ kind: "zoom", delta: -1 });
|
|
1260
1260
|
else if (ch === " ") events.push({ kind: "next-page" });
|
|
1261
1261
|
else if (ch === "\x7F" || ch === "\b") events.push({ kind: "back" });
|
|
1262
|
+
else if (ch === "f" || ch === "F") events.push({ kind: "follow-toggle" });
|
|
1262
1263
|
else if (ch >= "1" && ch <= "9") events.push({ kind: "page", index: ch.charCodeAt(0) - "1".charCodeAt(0) });
|
|
1263
1264
|
else if (KEY_PAN[ch]) events.push({ kind: "pan", ...KEY_PAN[ch] });
|
|
1264
1265
|
i += 1;
|
|
@@ -1274,6 +1275,7 @@ function parseArgs(argv, cwd) {
|
|
|
1274
1275
|
let color = true;
|
|
1275
1276
|
let mouse = true;
|
|
1276
1277
|
let page;
|
|
1278
|
+
let follow = true;
|
|
1277
1279
|
for (let i = 0; i < argv.length; i++) {
|
|
1278
1280
|
switch (argv[i]) {
|
|
1279
1281
|
case "--file":
|
|
@@ -1296,11 +1298,26 @@ function parseArgs(argv, cwd) {
|
|
|
1296
1298
|
case "--no-mouse":
|
|
1297
1299
|
mouse = false;
|
|
1298
1300
|
break;
|
|
1301
|
+
case "--no-follow":
|
|
1302
|
+
follow = false;
|
|
1303
|
+
break;
|
|
1299
1304
|
default:
|
|
1300
1305
|
break;
|
|
1301
1306
|
}
|
|
1302
1307
|
}
|
|
1303
|
-
return { file, intervalMs, unicode, color, mouse, page };
|
|
1308
|
+
return { file, intervalMs, unicode, color, mouse, page, follow };
|
|
1309
|
+
}
|
|
1310
|
+
function dividerRow(width, unicode, follow) {
|
|
1311
|
+
const grip = unicode ? " \u22EF " : " ~ ";
|
|
1312
|
+
let bar = (unicode ? "\u2500" : "-").repeat(width);
|
|
1313
|
+
const gripAt = Math.max(0, Math.floor((width - grip.length) / 2));
|
|
1314
|
+
if (width > grip.length + 2) bar = bar.slice(0, gripAt) + grip + bar.slice(gripAt + grip.length);
|
|
1315
|
+
if (follow) {
|
|
1316
|
+
const tag = unicode ? " \u21E2 follow " : " > follow ";
|
|
1317
|
+
const at = width - tag.length - 1;
|
|
1318
|
+
if (at > gripAt + grip.length) bar = bar.slice(0, at) + tag + bar.slice(at + tag.length);
|
|
1319
|
+
}
|
|
1320
|
+
return bar;
|
|
1304
1321
|
}
|
|
1305
1322
|
function mostRecentPageFile(files, mtimeOf) {
|
|
1306
1323
|
let best;
|
|
@@ -1518,19 +1535,8 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
|
|
|
1518
1535
|
}
|
|
1519
1536
|
return lines.slice(0, rows);
|
|
1520
1537
|
}
|
|
1521
|
-
var
|
|
1522
|
-
|
|
1523
|
-
E: ["####", "#", "###", "#", "####"],
|
|
1524
|
-
L: ["#", "#", "#", "#", "####"],
|
|
1525
|
-
O: [" ###", "# #", "# #", "# #", " ###"],
|
|
1526
|
-
S: [" ####", "#", " ###", " #", "####"],
|
|
1527
|
-
A: [" ###", "# #", "#####", "# #", "# #"],
|
|
1528
|
-
P: ["####", "# #", "####", "#", "#"],
|
|
1529
|
-
I: ["###", " #", " #", " #", "###"],
|
|
1530
|
-
N: ["# #", "## #", "# # #", "# ##", "# #"],
|
|
1531
|
-
G: [" ####", "#", "# ##", "# #", " ###"]
|
|
1532
|
-
};
|
|
1533
|
-
var SPLASH_ROWS = 5;
|
|
1538
|
+
var WATER_ROWS = 7;
|
|
1539
|
+
var WATER_COLS_MAX = 60;
|
|
1534
1540
|
var SPLASH_SHADES = {
|
|
1535
1541
|
unicode: ["\u2591", "\u2591", "\u2592", "\u2592", "\u2593", "\u2593", "\u2588", "\u2588"],
|
|
1536
1542
|
ascii: [".", ".", ":", ":", "=", "=", "#", "#"]
|
|
@@ -1540,23 +1546,24 @@ var SPINNER_FRAMES = {
|
|
|
1540
1546
|
unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
|
|
1541
1547
|
ascii: ["|", "/", "-", "\\"]
|
|
1542
1548
|
};
|
|
1543
|
-
function
|
|
1544
|
-
const
|
|
1545
|
-
const
|
|
1546
|
-
const
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
});
|
|
1559
|
-
|
|
1549
|
+
function elapsedLabel(ms) {
|
|
1550
|
+
const s = Math.max(0, Math.floor(ms / 1e3));
|
|
1551
|
+
const m = Math.floor(s / 60);
|
|
1552
|
+
const h = Math.floor(m / 60);
|
|
1553
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
1554
|
+
return h > 0 ? `${h}:${two(m % 60)}:${two(s % 60)}` : `${m}:${two(s % 60)}`;
|
|
1555
|
+
}
|
|
1556
|
+
function waitingInfo(s, width) {
|
|
1557
|
+
const w = Math.max(1, width);
|
|
1558
|
+
const clock = s.elapsedMs !== void 0 ? ` \xB7 waiting ${elapsedLabel(s.elapsedMs)}` : "";
|
|
1559
|
+
const lines = [
|
|
1560
|
+
`watching ${s.defaultFile}`,
|
|
1561
|
+
` and ${join2(s.pagesDir, "*.json")}`,
|
|
1562
|
+
`polling every ${s.intervalMs} ms${clock}`
|
|
1563
|
+
];
|
|
1564
|
+
for (const b of s.broken) lines.push(`! ${b}`);
|
|
1565
|
+
lines.push("the map appears at the first mmap_declare");
|
|
1566
|
+
return lines.map((l) => fitWidth(l, w));
|
|
1560
1567
|
}
|
|
1561
1568
|
var WAVE_INTERVAL = 18;
|
|
1562
1569
|
var WAVE_LIFETIME = 64;
|
|
@@ -1607,39 +1614,41 @@ function waveAt(ripples, x, y) {
|
|
|
1607
1614
|
function waveLevel(value) {
|
|
1608
1615
|
return Math.max(-WAVE_LEVELS, Math.min(WAVE_LEVELS, Math.round(value * WAVE_GAIN)));
|
|
1609
1616
|
}
|
|
1610
|
-
function splashFrame(notice, frame, width, height, unicode, color) {
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
if (width < artWidth + 2 || height < art.length + 2) return void 0;
|
|
1617
|
+
function splashFrame(notice, info, frame, width, height, unicode, color) {
|
|
1618
|
+
const fieldW = Math.min(width - 4, WATER_COLS_MAX);
|
|
1619
|
+
if (fieldW < 24 || height < WATER_ROWS + info.length + 3) return void 0;
|
|
1614
1620
|
const mode = unicode ? "unicode" : "ascii";
|
|
1615
1621
|
const shades = SPLASH_SHADES[mode];
|
|
1616
|
-
const
|
|
1617
|
-
const
|
|
1618
|
-
const
|
|
1619
|
-
|
|
1620
|
-
const level = waveLevel(waveAt(ripples, x, y));
|
|
1621
|
-
return color ? `38;5;${WAVE_RAMP[WAVE_LEVELS + level]}` : shades[Math.abs(level)];
|
|
1622
|
-
};
|
|
1623
|
-
const paintRow = (row, y) => {
|
|
1624
|
-
const cells = [...row].map((ch, x) => ch === "#" ? inkOf(x, y) : void 0);
|
|
1622
|
+
const indent = " ".repeat(Math.max(0, Math.floor((width - fieldW) / 2)));
|
|
1623
|
+
const ripples = liveRipples(frame, fieldW, WATER_ROWS);
|
|
1624
|
+
const paintRow = (y) => {
|
|
1625
|
+
const levels = Array.from({ length: fieldW }, (_, x) => waveLevel(waveAt(ripples, x, y)));
|
|
1625
1626
|
let out = "";
|
|
1626
|
-
for (let i = 0; i <
|
|
1627
|
-
const
|
|
1627
|
+
for (let i = 0; i < fieldW; ) {
|
|
1628
|
+
const level = levels[i];
|
|
1628
1629
|
let j = i;
|
|
1629
|
-
while (j <
|
|
1630
|
-
if (
|
|
1631
|
-
else
|
|
1630
|
+
while (j < fieldW && levels[j] === level) j++;
|
|
1631
|
+
if (level === 0) out += " ".repeat(j - i);
|
|
1632
|
+
else {
|
|
1633
|
+
const ink = shades[Math.abs(level)].repeat(j - i);
|
|
1634
|
+
out += color ? `\x1B[38;5;${WAVE_RAMP[WAVE_LEVELS + level]}m${ink}${RESET}` : ink;
|
|
1635
|
+
}
|
|
1632
1636
|
i = j;
|
|
1633
1637
|
}
|
|
1634
1638
|
return out;
|
|
1635
1639
|
};
|
|
1640
|
+
const dim = (s) => color ? `\x1B[90m${s}${RESET}` : s;
|
|
1636
1641
|
const spinner = SPINNER_FRAMES[mode];
|
|
1637
1642
|
const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
|
|
1638
1643
|
const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
|
|
1644
|
+
const infoWidth = Math.max(0, ...info.map((l) => displayWidth(l)));
|
|
1645
|
+
const infoIndent = " ".repeat(Math.max(0, Math.floor((width - infoWidth) / 2)));
|
|
1639
1646
|
const block = [
|
|
1640
|
-
...
|
|
1647
|
+
...Array.from({ length: WATER_ROWS }, (_, y) => indent + paintRow(y)),
|
|
1648
|
+
"",
|
|
1649
|
+
statusIndent + dim(status),
|
|
1641
1650
|
"",
|
|
1642
|
-
|
|
1651
|
+
...info.map((l) => infoIndent + dim(l))
|
|
1643
1652
|
];
|
|
1644
1653
|
return [...Array.from({ length: Math.max(0, Math.floor((height - block.length) / 2)) }, () => ""), ...block];
|
|
1645
1654
|
}
|
|
@@ -1668,8 +1677,10 @@ function main() {
|
|
|
1668
1677
|
let lastFrame = "";
|
|
1669
1678
|
let spinnerFrame = 0;
|
|
1670
1679
|
let splashTick = 0;
|
|
1680
|
+
const startedAt = Date.now();
|
|
1681
|
+
const standbyNotice = "waiting for the first mmap_declare ...";
|
|
1671
1682
|
let map;
|
|
1672
|
-
let notice =
|
|
1683
|
+
let notice = standbyNotice;
|
|
1673
1684
|
let lastCols = process.stdout.columns ?? 0;
|
|
1674
1685
|
let lastRows = process.stdout.rows ?? 0;
|
|
1675
1686
|
let pageFiles = [cfg.file];
|
|
@@ -1678,6 +1689,7 @@ function main() {
|
|
|
1678
1689
|
let activeFile;
|
|
1679
1690
|
let firstScan = true;
|
|
1680
1691
|
let pendingFocusFile = cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page);
|
|
1692
|
+
let follow = cfg.follow;
|
|
1681
1693
|
let lastTabSegments = [];
|
|
1682
1694
|
let tabScroll = 0;
|
|
1683
1695
|
let offsetX = 0;
|
|
@@ -1737,13 +1749,17 @@ function main() {
|
|
|
1737
1749
|
const entry = pageData.get(file);
|
|
1738
1750
|
if (entry !== void 0 && entry.fresh) pageData.set(file, { ...entry, fresh: false });
|
|
1739
1751
|
map = entry?.map;
|
|
1740
|
-
notice = map
|
|
1752
|
+
notice = map !== void 0 ? "" : entry?.error ?? (file === cfg.file ? standbyNotice : `waiting for ${file} ...`);
|
|
1741
1753
|
const top = topFiles();
|
|
1742
1754
|
const tabIndex = top.indexOf(file);
|
|
1743
1755
|
if (tabIndex >= 0) tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex);
|
|
1744
1756
|
};
|
|
1745
1757
|
const handSwitch = (file) => {
|
|
1746
1758
|
pendingFocusFile = void 0;
|
|
1759
|
+
if (follow) {
|
|
1760
|
+
follow = false;
|
|
1761
|
+
flash = { text: "auto-follow off \u2014 press f to re-enable", until: Date.now() + 3e3 };
|
|
1762
|
+
}
|
|
1747
1763
|
switchPage(file);
|
|
1748
1764
|
};
|
|
1749
1765
|
const hitTest = (termX, termY) => {
|
|
@@ -1791,7 +1807,17 @@ function main() {
|
|
|
1791
1807
|
lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
|
|
1792
1808
|
if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
|
|
1793
1809
|
} else {
|
|
1794
|
-
|
|
1810
|
+
const info = waitingInfo(
|
|
1811
|
+
{
|
|
1812
|
+
defaultFile: cfg.file,
|
|
1813
|
+
pagesDir: join2(dirname2(cfg.file), PAGES_DIR_NAME),
|
|
1814
|
+
intervalMs: cfg.intervalMs,
|
|
1815
|
+
elapsedMs: interactive ? Date.now() - startedAt : void 0,
|
|
1816
|
+
broken: [...pageData.values()].flatMap((e) => e.map === void 0 && e.error !== void 0 ? [e.error] : [])
|
|
1817
|
+
},
|
|
1818
|
+
Math.max(1, viewW - 2)
|
|
1819
|
+
);
|
|
1820
|
+
body = (interactive ? splashFrame(notice, info, splashTick, viewW, viewH, cfg.unicode, cfg.color) : void 0) ?? [fitWidth(notice, viewW), "", ...info.map((l) => fitWidth(` ${l}`, viewW))];
|
|
1795
1821
|
}
|
|
1796
1822
|
if (notice !== "" && map !== void 0) {
|
|
1797
1823
|
body[body.length - 1] = fitWidth(` ${notice}`, viewW);
|
|
@@ -1805,10 +1831,7 @@ function main() {
|
|
|
1805
1831
|
} else {
|
|
1806
1832
|
panel = mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
|
|
1807
1833
|
}
|
|
1808
|
-
const
|
|
1809
|
-
const bar = (cfg.unicode ? "\u2500" : "-").repeat(viewW);
|
|
1810
|
-
const gripAt = Math.max(0, Math.floor((viewW - grip.length) / 2));
|
|
1811
|
-
const separator = viewW > grip.length + 2 ? bar.slice(0, gripAt) + grip + bar.slice(gripAt + grip.length) : bar;
|
|
1834
|
+
const separator = dividerRow(viewW, cfg.unicode, follow);
|
|
1812
1835
|
const panelRows = [
|
|
1813
1836
|
cfg.color ? `\x1B[90m${separator}${RESET}` : separator,
|
|
1814
1837
|
...panel.map(
|
|
@@ -1875,6 +1898,7 @@ function main() {
|
|
|
1875
1898
|
pageViews.delete(known);
|
|
1876
1899
|
}
|
|
1877
1900
|
}
|
|
1901
|
+
const changedFiles = [];
|
|
1878
1902
|
for (const file of pageFiles) {
|
|
1879
1903
|
let mtimeMs;
|
|
1880
1904
|
try {
|
|
@@ -1886,6 +1910,7 @@ function main() {
|
|
|
1886
1910
|
if (mtimeMs === entry?.mtimeMs) continue;
|
|
1887
1911
|
const loaded = loadMapFile(file);
|
|
1888
1912
|
if (loaded.ok) {
|
|
1913
|
+
if (!firstScan) changedFiles.push(file);
|
|
1889
1914
|
const becameFresh = !firstScan && file !== activeFile;
|
|
1890
1915
|
pageData.set(file, { map: loaded.value, mtimeMs, fresh: becameFresh });
|
|
1891
1916
|
if (file === activeFile) {
|
|
@@ -1896,8 +1921,15 @@ function main() {
|
|
|
1896
1921
|
flash = { text: `${cfg.unicode ? "\u229E " : ""}${title} updated`, until: Date.now() + 4e3 };
|
|
1897
1922
|
}
|
|
1898
1923
|
} else if (loaded.error.kind === "malformed-json") {
|
|
1924
|
+
pageData.set(file, {
|
|
1925
|
+
map: entry?.map,
|
|
1926
|
+
mtimeMs: entry?.mtimeMs ?? -1,
|
|
1927
|
+
fresh: entry?.fresh ?? false,
|
|
1928
|
+
error: describeStoreError(loaded.error)
|
|
1929
|
+
});
|
|
1930
|
+
if (file === activeFile && entry?.map === void 0) notice = describeStoreError(loaded.error);
|
|
1899
1931
|
} else {
|
|
1900
|
-
pageData.set(file, { map: entry?.map, mtimeMs, fresh: entry?.fresh ?? false });
|
|
1932
|
+
pageData.set(file, { map: entry?.map, mtimeMs, fresh: entry?.fresh ?? false, error: describeStoreError(loaded.error) });
|
|
1901
1933
|
if (file === activeFile) notice = describeStoreError(loaded.error);
|
|
1902
1934
|
}
|
|
1903
1935
|
}
|
|
@@ -1905,9 +1937,15 @@ function main() {
|
|
|
1905
1937
|
if (request !== void 0 && !(firstScan && pendingFocusFile !== void 0)) {
|
|
1906
1938
|
pendingFocusFile = pageFilePath(cfg.file, request.page);
|
|
1907
1939
|
}
|
|
1940
|
+
let requestApplied = false;
|
|
1908
1941
|
if (pendingFocusFile !== void 0 && pageFiles.includes(pendingFocusFile)) {
|
|
1909
1942
|
if (pendingFocusFile !== activeFile) switchPage(pendingFocusFile);
|
|
1910
1943
|
pendingFocusFile = void 0;
|
|
1944
|
+
requestApplied = true;
|
|
1945
|
+
}
|
|
1946
|
+
if (follow && !requestApplied && changedFiles.length > 0 && dragAnchor === void 0) {
|
|
1947
|
+
const target = mostRecentPageFile(changedFiles, (f) => pageData.get(f)?.mtimeMs);
|
|
1948
|
+
if (target !== activeFile) switchPage(target);
|
|
1911
1949
|
}
|
|
1912
1950
|
firstScan = false;
|
|
1913
1951
|
if (activeFile === void 0 || !pageFiles.includes(activeFile)) {
|
|
@@ -2076,6 +2114,11 @@ function main() {
|
|
|
2076
2114
|
case "back":
|
|
2077
2115
|
if (climbBack()) dirty = true;
|
|
2078
2116
|
break;
|
|
2117
|
+
case "follow-toggle":
|
|
2118
|
+
follow = !follow;
|
|
2119
|
+
flash = { text: follow ? "auto-follow on" : "auto-follow off", until: Date.now() + 2500 };
|
|
2120
|
+
dirty = true;
|
|
2121
|
+
break;
|
|
2079
2122
|
}
|
|
2080
2123
|
}
|
|
2081
2124
|
if (dirty) paint();
|
|
@@ -2110,6 +2153,8 @@ export {
|
|
|
2110
2153
|
anchorOffsets,
|
|
2111
2154
|
clampPanelRows,
|
|
2112
2155
|
diveOrigin,
|
|
2156
|
+
dividerRow,
|
|
2157
|
+
elapsedLabel,
|
|
2113
2158
|
fitWidth,
|
|
2114
2159
|
launchedAsEntry,
|
|
2115
2160
|
liveRipples,
|
|
@@ -2120,14 +2165,13 @@ export {
|
|
|
2120
2165
|
pageTabRow,
|
|
2121
2166
|
panelRowsFromDividerY,
|
|
2122
2167
|
parseArgs,
|
|
2123
|
-
splashArt,
|
|
2124
2168
|
splashFrame,
|
|
2125
2169
|
tabScrollFor,
|
|
2126
2170
|
topLevelFiles,
|
|
2127
2171
|
usableColumns,
|
|
2172
|
+
waitingInfo,
|
|
2128
2173
|
waveAt,
|
|
2129
2174
|
waveHash,
|
|
2130
2175
|
waveLevel,
|
|
2131
|
-
wordArt,
|
|
2132
2176
|
wrapWidth
|
|
2133
2177
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mellos-mapping",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"mcpName": "io.github.GuangminJu/mellos-mapping",
|
|
5
5
|
"description": "A live layered dependency map for bottom-up development — MCP server + terminal pane. Ghost the design first, then light nodes up from the bottom as they are built and verified.",
|
|
6
6
|
"type": "module",
|