mellos-mapping 0.15.0 → 0.16.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
@@ -128,9 +128,11 @@ the script after updating the plugin.
128
128
  To watch the live pane beside a Codex session on Windows, run
129
129
  `node <plugin root>/scripts/open-pane.mjs <project dir>` — it splits the
130
130
  terminal window hosting the session (or falls back to a dedicated
131
- "mellos-mapping" window; `--window` picks that on purpose). Elsewhere run
132
- `node <plugin root>/dist/watch.mjs` from the project directory in a second
133
- terminal (or any terminal split).
131
+ "mellos-mapping" window; `--window` picks that on purpose). Add
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).
134
136
 
135
137
  ## Any MCP client
136
138
 
package/README.zh-CN.md CHANGED
@@ -118,8 +118,10 @@ node ~/.codex/plugins/cache/mellos-mapping/mellos-mapping/<版本>/scripts/codex
118
118
  要在 Codex 会话旁边看实况面板,Windows 上运行
119
119
  `node <插件根>/scripts/open-pane.mjs <项目目录>`——它会在承载本会话的
120
120
  终端窗口里分屏(识别不到就确定性地开到专属的 "mellos-mapping" 窗口;
121
- `--window` 则是主动选择专属窗口)。其他环境在项目目录下的第二个终端
122
- (或任意分屏)运行 `node <插件根>/dist/watch.mjs`。
121
+ `--window` 则是主动选择专属窗口)。加 `--page <slug>` 指定打开哪一页;
122
+ 面板已经开着时,带 `--page` 重跑一次不会再开新面板,而是让现有面板
123
+ 切到那一页。其他环境在项目目录下的第二个终端(或任意分屏)运行
124
+ `node <插件根>/dist/watch.mjs`(同样支持 `--page`)。
123
125
 
124
126
  ## 任意 MCP 客户端
125
127
 
package/dist/server.mjs CHANGED
@@ -22133,7 +22133,7 @@ function drawBox(canvas, box, opts, neutral, focused = false) {
22133
22133
  }
22134
22134
 
22135
22135
  // src/store/store.ts
22136
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
22136
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
22137
22137
  import { basename, dirname, join } from "node:path";
22138
22138
  var STATE_FILE_VERSION = 1;
22139
22139
  var STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
