mellos-mapping 0.20.2 → 0.22.1

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.
Files changed (68) hide show
  1. package/README.md +125 -88
  2. package/README.zh-CN.md +122 -84
  3. package/dist/hook-session-start.mjs +25 -19
  4. package/dist/mmap.mjs +302 -170
  5. package/dist/preview.mjs +1418 -0
  6. package/dist/server.mjs +1198 -400
  7. package/dist/store-paths.mjs +40 -21
  8. package/dist/terminal-worker.mjs +3188 -0
  9. package/dist/watch.mjs +523 -280
  10. package/dist/web/TERMINAL-LICENSES.txt +70 -0
  11. package/dist/web/app.css +967 -0
  12. package/dist/web/app.js +1356 -0
  13. package/dist/web/index.html +9 -0
  14. package/dist/web/terminal.css +9 -0
  15. package/dist/web/terminal.html +7 -0
  16. package/dist/web/terminal.js +9293 -0
  17. package/dist/web/xterm.css +285 -0
  18. package/dist/web.mjs +5535 -0
  19. package/docs/codex.md +183 -0
  20. package/lib/domain/text.d.ts +9 -0
  21. package/lib/domain/text.js +43 -0
  22. package/lib/domain/types.js +10 -1
  23. package/lib/preview/index.d.ts +3 -0
  24. package/lib/preview/index.js +3 -0
  25. package/lib/preview/markdown.d.ts +8 -0
  26. package/lib/preview/markdown.js +54 -0
  27. package/lib/preview/presentation.d.ts +6 -0
  28. package/lib/preview/presentation.js +14 -0
  29. package/lib/preview/publisher.d.ts +23 -0
  30. package/lib/preview/publisher.js +143 -0
  31. package/lib/preview/svg.d.ts +3 -0
  32. package/lib/preview/svg.js +74 -0
  33. package/lib/preview/text.d.ts +4 -0
  34. package/lib/preview/text.js +13 -0
  35. package/lib/render/canvas.d.ts +1 -1
  36. package/lib/render/canvas.js +4 -2
  37. package/lib/render/draw.js +9 -6
  38. package/lib/render/render.d.ts +6 -0
  39. package/lib/render/render.js +50 -18
  40. package/lib/render/width.js +3 -1
  41. package/lib/store/atomic.d.ts +18 -0
  42. package/lib/store/atomic.js +86 -0
  43. package/lib/store/channels.d.ts +40 -0
  44. package/lib/store/channels.js +135 -0
  45. package/lib/store/format.js +3 -1
  46. package/lib/store/json-text.d.ts +9 -0
  47. package/lib/store/json-text.js +16 -0
  48. package/lib/store/maps.d.ts +12 -0
  49. package/lib/store/maps.js +42 -0
  50. package/lib/store/migration.d.ts +12 -0
  51. package/lib/store/migration.js +46 -0
  52. package/lib/store/pages.d.ts +46 -0
  53. package/lib/store/pages.js +89 -0
  54. package/lib/store/policy.d.ts +74 -0
  55. package/lib/store/policy.js +144 -0
  56. package/lib/store/store.d.ts +9 -256
  57. package/lib/store/store.js +10 -694
  58. package/lib/store/viewers.d.ts +81 -0
  59. package/lib/store/viewers.js +186 -0
  60. package/package.json +25 -6
  61. package/scripts/codex-cli.mjs +42 -0
  62. package/scripts/codex-register.mjs +27 -94
  63. package/scripts/mmap.mjs +26 -16
  64. package/scripts/open-pane.mjs +37 -18
  65. package/scripts/pane-core.mjs +57 -220
  66. package/scripts/terminal-session.mjs +137 -0
  67. package/scripts/tmux-session.mjs +90 -0
  68. package/scripts/watcher-command.mjs +16 -0
