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 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. Elsewhere
134
- run `node <plugin root>/dist/watch.mjs` (same `--page` flag) from the
135
- project directory in a second terminal (or any terminal split).
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
- `node <插件根>/dist/watch.mjs`(同样支持 `--page`)。
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.17.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) => mutate(input.page, (map) => applyDeclare(map, 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 SPLASH_FONT = {
1522
- M: ["# #", "## ##", "# # #", "# #", "# #"],
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 wordArt(word) {
1544
- const glyphs = [...word.toUpperCase()].map((ch) => SPLASH_FONT[ch]).filter((g) => g !== void 0);
1545
- const widths = glyphs.map((g) => Math.max(...g.map((r) => r.length)));
1546
- const rows = [];
1547
- for (let r = 0; r < SPLASH_ROWS; r++) {
1548
- rows.push(glyphs.map((g, i) => (g[r] ?? "").padEnd(widths[i], " ")).join(" "));
1549
- }
1550
- return rows;
1551
- }
1552
- function splashArt() {
1553
- const words = [wordArt("MELLOS"), wordArt("MAPPING")];
1554
- const width = Math.max(...words.flat().map((r) => r.length));
1555
- const centered = words.map((rows) => {
1556
- const own = Math.max(...rows.map((r) => r.length));
1557
- return rows.map((r) => " ".repeat(Math.floor((width - own) / 2)) + r);
1558
- });
1559
- return [...centered[0], "", ...centered[1]];
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 art = splashArt();
1612
- const artWidth = Math.max(...art.map((r) => r.length));
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 solid = shades[shades.length - 1];
1617
- const indent = " ".repeat(Math.floor((width - artWidth) / 2));
1618
- const ripples = liveRipples(frame, artWidth, art.length);
1619
- const inkOf = (x, y) => {
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 < cells.length; ) {
1627
- const cell = cells[i];
1627
+ for (let i = 0; i < fieldW; ) {
1628
+ const level = levels[i];
1628
1629
  let j = i;
1629
- while (j < cells.length && cells[j] === cell) j++;
1630
- if (cell === void 0) out += " ".repeat(j - i);
1631
- else out += color ? `\x1B[${cell}m${solid.repeat(j - i)}${RESET}` : cell.repeat(j - i);
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
- ...art.map((row, y) => row.trim() === "" ? "" : indent + paintRow(row, y)),
1647
+ ...Array.from({ length: WATER_ROWS }, (_, y) => indent + paintRow(y)),
1648
+ "",
1649
+ statusIndent + dim(status),
1641
1650
  "",
1642
- statusIndent + (color ? `\x1B[90m${status}${RESET}` : status)
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 = `waiting for ${cfg.file} ...`;
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 === void 0 ? `waiting for ${file} ...` : "";
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
- body = (interactive ? splashFrame(notice, splashTick, viewW, viewH, cfg.unicode, cfg.color) : void 0) ?? [fitWidth(notice, viewW)];
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 grip = cfg.unicode ? " \u22EF " : " ~ ";
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.17.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",