@@ -22515,7 +22515,7 @@ function summarize(map) {
22515
22515
 
22516
22516
  // src/server/server.ts
22517
22517
  var SERVER_NAME = "mellos-mapping";
22518
- var SERVER_VERSION = "0.15.0";
22518
+ var SERVER_VERSION = "0.16.0";
22519
22519
  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
22520
  var PAGE = ID.optional().describe(
22521
22521
  "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."
package/dist/watch.mjs CHANGED
@@ -967,11 +967,14 @@ function drawBox(canvas, box, opts, neutral, focused = false) {
967
967
  }
968
968
 
969
969
  // src/store/store.ts
970
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
970
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
971
971
  import { basename, dirname, join } from "node:path";
972
972
  var STATE_FILE_VERSION = 1;
973
973
  var STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
974
974
  var PAGES_DIR_NAME = "mellos-mapping.pages";
975
+ function makePageId(raw) {
976
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
977
+ }
975
978
  function pageFilePath(defaultFile, page) {
976
979
  return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
977
980
  }
@@ -993,6 +996,35 @@ function listPageFiles(defaultFile) {
993
996
  }
994
997
  return out;
995
998
  }
999
+ var FOCUS_FILE_NAME = "mellos-mapping.focus";
1000
+ function focusFilePath(defaultFile) {
1001
+ return join(dirname(defaultFile), FOCUS_FILE_NAME);
1002
+ }
1003
+ function takeFocusRequest(defaultFile) {
1004
+ const path = focusFilePath(defaultFile);
1005
+ let raw;
1006
+ try {
1007
+ raw = readFileSync(path, "utf8");
1008
+ } catch {
1009
+ return void 0;
1010
+ }
1011
+ try {
1012
+ rmSync(path, { force: true });
1013
+ } catch {
1014
+ }
1015
+ let parsed;
1016
+ try {
1017
+ parsed = JSON.parse(raw);
1018
+ } catch {
1019
+ return void 0;
1020
+ }
1021
+ if (typeof parsed !== "object" || parsed === null) return void 0;
1022
+ const page = parsed.page;
1023
+ if (page === void 0 || page === null) return { page: void 0 };
1024
+ if (typeof page !== "string") return void 0;
1025
+ const id = makePageId(page);
1026
+ return id.ok ? { page: id.value } : void 0;
1027
+ }
996
1028
  function describeStoreError(e) {
997
1029
  switch (e.kind) {
998
1030
  case "not-found":
@@ -1241,11 +1273,17 @@ function parseArgs(argv, cwd) {
1241
1273
  let unicode = true;
1242
1274
  let color = true;
1243
1275
  let mouse = true;
1276
+ let page;
1244
1277
  for (let i = 0; i < argv.length; i++) {
1245
1278
  switch (argv[i]) {
1246
1279
  case "--file":
1247
1280
  file = argv[++i] ?? file;
1248
1281
  break;
1282
+ case "--page": {
1283
+ const parsed = makePageId(argv[++i] ?? "");
1284
+ if (parsed.ok) page = parsed.value;
1285
+ break;
1286
+ }
1249
1287
  case "--interval":
1250
1288
  intervalMs = Math.max(50, Number(argv[++i]) || intervalMs);
1251
1289
  break;
@@ -1262,7 +1300,19 @@ function parseArgs(argv, cwd) {
1262
1300
  break;
1263
1301
  }
1264
1302
  }
1265
- return { file, intervalMs, unicode, color, mouse };
1303
+ return { file, intervalMs, unicode, color, mouse, page };
1304
+ }
1305
+ function mostRecentPageFile(files, mtimeOf) {
1306
+ let best;
1307
+ let bestMtime = -Infinity;
1308
+ for (const file of files) {
1309
+ const mtime = mtimeOf(file);
1310
+ if (mtime !== void 0 && mtime > bestMtime) {
1311
+ best = file;
1312
+ bestMtime = mtime;
1313
+ }
1314
+ }
1315
+ return best ?? files[0];
1266
1316
  }
1267
1317
  var HIDE_CURSOR = "\x1B[?25l";
1268
1318
  var SHOW_CURSOR = "\x1B[?25h";
@@ -1627,6 +1677,7 @@ function main() {
1627
1677
  const pageViews = /* @__PURE__ */ new Map();
1628
1678
  let activeFile;
1629
1679
  let firstScan = true;
1680
+ let pendingFocusFile = cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page);
1630
1681
  let lastTabSegments = [];
1631
1682
  let tabScroll = 0;
1632
1683
  let offsetX = 0;
@@ -1656,7 +1707,7 @@ function main() {
1656
1707
  parent = diveOrigin(cfg.file, activeFile, pageFiles, maps())?.parent;
1657
1708
  }
1658
1709
  if (parent !== void 0 && parent !== activeFile) {
1659
- switchPage(parent);
1710
+ handSwitch(parent);
1660
1711
  return true;
1661
1712
  }
1662
1713
  return false;
@@ -1691,6 +1742,10 @@ function main() {
1691
1742
  const tabIndex = top.indexOf(file);
1692
1743
  if (tabIndex >= 0) tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex);
1693
1744
  };
1745
+ const handSwitch = (file) => {
1746
+ pendingFocusFile = void 0;
1747
+ switchPage(file);
1748
+ };
1694
1749
  const hitTest = (termX, termY) => {
1695
1750
  const sx = termX - 1;
1696
1751
  const sy = termY - 1 - tabRows();
@@ -1846,8 +1901,18 @@ function main() {
1846
1901
  if (file === activeFile) notice = describeStoreError(loaded.error);
1847
1902
  }
1848
1903
  }
1904
+ const request = takeFocusRequest(cfg.file);
1905
+ if (request !== void 0 && !(firstScan && pendingFocusFile !== void 0)) {
1906
+ pendingFocusFile = pageFilePath(cfg.file, request.page);
1907
+ }
1908
+ if (pendingFocusFile !== void 0 && pageFiles.includes(pendingFocusFile)) {
1909
+ if (pendingFocusFile !== activeFile) switchPage(pendingFocusFile);
1910
+ pendingFocusFile = void 0;
1911
+ }
1849
1912
  firstScan = false;
1850
- if (activeFile === void 0 || !pageFiles.includes(activeFile)) switchPage(pageFiles[0]);
1913
+ if (activeFile === void 0 || !pageFiles.includes(activeFile)) {
1914
+ switchPage(mostRecentPageFile(pageFiles, (f) => pageData.get(f)?.mtimeMs));
1915
+ }
1851
1916
  if ([...pageData.values()].some((p) => p.map?.nodes.some((n) => n.status === "in-progress"))) spinnerFrame++;
1852
1917
  if (flash !== void 0 && Date.now() > flash.until) flash = void 0;
1853
1918
  paint();
@@ -1959,7 +2024,7 @@ function main() {
1959
2024
  tabScroll = Math.max(0, Math.min(tabScroll + tabHit.action.delta, lastTabFiles.length - 1));
1960
2025
  } else {
1961
2026
  const target = lastTabFiles[tabHit.action.index];
1962
- if (target !== void 0 && target !== activeFile) switchPage(target);
2027
+ if (target !== void 0 && target !== activeFile) handSwitch(target);
1963
2028
  }
1964
2029
  } else {
1965
2030
  const id = hitTest(event.x, event.y);
@@ -1970,7 +2035,7 @@ function main() {
1970
2035
  const target = pageFilePath(cfg.file, submap);
1971
2036
  if (pageFiles.includes(target) && target !== activeFile) {
1972
2037
  diveStack.push(activeFile);
1973
- switchPage(target);
2038
+ handSwitch(target);
1974
2039
  } else if (!pageFiles.includes(target)) {
1975
2040
  flash = { text: `submap "${submap}" has no page yet`, until: now + 2500 };
1976
2041
  }
@@ -1994,7 +2059,7 @@ function main() {
1994
2059
  const step = event.kind === "next-page" ? 1 : -1;
1995
2060
  const target = top[(current + step + top.length) % top.length];
1996
2061
  if (target !== activeFile) {
1997
- switchPage(target);
2062
+ handSwitch(target);
1998
2063
  dirty = true;
1999
2064
  }
2000
2065
  }
@@ -2003,7 +2068,7 @@ function main() {
2003
2068
  case "page": {
2004
2069
  const target = topFiles()[event.index];
2005
2070
  if (target !== void 0 && target !== activeFile) {
2006
- switchPage(target);
2071
+ handSwitch(target);
2007
2072
  dirty = true;
2008
2073
  }
2009
2074
  break;
@@ -2049,6 +2114,7 @@ export {
2049
2114
  launchedAsEntry,
2050
2115
  liveRipples,
2051
2116
  mapPanel,
2117
+ mostRecentPageFile,
2052
2118
  nearestHit,
2053
2119
  nodePanel,
2054
2120
  pageTabRow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mellos-mapping",
3
- "version": "0.15.0",
3
+ "version": "0.16.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",
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Open the live map pane in the RIGHT Windows Terminal window.
4
4
  *
5
- * node scripts/open-pane.mjs <project-dir> [--window] [--ascii] [--force]
5
+ * node scripts/open-pane.mjs <project-dir> [--page <slug>] [--window] [--ascii] [--force]
6
6
  *
7
7
  * Why this exists: the agent's shell runs on a hidden console (no WT_SESSION),
8
8
  * so a bare `wt -w 0 sp` targets the MOST RECENTLY USED terminal window — with
@@ -33,18 +33,38 @@
33
33
  * Known race, accepted: if the user focuses a DIFFERENT terminal window in
34
34
  * the ~1s between our focus-verify and wt reading its MRU state, the split
35
35
  * can still land there. The window is at least one the user is actively in.
36
+ *
37
+ * --page <slug> opens the map ON that page (the effort under discussion, not
38
+ * whatever page the store lists first). With a watcher already running it
39
+ * writes the one-shot focus file instead — the existing pane retargets within
40
+ * a poll tick — so re-running with --page is also how you steer an open pane.
36
41
  */
37
42
  import { spawnSync } from 'node:child_process';
38
- import { existsSync } from 'node:fs';
43
+ import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
39
44
  import { dirname, join, resolve } from 'node:path';
40
45
  import { fileURLToPath } from 'node:url';
41
46
 
47
+ const USAGE = 'usage: node scripts/open-pane.mjs <project-dir> [--page <slug>] [--window] [--ascii] [--force]';
48
+
42
49
  const argv = process.argv.slice(2);
43
- const flags = new Set(argv.filter((a) => a.startsWith('--')));
44
- const positional = argv.filter((a) => !a.startsWith('--'));
50
+ const flags = new Set();
51
+ const positional = [];
52
+ let pageSlug;
53
+ for (let i = 0; i < argv.length; i++) {
54
+ const a = argv[i];
55
+ if (a === '--page') pageSlug = argv[++i];
56
+ else if (a.startsWith('--')) flags.add(a);
57
+ else positional.push(a);
58
+ }
45
59
 
46
60
  if (positional.length !== 1) {
47
- console.error('usage: node scripts/open-pane.mjs <project-dir> [--window] [--ascii] [--force]');
61
+ console.error(USAGE);
62
+ process.exit(1);
63
+ }
64
+ // Mirrors ID_RULE in src/domain/types.ts — this script runs standalone and
65
+ // cannot import the TypeScript sources.
66
+ if (pageSlug !== undefined && !/^[a-z0-9][a-z0-9-]{0,63}$/.test(pageSlug)) {
67
+ console.error(`--page needs a kebab-case slug (got "${pageSlug}")\n${USAGE}`);
48
68
  process.exit(1);
49
69
  }
50
70
  if (process.platform !== 'win32') {
@@ -203,6 +223,7 @@ Write-Output "FOCUS=$(if ($focused) { 1 } else { 0 })"
203
223
  function paneCommand() {
204
224
  const cmd = ['--title', 'mellos map', '-d', projectDir, 'node', watchPath, '--file', mapFile];
205
225
  if (flags.has('--ascii')) cmd.push('--ascii');
226
+ if (pageSlug !== undefined) cmd.push('--page', pageSlug);
206
227
  return cmd;
207
228
  }
208
229
 
@@ -221,8 +242,21 @@ function openDedicatedWindow(reason) {
221
242
  }
222
243
 
223
244
  if (!flags.has('--force') && watcherAlreadyRunning()) {
224
- console.log('MMAP_PANE already-open');
225
- console.log(`A watcher for ${mapFile} is already running — not opening another pane (use --force to override).`);
245
+ if (pageSlug !== undefined) {
246
+ // One-shot focus request (see takeFocusRequest in src/store/store.ts):
247
+ // the running watcher consumes and deletes it within a poll tick. Temp +
248
+ // rename because the watcher polls: a torn read would be swept as junk,
249
+ // silently losing the request.
250
+ mkdirSync(dirname(mapFile), { recursive: true });
251
+ const focusFile = join(dirname(mapFile), 'mellos-mapping.focus');
252
+ writeFileSync(`${focusFile}.tmp`, JSON.stringify({ page: pageSlug }));
253
+ renameSync(`${focusFile}.tmp`, focusFile);
254
+ console.log(`MMAP_PANE already-open refocused=${pageSlug}`);
255
+ console.log(`A watcher for ${mapFile} is already running — asked it to show page "${pageSlug}".`);
256
+ } else {
257
+ console.log('MMAP_PANE already-open');
258
+ console.log(`A watcher for ${mapFile} is already running — not opening another pane (use --force to override).`);
259
+ }
226
260
  process.exit(0);
227
261
  }
228
262