package/dist/watch.mjs CHANGED
@@ -3,8 +3,7 @@ import { createRequire } from 'node:module'; const require = createRequire(impor
3
3
 
4
4
  // src/watch/watch.ts
5
5
  import { realpathSync, statSync as statSync2 } from "node:fs";
6
- import { dirname as dirname2, join as join2 } from "node:path";
7
- import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { dirname as dirname7, join as join6 } from "node:path";
8
7
 
9
8
  // src/domain/types.ts
10
9
  var ok = (value) => ({ ok: true, value });
@@ -87,7 +86,7 @@ function describeMapError(e) {
87
86
  case "layer-holds-group":
88
87
  return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups (removeGroup) first`;
89
88
  case "edge-not-downward":
90
- return `edge ${e.from} (rank ${e.fromRank}) -> ${e.to} (rank ${e.toRank}) is not strictly downward; dependencies may only point to a lower layer`;
89
+ return `edge ${e.from} (rank ${e.fromRank}) -> ${e.to} (rank ${e.toRank}) is not strictly downward; ` + (e.fromRank === e.toRank ? `same-band siblings may not depend on each other \u2014 either "${e.to}" is really a lower concept (declare it on a lower band) or "${e.from}" and "${e.to}" are one node (merge them)` : `"${e.from}" would USE "${e.to}" from a lower band \u2014 reverse the edge if "${e.to}" is the user, otherwise move or re-rank so "${e.from}" sits above "${e.to}"`);
91
90
  }
92
91
  }
93
92
 
@@ -479,6 +478,41 @@ function flipForSequence(map) {
479
478
  };
480
479
  }
481
480
 
481
+ // src/domain/text.ts
482
+ var NO_CONTROLS = /^[^\u0000-\u001f\u007f-\u009f]*$/;
483
+ var NO_CONTROLS_TEXT = "one line of text; control characters (ESC, newline, tab) are not allowed";
484
+ var NO_CONTROLS_BUT_BREAKS = /^[^\u0000-\u0008\u000b-\u001f\u007f-\u009f]*$/;
485
+ var NO_CONTROLS_BUT_BREAKS_TEXT = "text with optional newlines (\\n) and tabs; other control characters (ESC, BEL, CR) are not allowed";
486
+ function mapTextError(map) {
487
+ const check = (field, value, multiline = false) => value === void 0 || (multiline ? NO_CONTROLS_BUT_BREAKS : NO_CONTROLS).test(value) ? void 0 : `${field}: ${multiline ? NO_CONTROLS_BUT_BREAKS_TEXT : NO_CONTROLS_TEXT}`;
488
+ let error = check("title", map.title);
489
+ if (error) return error;
490
+ for (const [i, layer] of map.layers.entries()) {
491
+ error = check(`layers[${i}].name`, layer.name);
492
+ if (error) return error;
493
+ }
494
+ for (const name of ["lanes", "groups"]) {
495
+ for (const [i, item] of map[name].entries()) {
496
+ error = check(`${name}[${i}].label`, item.label);
497
+ if (error) return error;
498
+ }
499
+ }
500
+ for (const [i, node] of map.nodes.entries()) {
501
+ for (const name of ["label", "evidence", "detail"]) {
502
+ error = check(`nodes[${i}].${name}`, node[name], name !== "label");
503
+ if (error) return error;
504
+ }
505
+ }
506
+ for (const [i, edge] of map.edges.entries()) {
507
+ error = check(`edges[${i}].label`, edge.label);
508
+ if (error) return error;
509
+ }
510
+ return void 0;
511
+ }
512
+ function terminalText(text, multiline = false) {
513
+ return text.replace(multiline ? /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g : /[\u0000-\u001f\u007f-\u009f]/g, "?");
514
+ }
515
+
482
516
  // src/render/width.ts
483
517
  var WIDE_RANGES = [
484
518
  [4352, 4447],
@@ -573,6 +607,7 @@ function displayWidth(text) {
573
607
  return w;
574
608
  }
575
609
  function fitWidth(s, width) {
610
+ s = terminalText(s);
576
611
  if (displayWidth(s) <= width) return s;
577
612
  let out = "";
578
613
  let w = 0;
@@ -588,7 +623,7 @@ function wrapWidth(s, width) {
588
623
  const lines = [];
589
624
  let line = "";
590
625
  let w = 0;
591
- for (const ch of s.replace(/\r/g, "")) {
626
+ for (const ch of terminalText(s.replace(/\r/g, "").replace(/\t/g, " "), true)) {
592
627
  if (ch === "\n") {
593
628
  lines.push(line);
594
629
  line = "";
@@ -617,7 +652,9 @@ var SGR = {
617
652
  greenDim: "32;2",
618
653
  // done, but nothing behind the claim: green, not fully lit
619
654
  red: "31",
620
- faint: "90"
655
+ faint: "90",
656
+ focus: "97"
657
+ // bright foreground without changing font weight
621
658
  };
622
659
  var ANSI_RESET = "\x1B[0m";
623
660
  var UP = 1;
@@ -678,7 +715,7 @@ var Canvas = class {
678
715
  /** Write literal text starting at (x, y). Returns the column just past it. */
679
716
  text(x, y, s, style, bold = false) {
680
717
  let cx = x;
681
- for (const ch of s) {
718
+ for (const ch of terminalText(s)) {
682
719
  const w = charWidth(ch.codePointAt(0));
683
720
  if (w === 0) {
684
721
  const base = this.cell(Math.max(0, cx - 1), y);
@@ -737,7 +774,7 @@ var Canvas = class {
737
774
  } else if (charWidth(ch.codePointAt(0)) === 2 && x + 1 >= vp.x + vp.width) {
738
775
  ch = " ";
739
776
  }
740
- const params = ch === " " ? "" : isWire ? c.bright ? "1" : SGR.faint : [SGR[c.style], c.bold ? "1" : ""].filter(Boolean).join(";");
777
+ const params = ch === " " ? "" : isWire ? c.bright ? SGR.focus : SGR.faint : [SGR[c.style], c.bold ? "1" : ""].filter(Boolean).join(";");
741
778
  if (opts.color && params !== open) {
742
779
  line += (open !== "" ? ANSI_RESET : "") + (params !== "" ? `\x1B[${params}m` : "");
743
780
  open = params;
@@ -1086,6 +1123,7 @@ function drawBands(canvas, columns, rows, wiredWidth, totalWidth) {
1086
1123
  function drawBox(canvas, box, opts, neutral, face, focused = false) {
1087
1124
  const { node, x, y, w } = box;
1088
1125
  const skin = neutral ? neutralSkin(opts.unicode) : skinFor(face, opts.unicode);
1126
+ const borderStyle = focused ? "focus" : skin.style;
1089
1127
  const slotGlyph = neutral ? neutralGlyph(node, opts.unicode) : glyphFor(face, opts);
1090
1128
  if (box.borderless) {
1091
1129
  canvas.text(x + 1, y, slotGlyph, skin.style, true);
@@ -1093,18 +1131,18 @@ function drawBox(canvas, box, opts, neutral, face, focused = false) {
1093
1131
  }
1094
1132
  const inner = w - 2;
1095
1133
  const pad = box.pad === 1 ? " " : "";
1096
- canvas.text(x, y, skin.corners[0] + skin.h.repeat(inner) + skin.corners[1], skin.style, focused);
1097
- canvas.text(x, y + 1, skin.v, skin.style, focused);
1134
+ canvas.text(x, y, skin.corners[0] + skin.h.repeat(inner) + skin.corners[1], borderStyle);
1135
+ canvas.text(x, y + 1, skin.v, borderStyle);
1098
1136
  canvas.text(x + 1, y + 1, `${pad}${slotGlyph} ${box.label}${pad}`, skin.style, true);
1099
- canvas.text(x + w - 1, y + 1, skin.v, skin.style, focused);
1137
+ canvas.text(x + w - 1, y + 1, skin.v, borderStyle);
1100
1138
  for (let i = 0; i < box.extra.length; i++) {
1101
1139
  const row = box.extra[i];
1102
1140
  const yy = y + 2 + i;
1103
- canvas.text(x, yy, skin.v, skin.style, focused);
1141
+ canvas.text(x, yy, skin.v, borderStyle);
1104
1142
  canvas.text(x + 1, yy, row.text, row.style);
1105
- canvas.text(x + w - 1, yy, skin.v, skin.style, focused);
1143
+ canvas.text(x + w - 1, yy, skin.v, borderStyle);
1106
1144
  }
1107
- canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3], skin.style, focused);
1145
+ canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3], borderStyle);
1108
1146
  }
1109
1147
  function drawEdges(canvas, edges, rows, opts) {
1110
1148
  for (const edge of edges) {
@@ -1283,7 +1321,27 @@ function layoutRows(columns, geo, gapRowCount, hasTitle, hasLanes) {
1283
1321
 
1284
1322
  // src/render/render.ts
1285
1323
  function renderMapWindow(map, opts, viewport) {
1286
- const built = buildCanvas(map, opts);
1324
+ return renderSceneWindow(prepareScene(map, opts), opts, viewport);
1325
+ }
1326
+ function createWindowRenderer() {
1327
+ let previous;
1328
+ let frame;
1329
+ return (map, opts, viewport) => {
1330
+ const zoom = opts.zoom ?? ZOOM_DEFAULT;
1331
+ if (previous?.map !== map || previous.unicode !== opts.unicode || previous.zoom !== zoom) {
1332
+ previous = { map, unicode: opts.unicode, zoom, scene: prepareScene(map, opts) };
1333
+ }
1334
+ const scene = previous.scene;
1335
+ if (frame?.scene !== scene || frame.spinner !== opts.spinnerFrame || frame.focus !== opts.focus || frame.color !== opts.color) {
1336
+ frame = { scene, spinner: opts.spinnerFrame, focus: opts.focus, color: opts.color, built: paint(scene, opts) };
1337
+ }
1338
+ return emitWindow(frame.built, opts, viewport);
1339
+ };
1340
+ }
1341
+ function renderSceneWindow(scene, opts, viewport) {
1342
+ return emitWindow(paint(scene, opts), opts, viewport);
1343
+ }
1344
+ function emitWindow(built, opts, viewport) {
1287
1345
  return {
1288
1346
  lines: built.canvas.emit(opts, viewport),
1289
1347
  contentWidth: built.canvas.width,
@@ -1291,26 +1349,40 @@ function renderMapWindow(map, opts, viewport) {
1291
1349
  hits: built.hits
1292
1350
  };
1293
1351
  }
1294
- function buildCanvas(map, opts) {
1352
+ function prepareScene(map, opts) {
1295
1353
  const oriented = flipForSequence(map);
1296
1354
  const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
1297
1355
  const aggregated = plainGeo.mode === "constellation" ? aggregateMap(oriented) : void 0;
1298
1356
  const drawn = aggregated ?? oriented;
1299
- return paint(drawn, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo, unverifiedDoneIds(oriented, drawn));
1357
+ const unverified = unverifiedDoneIds(oriented, drawn);
1358
+ if (drawn.layers.length === 0) return { map: drawn, unverified };
1359
+ return { map: drawn, unverified, geometry: prepareGeometry(drawn, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo) };
1300
1360
  }
1301
- function paint(map, opts, geo, unverified) {
1302
- const canvas = new Canvas();
1303
- if (map.layers.length === 0) {
1304
- canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
1305
- canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
1306
- return { canvas, hits: [] };
1307
- }
1361
+ function prepareGeometry(map, opts, geo) {
1308
1362
  const neutral = isNeutralKind(map);
1309
1363
  const columns = layoutColumns(map, geo, opts.unicode, neutral);
1310
1364
  const routing = routeEdges(map, columns);
1311
1365
  const rows = layoutRows(columns, geo, routing.gapRowCount, map.title !== void 0, map.lanes.length > 0);
1312
1366
  const wiredWidth = routing.fallbackCount > 0 ? columns.contentWidth + 2 + routing.fallbackCount * 2 : columns.contentWidth;
1313
1367
  const totalWidth = wiredWidth + Math.max(...columns.bandLabel.map(displayWidth));
1368
+ const hits = [...rows.boxOf.values()].map((b) => ({
1369
+ id: b.node.id,
1370
+ x: b.x,
1371
+ y: b.y,
1372
+ w: b.w,
1373
+ h: b.h
1374
+ }));
1375
+ return { neutral, columns, routing, rows, wiredWidth, totalWidth, hits };
1376
+ }
1377
+ function paint(scene, opts) {
1378
+ const { map, unverified, geometry } = scene;
1379
+ const canvas = new Canvas();
1380
+ if (geometry === void 0) {
1381
+ canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
1382
+ canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
1383
+ return { canvas, hits: [] };
1384
+ }
1385
+ const { neutral, columns, routing, rows, wiredWidth, totalWidth, hits } = geometry;
1314
1386
  if (map.title !== void 0) drawTitle(canvas, map.title);
1315
1387
  drawLaneHeaders(canvas, map, columns, rows);
1316
1388
  drawBands(canvas, columns, rows, wiredWidth, totalWidth);
@@ -1321,20 +1393,9 @@ function paint(map, opts, geo, unverified) {
1321
1393
  }
1322
1394
  drawEdges(canvas, routing.edges, rows, opts);
1323
1395
  drawLegend(canvas, map, opts, rows.legendY, neutral, unverified.size > 0);
1324
- const hits = [...rows.boxOf.values()].map((b) => ({
1325
- id: b.node.id,
1326
- x: b.x,
1327
- y: b.y,
1328
- w: b.w,
1329
- h: b.h
1330
- }));
1331
1396
  return { canvas, hits };
1332
1397
  }
1333
1398
 
1334
- // src/store/store.ts
1335
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
1336
- import { basename, dirname, join } from "node:path";
1337
-
1338
1399
  // src/store/format.ts
1339
1400
  var STATE_FILE_VERSION = 1;
1340
1401
  function makePageId(raw) {
@@ -1545,11 +1606,13 @@ function parseMap(raw, path) {
1545
1606
  if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
1546
1607
  map = linked.value;
1547
1608
  }
1548
- return ok(map);
1609
+ const textError = mapTextError(map);
1610
+ return textError ? err({ kind: "bad-shape", path, detail: textError }) : ok(map);
1549
1611
  }
1550
1612
 
1551
- // src/store/store.ts
1552
- var RENAME_MAX_ATTEMPTS = 10;
1613
+ // src/store/atomic.ts
1614
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
1615
+ import { dirname } from "node:path";
1553
1616
  var RENAME_BACKOFF_STEP_MS = 10;
1554
1617
  var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOENT"]);
1555
1618
  function sleepSync(ms) {
@@ -1564,7 +1627,7 @@ function discardTemp(tmp) {
1564
1627
  function errnoOf(e) {
1565
1628
  return e.code ?? e.message;
1566
1629
  }
1567
- function writeFileAtomic(path, contents) {
1630
+ function writeAtomic(path, contents, maxAttempts) {
1568
1631
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
1569
1632
  try {
1570
1633
  mkdirSync(dirname(path), { recursive: true });
@@ -1580,7 +1643,7 @@ function writeFileAtomic(path, contents) {
1580
1643
  return ok(void 0);
1581
1644
  } catch (e) {
1582
1645
  const code = errnoOf(e);
1583
- if (!TRANSIENT_RENAME_CODES.has(code) || attempt >= RENAME_MAX_ATTEMPTS) {
1646
+ if (!TRANSIENT_RENAME_CODES.has(code) || attempt >= maxAttempts) {
1584
1647
  discardTemp(tmp);
1585
1648
  return err({ kind: "save-failed", path, detail: `${code} after ${attempt} attempt(s)` });
1586
1649
  }
@@ -1589,17 +1652,15 @@ function writeFileAtomic(path, contents) {
1589
1652
  }
1590
1653
  }
1591
1654
  }
1592
- function isRecord2(v) {
1593
- return typeof v === "object" && v !== null && !Array.isArray(v);
1594
- }
1595
- function stripBom(text) {
1596
- return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
1597
- }
1655
+
1656
+ // src/store/pages.ts
1657
+ import { existsSync, readdirSync, rmSync as rmSync2 } from "node:fs";
1658
+ import { basename, dirname as dirname2, join } from "node:path";
1598
1659
  var STORE_DIR_NAME = ".mellos";
1599
1660
  var STATE_FILE_RELATIVE_PATH = join(STORE_DIR_NAME, "map.json");
1600
1661
  var PAGES_DIR_NAME = "pages";
1601
1662
  function pageFilePath(defaultFile, page) {
1602
- return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
1663
+ return page === void 0 ? defaultFile : join(dirname2(defaultFile), PAGES_DIR_NAME, `${page}.json`);
1603
1664
  }
1604
1665
  function pageIdOfFile(defaultFile, path) {
1605
1666
  if (path === defaultFile) return void 0;
@@ -1611,123 +1672,158 @@ function listPageFiles(defaultFile) {
1611
1672
  if (existsSync(defaultFile)) out.push(defaultFile);
1612
1673
  let entries = [];
1613
1674
  try {
1614
- entries = readdirSync(join(dirname(defaultFile), PAGES_DIR_NAME));
1675
+ entries = readdirSync(join(dirname2(defaultFile), PAGES_DIR_NAME));
1615
1676
  } catch {
1616
1677
  }
1617
1678
  for (const e of entries.sort()) {
1618
- if (e.endsWith(".json")) out.push(join(dirname(defaultFile), PAGES_DIR_NAME, e));
1679
+ if (e.endsWith(".json")) out.push(join(dirname2(defaultFile), PAGES_DIR_NAME, e));
1619
1680
  }
1620
1681
  return out;
1621
1682
  }
1622
1683
  function deletePageFile(path) {
1623
1684
  try {
1624
- rmSync(path, { force: true });
1685
+ rmSync2(path, { force: true });
1625
1686
  return ok(void 0);
1626
1687
  } catch (e) {
1627
1688
  return err({ kind: "delete-failed", path, detail: errnoOf(e) });
1628
1689
  }
1629
1690
  }
1691
+
1692
+ // src/store/channels.ts
1693
+ import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync4 } from "node:fs";
1694
+ import { dirname as dirname4, join as join3 } from "node:path";
1695
+
1696
+ // src/store/json-text.ts
1697
+ function isRecord2(v) {
1698
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1699
+ }
1700
+ function stripBom(text) {
1701
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
1702
+ }
1703
+
1704
+ // src/store/viewers.ts
1705
+ import { readdirSync as readdirSync2, readFileSync, statSync, rmSync as rmSync3 } from "node:fs";
1706
+ import { dirname as dirname3, join as join2 } from "node:path";
1707
+ var VIEWERS_DIR_NAME = "viewers";
1708
+ var VIEWER_FILE_VERSION = 1;
1709
+ var VIEWER_HEARTBEAT_MS = 1e3;
1710
+ function viewersDirPath(defaultFile) {
1711
+ return join2(dirname3(defaultFile), VIEWERS_DIR_NAME);
1712
+ }
1713
+ function viewerFilePath(defaultFile, pid) {
1714
+ return join2(viewersDirPath(defaultFile), `${pid}.json`);
1715
+ }
1716
+ function publishViewer(defaultFile, pid, report) {
1717
+ const body = { version: VIEWER_FILE_VERSION, page: report.page ?? null, follow: report.follow, owner: report.owner };
1718
+ return writeAtomic(viewerFilePath(defaultFile, pid), `${JSON.stringify(body, null, 2)}
1719
+ `, 1);
1720
+ }
1721
+ function retireViewer(defaultFile, pid) {
1722
+ try {
1723
+ rmSync3(viewerFilePath(defaultFile, pid), { force: true });
1724
+ } catch {
1725
+ }
1726
+ }
1727
+
1728
+ // src/store/channels.ts
1630
1729
  var FOCUS_FILE_NAME = "focus";
1631
- function focusFilePath(defaultFile) {
1632
- return join(dirname(defaultFile), FOCUS_FILE_NAME);
1730
+ function focusFilePath(defaultFile, pid) {
1731
+ return paneChannelPath(defaultFile, FOCUS_FILE_NAME, pid);
1732
+ }
1733
+ function paneChannelPath(defaultFile, channel, pid) {
1734
+ if (pid === void 0) return join3(dirname4(defaultFile), channel);
1735
+ if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("Invalid pane process id");
1736
+ return join3(viewersDirPath(defaultFile), `${pid}.${channel}`);
1633
1737
  }
1634
- function takeFocusRequest(defaultFile) {
1635
- const path = focusFilePath(defaultFile);
1738
+ function takeFocusRequest(defaultFile, pid) {
1739
+ const targeted = pid === void 0 ? void 0 : focusFilePath(defaultFile, pid);
1740
+ const path = targeted !== void 0 && existsSync2(targeted) ? targeted : focusFilePath(defaultFile);
1636
1741
  let raw;
1637
1742
  try {
1638
- raw = readFileSync(path, "utf8");
1743
+ raw = readFileSync2(path, "utf8");
1639
1744
  } catch {
1640
1745
  return void 0;
1641
1746
  }
1642
1747
  try {
1643
- rmSync(path, { force: true });
1748
+ rmSync4(path, { force: true });
1644
1749
  } catch {
1645
1750
  }
1646
- let parsed;
1751
+ let parsed2;
1647
1752
  try {
1648
- parsed = JSON.parse(raw);
1753
+ parsed2 = JSON.parse(raw);
1649
1754
  } catch {
1650
1755
  return void 0;
1651
1756
  }
1652
- if (typeof parsed !== "object" || parsed === null) return void 0;
1653
- const page = parsed.page;
1757
+ if (typeof parsed2 !== "object" || parsed2 === null) return void 0;
1758
+ const page = parsed2.page;
1654
1759
  if (page === void 0 || page === null) return { page: void 0 };
1655
1760
  if (typeof page !== "string") return void 0;
1656
1761
  const id = makePageId(page);
1657
1762
  return id.ok ? { page: id.value } : void 0;
1658
1763
  }
1659
1764
  var QUIT_FILE_NAME = "quit";
1660
- function quitFilePath(defaultFile) {
1661
- return join(dirname(defaultFile), QUIT_FILE_NAME);
1765
+ function quitFilePath(defaultFile, pid) {
1766
+ return paneChannelPath(defaultFile, QUIT_FILE_NAME, pid);
1662
1767
  }
1663
- function takeQuitRequest(defaultFile) {
1664
- const path = quitFilePath(defaultFile);
1768
+ function takeQuitRequest(defaultFile, pid) {
1769
+ const targeted = pid === void 0 ? void 0 : quitFilePath(defaultFile, pid);
1770
+ const path = targeted !== void 0 && existsSync2(targeted) ? targeted : quitFilePath(defaultFile);
1665
1771
  let raw;
1666
1772
  try {
1667
- raw = readFileSync(path, "utf8");
1773
+ raw = readFileSync2(path, "utf8");
1668
1774
  } catch {
1669
1775
  return false;
1670
1776
  }
1671
- sweepQuitRequest(defaultFile);
1672
- let parsed;
1777
+ sweepQuitRequest(defaultFile, path === targeted ? pid : void 0);
1778
+ let parsed2;
1673
1779
  try {
1674
- parsed = JSON.parse(stripBom(raw));
1780
+ parsed2 = JSON.parse(stripBom(raw));
1675
1781
  } catch {
1676
1782
  return false;
1677
1783
  }
1678
- return isRecord2(parsed);
1679
- }
1680
- function sweepQuitRequest(defaultFile) {
1681
- try {
1682
- rmSync(quitFilePath(defaultFile), { force: true });
1683
- } catch {
1684
- }
1685
- }
1686
- var VIEWERS_DIR_NAME = "viewers";
1687
- var VIEWER_FILE_VERSION = 1;
1688
- var VIEWER_HEARTBEAT_MS = 1e3;
1689
- function viewersDirPath(defaultFile) {
1690
- return join(dirname(defaultFile), VIEWERS_DIR_NAME);
1691
- }
1692
- function viewerFilePath(defaultFile, pid) {
1693
- return join(viewersDirPath(defaultFile), `${pid}.json`);
1694
- }
1695
- function publishViewer(defaultFile, pid, report) {
1696
- const body = { version: VIEWER_FILE_VERSION, page: report.page ?? null, follow: report.follow };
1697
- return writeFileAtomic(viewerFilePath(defaultFile, pid), `${JSON.stringify(body, null, 2)}
1698
- `);
1784
+ return isRecord2(parsed2);
1699
1785
  }
1700
- function retireViewer(defaultFile, pid) {
1786
+ function sweepQuitRequest(defaultFile, pid) {
1701
1787
  try {
1702
- rmSync(viewerFilePath(defaultFile, pid), { force: true });
1788
+ rmSync4(quitFilePath(defaultFile, pid), { force: true });
1703
1789
  } catch {
1704
1790
  }
1705
1791
  }
1792
+
1793
+ // src/store/policy.ts
1794
+ import { dirname as dirname5, join as join4 } from "node:path";
1706
1795
  var CONFIG_FILE_NAME = "config.json";
1707
1796
  function configFilePath(defaultFile) {
1708
- return join(dirname(defaultFile), CONFIG_FILE_NAME);
1797
+ return join4(dirname5(defaultFile), CONFIG_FILE_NAME);
1709
1798
  }
1710
- var LEGACY_STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
1799
+
1800
+ // src/store/migration.ts
1801
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync as renameSync2 } from "node:fs";
1802
+ import { dirname as dirname6, join as join5 } from "node:path";
1803
+ var LEGACY_STATE_FILE_RELATIVE_PATH = join5(".claude", "mellos-mapping.json");
1711
1804
  var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
1712
1805
  var LEGACY_CONFIG_FILE_NAME = "mellos-mapping.config.json";
1713
1806
  function migrateLegacyStore(defaultFile) {
1714
- const projectRoot = dirname(dirname(defaultFile));
1715
- const legacyDefault = join(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
1716
- const legacyPages = join(dirname(legacyDefault), LEGACY_PAGES_DIR_NAME);
1717
- const legacyConfig = join(dirname(legacyDefault), LEGACY_CONFIG_FILE_NAME);
1718
- const hasLegacy = existsSync(legacyDefault) || existsSync(legacyPages) || existsSync(legacyConfig);
1719
- const hasCurrent = existsSync(defaultFile) || existsSync(join(dirname(defaultFile), PAGES_DIR_NAME)) || existsSync(configFilePath(defaultFile));
1807
+ const projectRoot = dirname6(dirname6(defaultFile));
1808
+ const legacyDefault = join5(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
1809
+ const legacyPages = join5(dirname6(legacyDefault), LEGACY_PAGES_DIR_NAME);
1810
+ const legacyConfig = join5(dirname6(legacyDefault), LEGACY_CONFIG_FILE_NAME);
1811
+ const hasLegacy = existsSync3(legacyDefault) || existsSync3(legacyPages) || existsSync3(legacyConfig);
1812
+ const hasCurrent = existsSync3(defaultFile) || existsSync3(join5(dirname6(defaultFile), PAGES_DIR_NAME)) || existsSync3(configFilePath(defaultFile));
1720
1813
  if (!hasLegacy || hasCurrent) return false;
1721
- mkdirSync(dirname(defaultFile), { recursive: true });
1722
- if (existsSync(legacyDefault)) renameSync(legacyDefault, defaultFile);
1723
- if (existsSync(legacyPages)) renameSync(legacyPages, join(dirname(defaultFile), PAGES_DIR_NAME));
1724
- if (existsSync(legacyConfig)) renameSync(legacyConfig, configFilePath(defaultFile));
1814
+ mkdirSync2(dirname6(defaultFile), { recursive: true });
1815
+ if (existsSync3(legacyDefault)) renameSync2(legacyDefault, defaultFile);
1816
+ if (existsSync3(legacyPages)) renameSync2(legacyPages, join5(dirname6(defaultFile), PAGES_DIR_NAME));
1817
+ if (existsSync3(legacyConfig)) renameSync2(legacyConfig, configFilePath(defaultFile));
1725
1818
  return true;
1726
1819
  }
1820
+
1821
+ // src/store/maps.ts
1822
+ import { readFileSync as readFileSync3 } from "node:fs";
1727
1823
  function loadMapFile(path) {
1728
1824
  let text;
1729
1825
  try {
1730
- text = readFileSync(path, "utf8");
1826
+ text = readFileSync3(path, "utf8");
1731
1827
  } catch (e) {
1732
1828
  const code = e.code;
1733
1829
  if (code === "ENOENT") return err({ kind: "not-found", path });
@@ -1843,6 +1939,175 @@ function parseInput(chunk) {
1843
1939
  return { events, rest: "" };
1844
1940
  }
1845
1941
 
1942
+ // src/watch/view-state.ts
1943
+ var defaultPage = () => ({ offsetX: 0, offsetY: 0, zoom: ZOOM_DEFAULT, selectedId: void 0 });
1944
+ function initialViewState() {
1945
+ return {
1946
+ ...defaultPage(),
1947
+ pageViews: /* @__PURE__ */ new Map(),
1948
+ hoverId: void 0,
1949
+ dragAnchor: void 0,
1950
+ press: void 0,
1951
+ dividerDrag: false,
1952
+ lastClick: void 0
1953
+ };
1954
+ }
1955
+ function reduceView(state, event) {
1956
+ switch (event.kind) {
1957
+ case "reset":
1958
+ return { ...state, offsetX: 0, offsetY: 0, zoom: ZOOM_DEFAULT };
1959
+ case "pan":
1960
+ return { ...state, offsetX: state.offsetX + event.dx, offsetY: state.offsetY + event.dy };
1961
+ case "position":
1962
+ return { ...state, offsetX: event.x, offsetY: event.y };
1963
+ case "zoom":
1964
+ return { ...state, zoom: event.value };
1965
+ case "hover":
1966
+ return event.id === state.hoverId ? state : { ...state, hoverId: event.id };
1967
+ case "select":
1968
+ return { ...state, selectedId: event.id };
1969
+ case "down":
1970
+ return {
1971
+ ...state,
1972
+ dividerDrag: event.divider,
1973
+ dragAnchor: event.divider ? void 0 : { x: event.x, y: event.y, ox: state.offsetX, oy: state.offsetY },
1974
+ press: event.divider ? void 0 : { moved: false }
1975
+ };
1976
+ case "drag": {
1977
+ if (state.dividerDrag || !state.dragAnchor) return state;
1978
+ const { x, y, ox, oy } = state.dragAnchor;
1979
+ const offsetX = ox - (event.x - x);
1980
+ const offsetY = oy - (event.y - y);
1981
+ return offsetX === state.offsetX && offsetY === state.offsetY ? state : { ...state, offsetX, offsetY, press: { moved: true } };
1982
+ }
1983
+ case "up":
1984
+ return { ...state, dragAnchor: void 0, press: void 0, dividerDrag: false };
1985
+ case "pages": {
1986
+ const pageViews = new Map([...state.pageViews].filter(([file]) => event.files.includes(file)));
1987
+ if (event.previous === event.active || event.active === void 0) return { ...state, pageViews };
1988
+ if (event.previous !== void 0 && event.files.includes(event.previous)) pageViews.set(event.previous, {
1989
+ offsetX: state.offsetX,
1990
+ offsetY: state.offsetY,
1991
+ zoom: state.zoom,
1992
+ selectedId: state.selectedId
1993
+ });
1994
+ return {
1995
+ ...state,
1996
+ ...pageViews.get(event.active) ?? defaultPage(),
1997
+ pageViews,
1998
+ hoverId: void 0,
1999
+ dragAnchor: void 0,
2000
+ press: void 0,
2001
+ dividerDrag: false,
2002
+ lastClick: void 0
2003
+ };
2004
+ }
2005
+ }
2006
+ }
2007
+ function isPointerClick(state) {
2008
+ return !state.dividerDrag && state.press !== void 0 && !state.press.moved;
2009
+ }
2010
+ function clickNode(state, id, now, doubleClickMs) {
2011
+ const double = id !== void 0 && state.lastClick?.id === id && now - state.lastClick.at <= doubleClickMs;
2012
+ return {
2013
+ state: { ...state, selectedId: id, lastClick: double || id === void 0 ? void 0 : { id, at: now } },
2014
+ diveId: double ? id : void 0
2015
+ };
2016
+ }
2017
+
2018
+ // src/watch/io.ts
2019
+ var nativeWatcherIO = () => ({
2020
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
2021
+ input: process.stdin,
2022
+ output: process.stdout
2023
+ });
2024
+
2025
+ // src/watch/terminal-session.ts
2026
+ var HIDE_CURSOR = "\x1B[?25l";
2027
+ var SHOW_CURSOR = "\x1B[?25h";
2028
+ var ENTER_SCREEN = "\x1B[?1049h";
2029
+ var LEAVE_SCREEN = "\x1B[?1049l";
2030
+ var CLEAR_SCREEN = "\x1B[H\x1B[2J";
2031
+ var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
2032
+ var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
2033
+ var RESET = "\x1B[0m";
2034
+ function terminalRestoreSequence(mouseActive, alternateScreen = false) {
2035
+ return (mouseActive ? MOUSE_OFF : "") + RESET + (alternateScreen ? LEAVE_SCREEN : "") + SHOW_CURSOR + (alternateScreen ? "" : "\n");
2036
+ }
2037
+ function openTerminalSession(options) {
2038
+ const { interactive, write } = options;
2039
+ const mouseActive = interactive && options.mouse;
2040
+ write((interactive ? ENTER_SCREEN : "") + HIDE_CURSOR + CLEAR_SCREEN + (mouseActive ? MOUSE_ON : ""));
2041
+ const timer = mouseActive ? setInterval(() => write(HIDE_CURSOR + MOUSE_ON), 1e3) : void 0;
2042
+ timer?.unref();
2043
+ let closed = false;
2044
+ return {
2045
+ close() {
2046
+ if (closed) return;
2047
+ closed = true;
2048
+ clearInterval(timer);
2049
+ write(terminalRestoreSequence(mouseActive, interactive));
2050
+ }
2051
+ };
2052
+ }
2053
+
2054
+ // src/watch/frame-output.ts
2055
+ function frameDifference(previous, next) {
2056
+ const compatible = previous?.columns === next.columns;
2057
+ let output = "";
2058
+ for (let row = 0; row < Math.max(next.rows.length, previous?.rows.length ?? 0); row++) {
2059
+ const text = next.rows[row] ?? "";
2060
+ if (compatible && text === previous?.rows[row]) continue;
2061
+ output += `\x1B[${row + 1};1H${text}\x1B[0m\x1B[K`;
2062
+ }
2063
+ return output;
2064
+ }
2065
+ function createFrameOutput(port) {
2066
+ let previous;
2067
+ let latest;
2068
+ let pending;
2069
+ let unsubscribe;
2070
+ let closed = false;
2071
+ const flush = () => {
2072
+ if (closed || unsubscribe !== void 0 || pending === void 0) return;
2073
+ const frame = pending;
2074
+ pending = void 0;
2075
+ const output = frameDifference(previous, frame);
2076
+ if (output === "") return;
2077
+ previous = frame;
2078
+ if (!port.write(output)) {
2079
+ unsubscribe = port.onDrain(() => {
2080
+ unsubscribe?.();
2081
+ unsubscribe = void 0;
2082
+ flush();
2083
+ });
2084
+ }
2085
+ };
2086
+ const refresh = setInterval(() => {
2087
+ previous = void 0;
2088
+ pending = latest;
2089
+ flush();
2090
+ }, 1e3);
2091
+ refresh.unref();
2092
+ return {
2093
+ present(frame) {
2094
+ if (closed) return;
2095
+ latest = pending = frame;
2096
+ flush();
2097
+ },
2098
+ invalidate() {
2099
+ previous = void 0;
2100
+ },
2101
+ close() {
2102
+ closed = true;
2103
+ clearInterval(refresh);
2104
+ unsubscribe?.();
2105
+ unsubscribe = void 0;
2106
+ pending = latest = previous = void 0;
2107
+ }
2108
+ };
2109
+ }
2110
+
1846
2111
  // src/watch/pane-state.ts
1847
2112
  function describePageFault(fault) {
1848
2113
  return fault.kind === "unreadable" ? `cannot read ${fault.path}: ${fault.detail}` : describeStoreError(fault);
@@ -2011,13 +2276,14 @@ function describeArgsError(e) {
2011
2276
  }
2012
2277
  var USAGE = "usage: mellos-mapping-watch [--file <map.json>] [--page <slug>] [--interval <ms>] [--ascii] [--no-color] [--no-mouse] [--no-follow]";
2013
2278
  function parseArgs(argv, cwd) {
2014
- let file = join2(cwd, STATE_FILE_RELATIVE_PATH);
2279
+ let file = join6(cwd, STATE_FILE_RELATIVE_PATH);
2015
2280
  let intervalMs = POLL_INTERVAL_DEFAULT_MS;
2016
2281
  let unicode = true;
2017
2282
  let color = true;
2018
2283
  let mouse = true;
2019
2284
  let page;
2020
2285
  let follow = true;
2286
+ let owner;
2021
2287
  const valueOf = (flag, raw) => raw === void 0 || raw.startsWith("--") ? err({ kind: "missing-value", flag }) : ok(raw);
2022
2288
  for (let i = 0; i < argv.length; i++) {
2023
2289
  const flag = argv[i];
@@ -2031,9 +2297,17 @@ function parseArgs(argv, cwd) {
2031
2297
  case "--page": {
2032
2298
  const value = valueOf(flag, argv[++i]);
2033
2299
  if (!value.ok) return value;
2034
- const parsed = makePageId(value.value);
2035
- if (!parsed.ok) return err({ kind: "invalid-value", flag, raw: value.value, rule: parsed.error.rule });
2036
- page = parsed.value;
2300
+ const parsed2 = makePageId(value.value);
2301
+ if (!parsed2.ok) return err({ kind: "invalid-value", flag, raw: value.value, rule: parsed2.error.rule });
2302
+ page = parsed2.value;
2303
+ break;
2304
+ }
2305
+ case "--owner": {
2306
+ const value = valueOf(flag, argv[++i]);
2307
+ if (!value.ok) return value;
2308
+ const parsed2 = makePageId(value.value);
2309
+ if (!parsed2.ok) return err({ kind: "invalid-value", flag, raw: value.value, rule: parsed2.error.rule });
2310
+ owner = parsed2.value;
2037
2311
  break;
2038
2312
  }
2039
2313
  case "--interval": {
@@ -2062,7 +2336,7 @@ function parseArgs(argv, cwd) {
2062
2336
  return err({ kind: "unknown-flag", flag });
2063
2337
  }
2064
2338
  }
2065
- return ok({ file, intervalMs, unicode, color, mouse, page, follow });
2339
+ return ok({ file, intervalMs, unicode, color, mouse, page, follow, ...owner === void 0 ? {} : { owner } });
2066
2340
  }
2067
2341
  function readPage(file) {
2068
2342
  try {
@@ -2071,9 +2345,9 @@ function readPage(file) {
2071
2345
  return err({ kind: "unreadable", path: file, detail: e.code ?? e.message });
2072
2346
  }
2073
2347
  }
2074
- function renderWindow(map, opts, viewport) {
2348
+ function renderWindow(map, opts, viewport, render = renderMapWindow) {
2075
2349
  try {
2076
- return ok(renderMapWindow(map, opts, viewport));
2350
+ return ok(render(map, opts, viewport));
2077
2351
  } catch (e) {
2078
2352
  return err(`this map could not be drawn: ${e.message}`);
2079
2353
  }
@@ -2090,17 +2364,10 @@ function dividerRow(width, unicode, follow) {
2090
2364
  }
2091
2365
  return bar;
2092
2366
  }
2093
- var HIDE_CURSOR = "\x1B[?25l";
2094
- var SHOW_CURSOR = "\x1B[?25h";
2095
2367
  var CLEAR_ALL = "\x1B[H\x1B[2J";
2096
2368
  var HOME = "\x1B[H";
2097
2369
  var ERASE_LINE_END = "\x1B[K";
2098
- var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
2099
- var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
2100
- var RESET = "\x1B[0m";
2101
- function terminalRestoreSequence(mouseActive) {
2102
- return (mouseActive ? MOUSE_OFF : "") + SHOW_CURSOR + RESET + "\n";
2103
- }
2370
+ var RESET2 = "\x1B[0m";
2104
2371
  var PANEL_CONTENT_ROWS = 6;
2105
2372
  var PANEL_ROWS_MIN = 2;
2106
2373
  var MAP_ROWS_MIN = 4;
@@ -2291,7 +2558,7 @@ function waitingInfo(s, width) {
2291
2558
  const clock = s.elapsedMs !== void 0 ? ` \xB7 waiting ${elapsedLabel(s.elapsedMs)}` : "";
2292
2559
  const lines = [
2293
2560
  `watching ${s.defaultFile}`,
2294
- ` and ${join2(s.pagesDir, "*.json")}`,
2561
+ ` and ${join6(s.pagesDir, "*.json")}`,
2295
2562
  `polling every ${s.intervalMs} ms${clock}`
2296
2563
  ];
2297
2564
  for (const b of s.broken) lines.push(`! ${b}`);
@@ -2300,7 +2567,7 @@ function waitingInfo(s, width) {
2300
2567
  }
2301
2568
  function splashFrame(notice, info, frame, width, height, unicode, color) {
2302
2569
  if (width < 24 || height < info.length + 3) return void 0;
2303
- const dim = (s) => color ? `\x1B[90m${s}${RESET}` : s;
2570
+ const dim = (s) => color ? `\x1B[90m${s}${RESET2}` : s;
2304
2571
  const spinner = SPINNER_FRAMES[unicode ? "unicode" : "ascii"];
2305
2572
  const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
2306
2573
  const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
@@ -2331,17 +2598,12 @@ function viewerReportOf(pane, defaultFile) {
2331
2598
  const shown = pane.activeFile ?? pane.pendingFocusFile ?? defaultFile;
2332
2599
  return { page: pageIdOfFile(defaultFile, shown), follow: pane.follow };
2333
2600
  }
2334
- function main() {
2335
- const parsed = parseArgs(process.argv.slice(2), process.cwd());
2336
- if (!parsed.ok) {
2337
- console.error(`mellos-mapping-watch: ${describeArgsError(parsed.error)}
2338
- ${USAGE}`);
2339
- process.exit(1);
2340
- }
2341
- const cfg = parsed.value;
2601
+ function runWatcher(cfg, io = nativeWatcherIO()) {
2602
+ const renderScene = createWindowRenderer();
2342
2603
  if (migrateLegacyStore(cfg.file)) console.error("mellos-mapping: moved the legacy .claude map store to .mellos/ \u2014 commit the move.");
2343
2604
  sweepQuitRequest(cfg.file);
2344
- const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
2605
+ sweepQuitRequest(cfg.file, process.pid);
2606
+ const interactive = io.interactive;
2345
2607
  const mouseActive = interactive && cfg.mouse;
2346
2608
  let lastFrame = "";
2347
2609
  let spinnerFrame = 0;
@@ -2350,28 +2612,19 @@ ${USAGE}`);
2350
2612
  const standbyNotice = "waiting for the first mmap_declare ...";
2351
2613
  let map;
2352
2614
  let notice = standbyNotice;
2353
- let lastCols = process.stdout.columns ?? 0;
2354
- let lastRows = process.stdout.rows ?? 0;
2615
+ let lastCols = io.output.columns ?? 0;
2616
+ let lastRows = io.output.rows ?? 0;
2355
2617
  let pane = initialPaneState(
2356
2618
  cfg.follow,
2357
2619
  cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page)
2358
2620
  );
2359
- const pageViews = /* @__PURE__ */ new Map();
2621
+ let view = initialViewState();
2360
2622
  let lastTabSegments = [];
2361
2623
  let tabScroll = 0;
2362
- let offsetX = 0;
2363
- let offsetY = 0;
2364
- let zoom = ZOOM_DEFAULT;
2365
- let dragAnchor;
2366
- let press;
2367
- let hoverId;
2368
- let selectedId;
2369
2624
  let lastHits = [];
2370
2625
  let lastContent = { w: 0, h: 0 };
2371
2626
  let pendingInput = "";
2372
2627
  let panelContentRows = PANEL_CONTENT_ROWS;
2373
- let dividerDrag = false;
2374
- let lastClick;
2375
2628
  let flash;
2376
2629
  let lastTabFiles = [];
2377
2630
  const topFiles = () => topLevelFiles(cfg.file, filesOf(pane), mapsOf(pane));
@@ -2387,8 +2640,8 @@ ${USAGE}`);
2387
2640
  }
2388
2641
  return false;
2389
2642
  };
2390
- const viewWidth = () => usableColumns(process.stdout.columns ?? FALLBACK_COLUMNS);
2391
- const viewHeight = () => Math.max(1, (process.stdout.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
2643
+ const viewWidth = () => usableColumns(io.output.columns ?? FALLBACK_COLUMNS);
2644
+ const viewHeight = () => Math.max(1, (io.output.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
2392
2645
  const dividerY = () => tabRows() + viewHeight() + 1;
2393
2646
  const pageTabsOf = (files) => files.map((f) => {
2394
2647
  const entry = entryOf(pane, f);
@@ -2415,14 +2668,8 @@ ${USAGE}`);
2415
2668
  };
2416
2669
  const adoptView = (previous) => {
2417
2670
  const file = pane.activeFile;
2671
+ view = reduceView(view, { kind: "pages", previous, active: file, files: filesOf(pane) });
2418
2672
  if (file === void 0 || file === previous) return;
2419
- if (previous !== void 0) pageViews.set(previous, { offsetX, offsetY, zoom, selectedId });
2420
- const view = pageViews.get(file);
2421
- offsetX = view?.offsetX ?? 0;
2422
- offsetY = view?.offsetY ?? 0;
2423
- zoom = view?.zoom ?? ZOOM_DEFAULT;
2424
- selectedId = view?.selectedId;
2425
- hoverId = void 0;
2426
2673
  const top = topFiles();
2427
2674
  const tabIndex = top.indexOf(file);
2428
2675
  if (tabIndex >= 0) {
@@ -2444,16 +2691,33 @@ ${USAGE}`);
2444
2691
  const sy = termY - 1 - tabRows();
2445
2692
  if (sx < 0 || sx >= viewWidth()) return void 0;
2446
2693
  if (sy < 0 || sy >= viewHeight()) return void 0;
2447
- const cx = sx + offsetX;
2448
- const cy = sy + offsetY;
2694
+ const cx = sx + view.offsetX;
2695
+ const cy = sy + view.offsetY;
2449
2696
  return lastHits.find((h) => cx >= h.x && cx < h.x + h.w && cy >= h.y && cy < h.y + h.h)?.id;
2450
2697
  };
2451
2698
  const publishPresence = () => {
2452
- publishViewer(cfg.file, process.pid, viewerReportOf(pane, cfg.file));
2699
+ io.report?.(viewerReportOf(pane, cfg.file));
2700
+ publishViewer(cfg.file, process.pid, {
2701
+ ...viewerReportOf(pane, cfg.file),
2702
+ ...cfg.owner === void 0 ? {} : { owner: cfg.owner }
2703
+ });
2453
2704
  };
2454
- process.stdout.write(HIDE_CURSOR + CLEAR_ALL + (mouseActive ? MOUSE_ON : ""));
2705
+ if (interactive) io.input.setRawMode(true);
2706
+ const terminal = openTerminalSession({ interactive, mouse: cfg.mouse, write: (text) => io.output.write(text) });
2707
+ const frameOutput = interactive ? createFrameOutput({
2708
+ write: (text) => io.output.write(text),
2709
+ onDrain: (ready) => {
2710
+ io.output.once("drain", ready);
2711
+ return () => {
2712
+ io.output.off("drain", ready);
2713
+ };
2714
+ }
2715
+ }) : void 0;
2716
+ let paintTimer;
2455
2717
  process.on("exit", () => {
2456
- process.stdout.write(terminalRestoreSequence(mouseActive));
2718
+ clearTimeout(paintTimer);
2719
+ frameOutput?.close();
2720
+ terminal.close();
2457
2721
  retireViewer(cfg.file, process.pid);
2458
2722
  });
2459
2723
  const quit = () => process.exit(0);
@@ -2465,20 +2729,31 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2465
2729
  `);
2466
2730
  process.exit(1);
2467
2731
  });
2732
+ const sceneOptions = () => ({
2733
+ color: cfg.color,
2734
+ unicode: cfg.unicode,
2735
+ zoom: view.zoom,
2736
+ focus: view.hoverId ?? view.selectedId,
2737
+ // A spinner on another page must not invalidate this completed picture.
2738
+ spinnerFrame: map?.nodes.some((node) => node.status === "in-progress") ? spinnerFrame : 0
2739
+ });
2468
2740
  const paint2 = () => {
2469
- const cols = process.stdout.columns ?? FALLBACK_COLUMNS;
2741
+ clearTimeout(paintTimer);
2742
+ paintTimer = void 0;
2743
+ const cols = io.output.columns ?? FALLBACK_COLUMNS;
2470
2744
  const viewW = viewWidth();
2471
- panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ?? FALLBACK_ROWS, tabRows());
2745
+ panelContentRows = clampPanelRows(panelContentRows, io.output.rows ?? FALLBACK_ROWS, tabRows());
2472
2746
  const viewH = viewHeight();
2473
- const focus = hoverId ?? selectedId;
2747
+ const focus = view.hoverId ?? view.selectedId;
2474
2748
  let body;
2475
2749
  let panned = "";
2476
2750
  let pannable = false;
2477
2751
  if (map !== void 0) {
2478
2752
  const rendered = renderWindow(
2479
2753
  map,
2480
- { color: cfg.color, unicode: cfg.unicode, spinnerFrame, focus, zoom },
2481
- { x: offsetX, y: offsetY, width: viewW, height: viewH }
2754
+ sceneOptions(),
2755
+ { x: view.offsetX, y: view.offsetY, width: viewW, height: viewH },
2756
+ renderScene
2482
2757
  );
2483
2758
  if (!rendered.ok) {
2484
2759
  body = ["", fitWidth(` ! ${rendered.error}`, viewW), ""];
@@ -2487,9 +2762,12 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2487
2762
  const windowed = rendered.value;
2488
2763
  const maxX = Math.max(0, windowed.contentWidth - viewW);
2489
2764
  const maxY = Math.max(0, windowed.contentHeight - viewH);
2490
- if (offsetX > maxX || offsetY > maxY || offsetX < 0 || offsetY < 0) {
2491
- offsetX = Math.min(Math.max(0, offsetX), maxX);
2492
- offsetY = Math.min(Math.max(0, offsetY), maxY);
2765
+ if (view.offsetX > maxX || view.offsetY > maxY || view.offsetX < 0 || view.offsetY < 0) {
2766
+ view = reduceView(view, {
2767
+ kind: "position",
2768
+ x: Math.min(Math.max(0, view.offsetX), maxX),
2769
+ y: Math.min(Math.max(0, view.offsetY), maxY)
2770
+ });
2493
2771
  paint2();
2494
2772
  return;
2495
2773
  }
@@ -2497,13 +2775,13 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2497
2775
  body = windowed.lines;
2498
2776
  lastHits = windowed.hits;
2499
2777
  lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
2500
- if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
2778
+ if (view.offsetX !== 0 || view.offsetY !== 0) panned = ` (+${view.offsetX},+${view.offsetY})`;
2501
2779
  }
2502
2780
  } else {
2503
2781
  const info = waitingInfo(
2504
2782
  {
2505
2783
  defaultFile: cfg.file,
2506
- pagesDir: join2(dirname2(cfg.file), PAGES_DIR_NAME),
2784
+ pagesDir: join6(dirname7(cfg.file), PAGES_DIR_NAME),
2507
2785
  intervalMs: cfg.intervalMs,
2508
2786
  elapsedMs: interactive ? Date.now() - startedAt : void 0,
2509
2787
  broken: pane.pages.flatMap(
@@ -2522,15 +2800,15 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2522
2800
  if (map === void 0) {
2523
2801
  panel = Array.from({ length: panelContentRows }, () => ({ text: "", sgr: "" }));
2524
2802
  } else if (focus !== void 0) {
2525
- panel = nodePanel(map, focus, cfg.unicode, panelWidth, selectedId === focus, panelContentRows) ?? mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
2803
+ panel = nodePanel(map, focus, cfg.unicode, panelWidth, view.selectedId === focus, panelContentRows) ?? mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
2526
2804
  } else {
2527
2805
  panel = mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
2528
2806
  }
2529
2807
  const separator = dividerRow(viewW, cfg.unicode, pane.follow);
2530
2808
  const panelRows = [
2531
- cfg.color ? `\x1B[90m${separator}${RESET}` : separator,
2809
+ cfg.color ? `\x1B[90m${separator}${RESET2}` : separator,
2532
2810
  ...panel.map(
2533
- (l) => cfg.color && l.sgr !== "" && l.text !== "" ? ` \x1B[${l.sgr}m${l.text}${RESET}` : ` ${l.text}`
2811
+ (l) => cfg.color && l.sgr !== "" && l.text !== "" ? ` \x1B[${l.sgr}m${l.text}${RESET2}` : ` ${l.text}`
2534
2812
  )
2535
2813
  ];
2536
2814
  let tabLine;
@@ -2552,43 +2830,53 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2552
2830
  action: { kind: "back" }
2553
2831
  };
2554
2832
  lastTabSegments = [head, tail];
2555
- tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
2833
+ tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET2}` : s.text).join("");
2556
2834
  } else if (tabRows() > 0) {
2557
2835
  const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll, mouseActive);
2558
2836
  lastTabSegments = segments;
2559
- tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
2837
+ tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET2}` : s.text).join("");
2560
2838
  } else {
2561
2839
  lastTabSegments = [];
2562
2840
  }
2563
- const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(zoom)}`;
2841
+ const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(view.zoom)}`;
2564
2842
  const hint = !interactive ? cfg.file : (flash !== void 0 ? `${flash.text} \xB7 ` : "") + `${zoomTag} \xB7 wheel zoom \xB7 ` + (pannable ? "drag pan \xB7 " : "") + "hover/click \xB7 0 reset \xB7 x delete page \xB7 q quit";
2565
2843
  const footerText = fitWidth(` ${hint}${panned}`, viewW);
2566
- const footer = cfg.color ? `\x1B[90m${footerText}${RESET}` : footerText;
2567
- let frame = HOME;
2568
- if (tabLine !== void 0) frame += tabLine + ERASE_LINE_END + "\n";
2569
- for (let i = 0; i < viewH; i++) frame += (body[i] ?? "") + ERASE_LINE_END + "\n";
2570
- for (const row of panelRows) frame += row + ERASE_LINE_END + "\n";
2571
- frame += footer + ERASE_LINE_END;
2572
- if (frame !== lastFrame) {
2573
- process.stdout.write(frame);
2574
- lastFrame = frame;
2844
+ const footer = cfg.color ? `\x1B[90m${footerText}${RESET2}` : footerText;
2845
+ const rows = [
2846
+ ...tabLine === void 0 ? [] : [tabLine],
2847
+ ...Array.from({ length: viewH }, (_, i) => body[i] ?? ""),
2848
+ ...panelRows,
2849
+ footer
2850
+ ];
2851
+ if (frameOutput !== void 0) {
2852
+ frameOutput.present({ columns: cols, rows });
2853
+ } else {
2854
+ const frame = HOME + rows.map((row) => row + ERASE_LINE_END).join("\n");
2855
+ if (frame !== lastFrame) {
2856
+ io.output.write(frame);
2857
+ lastFrame = frame;
2858
+ }
2575
2859
  }
2576
2860
  };
2861
+ const requestPaint = () => {
2862
+ paintTimer ??= setTimeout(paint2, 16);
2863
+ };
2577
2864
  const handleResize = () => {
2578
- lastCols = process.stdout.columns ?? lastCols;
2579
- lastRows = process.stdout.rows ?? lastRows;
2865
+ lastCols = io.output.columns ?? lastCols;
2866
+ lastRows = io.output.rows ?? lastRows;
2580
2867
  lastFrame = "";
2581
- process.stdout.write(CLEAR_ALL);
2868
+ frameOutput?.invalidate();
2869
+ io.output.write(CLEAR_ALL);
2582
2870
  paint2();
2583
2871
  };
2584
2872
  const tick = () => {
2585
- if (takeQuitRequest(cfg.file)) quit();
2586
- if ((process.stdout.columns ?? lastCols) !== lastCols || (process.stdout.rows ?? lastRows) !== lastRows) {
2873
+ if (takeQuitRequest(cfg.file, process.pid)) quit();
2874
+ if ((io.output.columns ?? lastCols) !== lastCols || (io.output.rows ?? lastRows) !== lastRows) {
2587
2875
  handleResize();
2588
2876
  }
2589
2877
  const discovered = listPageFiles(cfg.file);
2590
2878
  const files = discovered.length > 0 ? discovered : [cfg.file];
2591
- const request = takeFocusRequest(cfg.file);
2879
+ const request = takeFocusRequest(cfg.file, process.pid);
2592
2880
  const previous = pane.activeFile;
2593
2881
  const scanned = scan(pane, {
2594
2882
  files,
@@ -2603,12 +2891,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2603
2891
  focusRequest: request === void 0 ? void 0 : pageFilePath(cfg.file, request.page),
2604
2892
  // a drag in progress holds auto-follow off: the user is engaged with
2605
2893
  // THIS page, and a missed switch is re-triggered by the next save
2606
- engaged: dragAnchor !== void 0
2894
+ engaged: view.dragAnchor !== void 0
2607
2895
  });
2608
2896
  pane = scanned.state;
2609
- for (const known of [...pageViews.keys()]) {
2610
- if (!files.includes(known)) pageViews.delete(known);
2611
- }
2612
2897
  adoptView(previous);
2613
2898
  adoptPage();
2614
2899
  if (flash?.confirm === true && pane.pendingDelete === void 0) flash = void 0;
@@ -2649,10 +2934,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2649
2934
  tick();
2650
2935
  };
2651
2936
  if (interactive) {
2652
- process.stdin.setRawMode(true);
2653
- process.stdin.resume();
2654
- process.stdin.setEncoding("utf8");
2655
- process.stdin.on("data", (chunk) => {
2937
+ io.input.resume();
2938
+ io.input.setEncoding("utf8");
2939
+ io.input.on("data", (chunk) => {
2656
2940
  const parsed2 = parseInput(pendingInput + chunk);
2657
2941
  pendingInput = parsed2.rest;
2658
2942
  let dirty = false;
@@ -2662,22 +2946,19 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2662
2946
  quit();
2663
2947
  return;
2664
2948
  case "reset":
2665
- offsetX = 0;
2666
- offsetY = 0;
2667
- zoom = ZOOM_DEFAULT;
2949
+ view = reduceView(view, { kind: "reset" });
2668
2950
  dirty = true;
2669
2951
  break;
2670
2952
  case "clear":
2671
2953
  if (pane.pendingDelete !== void 0) {
2672
2954
  pane = disarmDelete(pane);
2673
2955
  flash = void 0;
2674
- } else if (selectedId !== void 0) selectedId = void 0;
2956
+ } else if (view.selectedId !== void 0) view = reduceView(view, { kind: "select", id: void 0 });
2675
2957
  else climbBack();
2676
2958
  dirty = true;
2677
2959
  break;
2678
2960
  case "pan":
2679
- offsetX += event.dx;
2680
- offsetY += event.dy;
2961
+ view = reduceView(view, event);
2681
2962
  dirty = true;
2682
2963
  break;
2683
2964
  case "zoom": {
@@ -2686,15 +2967,16 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2686
2967
  dirty = true;
2687
2968
  break;
2688
2969
  }
2689
- const next = clampZoom(zoom + event.delta);
2690
- if (next === zoom || map === void 0) break;
2691
- const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + viewWidth() / 2, offsetY + viewHeight() / 2)?.id;
2970
+ const next = clampZoom(view.zoom + event.delta);
2971
+ if (next === view.zoom || map === void 0) break;
2972
+ const anchorId = view.hoverId ?? view.selectedId ?? nearestHit(lastHits, view.offsetX + viewWidth() / 2, view.offsetY + viewHeight() / 2)?.id;
2692
2973
  const before = lastHits.find((h) => h.id === anchorId);
2693
- zoom = next;
2974
+ view = reduceView(view, { kind: "zoom", value: next });
2694
2975
  const measured = renderWindow(
2695
2976
  map,
2696
- { color: false, unicode: cfg.unicode, spinnerFrame: 0, zoom },
2697
- { x: 0, y: 0, width: 0, height: 0 }
2977
+ sceneOptions(),
2978
+ { x: 0, y: 0, width: 0, height: 0 },
2979
+ renderScene
2698
2980
  );
2699
2981
  if (!measured.ok) {
2700
2982
  dirty = true;
@@ -2704,57 +2986,48 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2704
2986
  const after = before === void 0 ? void 0 : sized.hits.find((h) => h.id === before.id);
2705
2987
  const moved = anchorOffsets(
2706
2988
  before !== void 0 && after !== void 0 ? { before, after } : void 0,
2707
- { x: offsetX, y: offsetY },
2989
+ { x: view.offsetX, y: view.offsetY },
2708
2990
  lastContent,
2709
2991
  { w: sized.contentWidth, h: sized.contentHeight }
2710
2992
  );
2711
- offsetX = moved.x;
2712
- offsetY = moved.y;
2993
+ view = reduceView(view, { kind: "position", ...moved });
2994
+ lastHits = sized.hits;
2995
+ lastContent = { w: sized.contentWidth, h: sized.contentHeight };
2713
2996
  dirty = true;
2714
2997
  break;
2715
2998
  }
2716
2999
  case "mouse-move": {
2717
3000
  const over = hitTest(event.x, event.y);
2718
- if (over !== hoverId) {
2719
- hoverId = over;
3001
+ if (over !== view.hoverId) {
3002
+ view = reduceView(view, { kind: "hover", id: over });
2720
3003
  dirty = true;
2721
3004
  }
2722
3005
  break;
2723
3006
  }
2724
3007
  case "mouse-down":
2725
- if (event.y === dividerY()) {
2726
- dividerDrag = true;
2727
- break;
2728
- }
2729
- dragAnchor = { x: event.x, y: event.y, ox: offsetX, oy: offsetY };
2730
- press = { moved: false };
3008
+ view = reduceView(view, { kind: "down", x: event.x, y: event.y, divider: event.y === dividerY() });
2731
3009
  break;
2732
3010
  case "mouse-drag":
2733
- if (dividerDrag) {
2734
- const next = panelRowsFromDividerY(event.y, process.stdout.rows ?? FALLBACK_ROWS, tabRows());
3011
+ if (view.dividerDrag) {
3012
+ const next = panelRowsFromDividerY(event.y, io.output.rows ?? FALLBACK_ROWS, tabRows());
2735
3013
  if (next !== panelContentRows) {
2736
3014
  panelContentRows = next;
2737
3015
  dirty = true;
2738
3016
  }
2739
3017
  break;
2740
3018
  }
2741
- if (dragAnchor) {
2742
- const nx = dragAnchor.ox - (event.x - dragAnchor.x);
2743
- const ny = dragAnchor.oy - (event.y - dragAnchor.y);
2744
- if (nx !== offsetX || ny !== offsetY) {
2745
- offsetX = nx;
2746
- offsetY = ny;
2747
- if (press) press.moved = true;
2748
- dirty = true;
2749
- }
3019
+ {
3020
+ const nextView = reduceView(view, { kind: "drag", x: event.x, y: event.y });
3021
+ dirty ||= nextView !== view;
3022
+ view = nextView;
2750
3023
  }
2751
3024
  break;
2752
3025
  case "mouse-up":
2753
- if (dividerDrag) {
2754
- dividerDrag = false;
3026
+ if (view.dividerDrag) {
3027
+ view = reduceView(view, { kind: "up" });
2755
3028
  break;
2756
3029
  }
2757
- if (press && !press.moved) {
3030
+ if (isPointerClick(view)) {
2758
3031
  const tabHit = tabRows() > 0 && event.y === 1 ? lastTabSegments.find((s) => event.x >= s.lo && event.x <= s.hi) : void 0;
2759
3032
  if (tabHit !== void 0) {
2760
3033
  if (tabHit.action.kind === "back") {
@@ -2770,7 +3043,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2770
3043
  } else {
2771
3044
  const id = hitTest(event.x, event.y);
2772
3045
  const now = Date.now();
2773
- if (id !== void 0 && lastClick?.id === id && now - lastClick.at <= DOUBLE_CLICK_MS) {
3046
+ const clicked = clickNode(view, id, now, DOUBLE_CLICK_MS);
3047
+ view = clicked.state;
3048
+ if (clicked.diveId !== void 0) {
2774
3049
  const submap = map?.nodes.find((n) => n.id === id)?.submap;
2775
3050
  if (submap !== void 0 && pane.activeFile !== void 0) {
2776
3051
  const target = pageFilePath(cfg.file, submap);
@@ -2782,16 +3057,11 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2782
3057
  flash = { text: `submap "${submap}" has no page yet`, until: now + FLASH_ACK_MS };
2783
3058
  }
2784
3059
  }
2785
- lastClick = void 0;
2786
- } else {
2787
- lastClick = id !== void 0 ? { id, at: now } : void 0;
2788
3060
  }
2789
- selectedId = id;
2790
3061
  }
2791
3062
  dirty = true;
2792
3063
  }
2793
- dragAnchor = void 0;
2794
- press = void 0;
3064
+ view = reduceView(view, { kind: "up" });
2795
3065
  break;
2796
3066
  case "next-page":
2797
3067
  case "prev-page": {
@@ -2829,9 +3099,13 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2829
3099
  break;
2830
3100
  }
2831
3101
  }
2832
- if (dirty) paint2();
3102
+ if (dirty) {
3103
+ const motionOnly = parsed2.events.every((event) => event.kind === "mouse-move" || event.kind === "mouse-drag");
3104
+ if (motionOnly) requestPaint();
3105
+ else paint2();
3106
+ }
2833
3107
  });
2834
- process.stdout.on("resize", handleResize);
3108
+ io.output.on("resize", handleResize);
2835
3109
  }
2836
3110
  tick();
2837
3111
  setInterval(tick, cfg.intervalMs);
@@ -2845,43 +3119,12 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2845
3119
  }, SPLASH_FRAME_MS);
2846
3120
  }
2847
3121
  }
2848
- function launchedAsEntry(argv1, moduleUrl) {
2849
- if (argv1 === void 0) return false;
2850
- try {
2851
- return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
2852
- } catch {
2853
- return pathToFileURL(argv1).href === moduleUrl;
2854
- }
2855
- }
2856
- if (launchedAsEntry(process.argv[1], import.meta.url)) {
2857
- main();
2858
- }
2859
- export {
2860
- PANEL_ROWS_MIN,
2861
- USAGE,
2862
- anchorOffsets,
2863
- clampPanelRows,
2864
- describeArgsError,
2865
- describePageFault,
2866
- diveOrigin,
2867
- dividerRow,
2868
- elapsedLabel,
2869
- fitWidth,
2870
- launchedAsEntry,
2871
- mapPanel,
2872
- nearestHit,
2873
- nodePanel,
2874
- pageTabRow,
2875
- panelRowsFromDividerY,
2876
- parseArgs,
2877
- readPage,
2878
- renderWindow,
2879
- splashFrame,
2880
- tabScrollFor,
2881
- terminalRestoreSequence,
2882
- topLevelFiles,
2883
- usableColumns,
2884
- viewerReportOf,
2885
- waitingInfo,
2886
- wrapWidth
2887
- };
3122
+
3123
+ // src/watch/cli.ts
3124
+ var parsed = parseArgs(process.argv.slice(2), process.cwd());
3125
+ if (!parsed.ok) {
3126
+ console.error(`mellos-mapping-watch: ${describeArgsError(parsed.error)}
3127
+ ${USAGE}`);
3128
+ process.exit(1);
3129
+ }
3130
+ runWatcher(parsed.value);