mellos-mapping 0.19.0 → 0.20.2

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 (43) hide show
  1. package/README.md +366 -56
  2. package/README.zh-CN.md +319 -47
  3. package/dist/hook-session-start.mjs +239 -0
  4. package/dist/mmap.mjs +338 -0
  5. package/dist/server.mjs +1737 -897
  6. package/dist/store-paths.mjs +107 -0
  7. package/dist/watch.mjs +1612 -902
  8. package/lib/domain/ops.d.ts +171 -0
  9. package/lib/domain/ops.js +384 -0
  10. package/lib/domain/types.d.ts +283 -0
  11. package/lib/domain/types.js +153 -0
  12. package/lib/render/canvas.d.ts +50 -0
  13. package/lib/render/canvas.js +210 -0
  14. package/lib/render/draw.d.ts +37 -0
  15. package/lib/render/draw.js +111 -0
  16. package/lib/render/layout.d.ts +89 -0
  17. package/lib/render/layout.js +200 -0
  18. package/lib/render/options.d.ts +39 -0
  19. package/lib/render/options.js +10 -0
  20. package/lib/render/render.d.ts +88 -0
  21. package/lib/render/render.js +128 -0
  22. package/lib/render/routing.d.ts +56 -0
  23. package/lib/render/routing.js +244 -0
  24. package/lib/render/skins.d.ts +54 -0
  25. package/lib/render/skins.js +99 -0
  26. package/lib/render/width.d.ts +24 -0
  27. package/lib/render/width.js +139 -0
  28. package/lib/render/zoom-geometry.d.ts +52 -0
  29. package/lib/render/zoom-geometry.js +56 -0
  30. package/lib/semantics/semantics.d.ts +169 -0
  31. package/lib/semantics/semantics.js +380 -0
  32. package/lib/semantics/vocabulary.d.ts +79 -0
  33. package/lib/semantics/vocabulary.js +112 -0
  34. package/lib/store/format.d.ts +67 -0
  35. package/lib/store/format.js +334 -0
  36. package/lib/store/store.d.ts +296 -0
  37. package/lib/store/store.js +734 -0
  38. package/package.json +41 -5
  39. package/scripts/codex-register.mjs +89 -20
  40. package/scripts/install-mmap-command.mjs +293 -0
  41. package/scripts/mmap.mjs +213 -0
  42. package/scripts/open-pane.mjs +115 -254
  43. package/scripts/pane-core.mjs +418 -0
package/dist/watch.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
3
3
 
4
4
  // src/watch/watch.ts
5
- import { realpathSync, statSync } from "node:fs";
5
+ import { realpathSync, statSync as statSync2 } from "node:fs";
6
6
  import { dirname as dirname2, join as join2 } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
 
@@ -29,6 +29,12 @@ function makeNodeKind(raw) {
29
29
  function makeSubmapRef(raw) {
30
30
  return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
31
31
  }
32
+ var RANK_MIN = 0;
33
+ var RANK_MAX = 99;
34
+ var RANK_RULE_TEXT = `an integer in ${RANK_MIN}..${RANK_MAX}, 0 = bottom / most primitive`;
35
+ function makeRank(raw) {
36
+ return Number.isInteger(raw) && raw >= RANK_MIN && raw <= RANK_MAX ? ok(raw) : err({ kind: "invalid-rank", raw, rule: RANK_RULE_TEXT });
37
+ }
32
38
  var MAP_KINDS = ["dev", "architecture", "dataflow", "behavior-tree", "sequence"];
33
39
  function makeMapKind(raw) {
34
40
  return MAP_KINDS.includes(raw) ? ok(raw) : err({ kind: "invalid-map-kind", raw });
@@ -42,6 +48,8 @@ function describeMapError(e) {
42
48
  switch (e.kind) {
43
49
  case "invalid-id":
44
50
  return `invalid id "${e.raw}" (rule: ${e.rule})`;
51
+ case "invalid-rank":
52
+ return `invalid rank ${e.raw} (rule: ${e.rule})`;
45
53
  case "invalid-status":
46
54
  return `invalid status "${e.raw}" (expected: ${NODE_STATUSES.join(" | ")})`;
47
55
  case "duplicate-layer":
@@ -62,6 +70,8 @@ function describeMapError(e) {
62
70
  return `node "${e.id}" cannot depend on itself`;
63
71
  case "duplicate-group":
64
72
  return `group "${e.id}" already exists`;
73
+ case "id-collision":
74
+ return `id "${e.id}" already names a ${e.taken} on this map; nodes and groups share one id namespace (both render as boxes, so one id must mean one box) \u2014 rename "${e.id}"`;
65
75
  case "unknown-group":
66
76
  return `group "${e.id}" does not exist`;
67
77
  case "invalid-map-kind":
@@ -73,9 +83,9 @@ function describeMapError(e) {
73
83
  case "group-layer-mismatch":
74
84
  return `node "${e.node}" (layer ${e.nodeLayer}) cannot join group "${e.group}" (layer ${e.groupLayer}); groups cluster nodes within one band`;
75
85
  case "layer-not-empty":
76
- return `layer "${e.id}" still holds node "${e.occupant}"; move or remove its nodes first`;
86
+ return `layer "${e.id}" still holds node "${e.occupant}"; move its nodes to another band (moveNode) or remove them (removeNode) first`;
77
87
  case "layer-holds-group":
78
- return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups first`;
88
+ return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups (removeGroup) first`;
79
89
  case "edge-not-downward":
80
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`;
81
91
  }
@@ -101,7 +111,15 @@ function checkMembership(map, node, nodeLayer, group) {
101
111
  function hasEdge(map, from, to) {
102
112
  return map.edges.some((e) => e.from === from && e.to === to);
103
113
  }
114
+ function checkIdSpace(map, id, declaring) {
115
+ const taken = declaring === "node" ? map.groups.some((g) => g.id === id) : map.nodes.some((n) => n.id === id);
116
+ return taken ? { kind: "id-collision", id, taken: declaring === "node" ? "group" : "node" } : void 0;
117
+ }
104
118
  function setTitle(map, title) {
119
+ if (title === null || title === void 0) {
120
+ const { title: _dropped, ...rest } = map;
121
+ return rest;
122
+ }
105
123
  return { ...map, title };
106
124
  }
107
125
  function setKind(map, kind) {
@@ -122,6 +140,8 @@ function declareLayer(map, input) {
122
140
  }
123
141
  function declareGroup(map, input) {
124
142
  if (findGroup(map, input.id)) return err({ kind: "duplicate-group", id: input.id });
143
+ const collision = checkIdSpace(map, input.id, "group");
144
+ if (collision) return err(collision);
125
145
  if (!findLayer(map, input.layer)) return err({ kind: "unknown-layer", id: input.layer });
126
146
  return ok({ ...map, groups: [...map.groups, { id: input.id, label: input.label, layer: input.layer }] });
127
147
  }
@@ -139,6 +159,8 @@ function mapStatus(map) {
139
159
  }
140
160
  function declareNode(map, input) {
141
161
  if (findNode(map, input.id)) return err({ kind: "duplicate-node", id: input.id });
162
+ const collision = checkIdSpace(map, input.id, "node");
163
+ if (collision) return err(collision);
142
164
  if (!findLayer(map, input.layer)) return err({ kind: "unknown-layer", id: input.layer });
143
165
  if (input.group !== void 0) {
144
166
  const bad = checkMembership(map, input.id, input.layer, input.group);
@@ -150,6 +172,7 @@ function declareNode(map, input) {
150
172
  label: input.label,
151
173
  layer: input.layer,
152
174
  status: input.status ?? "planned",
175
+ ...input.evidence !== void 0 ? { evidence: input.evidence } : {},
153
176
  ...input.detail !== void 0 ? { detail: input.detail } : {},
154
177
  ...input.group !== void 0 ? { group: input.group } : {},
155
178
  ...input.kind !== void 0 ? { kind: input.kind } : {},
@@ -170,6 +193,9 @@ function linkNodes(map, from, to, label) {
170
193
  if (fromRank <= toRank) return err({ kind: "edge-not-downward", from, fromRank, to, toRank });
171
194
  return ok({ ...map, edges: [...map.edges, { from, to, ...label !== void 0 ? { label } : {} }] });
172
195
  }
196
+ function resolveOptional(input, current) {
197
+ return input === void 0 ? current : input === null ? void 0 : input;
198
+ }
173
199
  function updateNode(map, input) {
174
200
  const node = findNode(map, input.id);
175
201
  if (!node) return err({ kind: "unknown-node", id: input.id });
@@ -180,32 +206,91 @@ function updateNode(map, input) {
180
206
  if (input.lane !== void 0 && input.lane !== null && !findLane(map, input.lane)) {
181
207
  return err({ kind: "unknown-lane", id: input.lane });
182
208
  }
183
- const { group: currentGroup, kind: currentKind, lane: currentLane, submap: currentSubmap, ...bare } = node;
184
- const nextGroup = input.group === void 0 ? currentGroup : input.group === null ? void 0 : input.group;
185
- const nextKind = input.kind === void 0 ? currentKind : input.kind === null ? void 0 : input.kind;
186
- const nextLane = input.lane === void 0 ? currentLane : input.lane === null ? void 0 : input.lane;
187
- const nextSubmap = input.submap === void 0 ? currentSubmap : input.submap === null ? void 0 : input.submap;
209
+ const {
210
+ group: currentGroup,
211
+ kind: currentKind,
212
+ lane: currentLane,
213
+ submap: currentSubmap,
214
+ evidence: currentEvidence,
215
+ detail: currentDetail,
216
+ ...bare
217
+ } = node;
218
+ const nextGroup = resolveOptional(input.group, currentGroup);
219
+ const nextKind = resolveOptional(input.kind, currentKind);
220
+ const nextLane = resolveOptional(input.lane, currentLane);
221
+ const nextSubmap = resolveOptional(input.submap, currentSubmap);
222
+ const nextEvidence = resolveOptional(input.evidence, currentEvidence);
223
+ const nextDetail = resolveOptional(input.detail, currentDetail);
188
224
  const updated = {
189
225
  ...bare,
226
+ ...nextEvidence !== void 0 ? { evidence: nextEvidence } : {},
227
+ ...nextDetail !== void 0 ? { detail: nextDetail } : {},
190
228
  ...nextGroup !== void 0 ? { group: nextGroup } : {},
191
229
  ...nextKind !== void 0 ? { kind: nextKind } : {},
192
230
  ...nextLane !== void 0 ? { lane: nextLane } : {},
193
231
  ...nextSubmap !== void 0 ? { submap: nextSubmap } : {},
194
232
  ...input.status !== void 0 ? { status: input.status } : {},
195
- ...input.label !== void 0 ? { label: input.label } : {},
196
- ...input.evidence !== void 0 ? { evidence: input.evidence } : {},
197
- ...input.detail !== void 0 ? { detail: input.detail } : {}
233
+ ...input.label !== void 0 ? { label: input.label } : {}
198
234
  };
199
235
  return ok({ ...map, nodes: map.nodes.map((n) => n.id === input.id ? updated : n) });
200
236
  }
201
237
 
202
- // src/render/render.ts
238
+ // src/semantics/vocabulary.ts
239
+ var STATUS_GLYPHS = {
240
+ planned: ["\xB7", "."],
241
+ "in-progress": ["\u283F", "*"],
242
+ done: ["\u25A0", "#"],
243
+ regressed: ["\u2717", "X"]
244
+ };
245
+ function statusGlyph(status, unicode) {
246
+ const [uni, ascii] = STATUS_GLYPHS[status];
247
+ return unicode ? uni : ascii;
248
+ }
249
+ var UNVERIFIED_DONE_GLYPHS = ["\u25A1", "o"];
250
+ function unverifiedDoneGlyph(unicode) {
251
+ const [uni, ascii] = UNVERIFIED_DONE_GLYPHS;
252
+ return unicode ? uni : ascii;
253
+ }
254
+ var SPINNER_FRAMES = {
255
+ unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
256
+ ascii: ["|", "/", "-", "\\"]
257
+ };
258
+ function spinnerGlyph(frame, unicode) {
259
+ const frames = SPINNER_FRAMES[unicode ? "unicode" : "ascii"];
260
+ return frames[(frame % frames.length + frames.length) % frames.length];
261
+ }
262
+ var NODE_KIND_GLYPHS = {
263
+ selector: ["?", "?"],
264
+ sequence: ["\xBB", ">"],
265
+ parallel: ["\u2016", "="],
266
+ decorator: ["\u25CC", "o"],
267
+ condition: ["\u25C7", "c"],
268
+ action: ["\xB7", "."],
269
+ source: ["\u25CB", "o"],
270
+ transform: ["\u25D0", "%"],
271
+ sink: ["\u25CF", "*"],
272
+ service: ["\u25C6", "S"],
273
+ db: ["\u25A4", "D"],
274
+ queue: ["\u2263", "Q"],
275
+ ui: ["\u25A3", "U"]
276
+ };
277
+ function kindGlyph(kind, unicode) {
278
+ const pair = NODE_KIND_GLYPHS[kind];
279
+ return pair === void 0 ? void 0 : unicode ? pair[0] : pair[1];
280
+ }
281
+
282
+ // src/semantics/semantics.ts
203
283
  var ZOOM_MIN = -4;
204
284
  var ZOOM_MAX = 2;
205
285
  var ZOOM_DEFAULT = 0;
206
286
  function clampZoom(n) {
207
287
  return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.round(n)));
208
288
  }
289
+ function zoomMode(zoom) {
290
+ if (zoom >= 1) return "detail";
291
+ if (zoom <= -4) return "overview";
292
+ return "boxes";
293
+ }
209
294
  function zoomLabel(zoom) {
210
295
  switch (zoom) {
211
296
  case 2:
@@ -224,9 +309,216 @@ function zoomLabel(zoom) {
224
309
  return "overview";
225
310
  }
226
311
  }
312
+ function isNeutralKind(map) {
313
+ return map.kind !== void 0 && map.kind !== "dev";
314
+ }
315
+ function aggregateMap(map) {
316
+ if (map.groups.length === 0) return void 0;
317
+ const representative = /* @__PURE__ */ new Map();
318
+ for (const n of map.nodes) representative.set(n.id, n.group ?? n.id);
319
+ const nodes = map.groups.map((g) => {
320
+ const members = map.nodes.filter((n) => n.group === g.id);
321
+ const done = members.filter((n) => n.status === "done").length;
322
+ return {
323
+ id: g.id,
324
+ // neutral kinds document structure, not progress — no member counts
325
+ label: isNeutralKind(map) ? g.label : `${g.label} ${done}/${members.length}`,
326
+ layer: g.layer,
327
+ status: groupStatus(map, g.id)
328
+ };
329
+ });
330
+ for (const n of map.nodes) if (n.group === void 0) nodes.push(n);
331
+ const seen = /* @__PURE__ */ new Set();
332
+ const edges = [];
333
+ for (const e of map.edges) {
334
+ const from = representative.get(e.from);
335
+ const to = representative.get(e.to);
336
+ if (from === to || seen.has(`${from}->${to}`)) continue;
337
+ seen.add(`${from}->${to}`);
338
+ edges.push({ from, to });
339
+ }
340
+ return {
341
+ ...map.title !== void 0 ? { title: map.title } : {},
342
+ ...map.kind !== void 0 ? { kind: map.kind } : {},
343
+ layers: map.layers,
344
+ groups: [],
345
+ lanes: map.lanes,
346
+ nodes,
347
+ edges
348
+ };
349
+ }
350
+ function focusInfo(map, focusId) {
351
+ const layerNameOf = (layerId) => map.layers.find((l) => l.id === layerId)?.name ?? layerId;
352
+ const group = map.groups.find((g) => g.id === focusId);
353
+ if (group) {
354
+ const members = map.nodes.filter((n) => n.group === group.id);
355
+ const memberIds = new Set(members.map((n) => n.id));
356
+ const rep = (id) => {
357
+ const n = map.nodes.find((x) => x.id === id);
358
+ const owner = n.group !== void 0 ? map.groups.find((g) => g.id === n.group) : void 0;
359
+ return owner !== void 0 ? { id: owner.id, label: owner.label, status: groupStatus(map, owner.id) } : { id: n.id, label: n.label, status: n.status };
360
+ };
361
+ const dedupe = (refs) => {
362
+ const seen = /* @__PURE__ */ new Set();
363
+ const out = [];
364
+ for (const r of refs) {
365
+ if (seen.has(r.id)) continue;
366
+ seen.add(r.id);
367
+ out.push(r);
368
+ }
369
+ return out;
370
+ };
371
+ return {
372
+ kind: "group",
373
+ group,
374
+ status: groupStatus(map, group.id),
375
+ layerName: layerNameOf(group.layer),
376
+ members,
377
+ uses: dedupe(
378
+ map.edges.filter((e) => memberIds.has(e.from) && !memberIds.has(e.to)).map((e) => rep(e.to))
379
+ ),
380
+ usedBy: dedupe(
381
+ map.edges.filter((e) => memberIds.has(e.to) && !memberIds.has(e.from)).map((e) => rep(e.from))
382
+ )
383
+ };
384
+ }
385
+ const node = map.nodes.find((n) => n.id === focusId);
386
+ if (!node) return void 0;
387
+ const ref = (id, edgeLabel) => {
388
+ const n = map.nodes.find((x) => x.id === id);
389
+ return {
390
+ id,
391
+ label: n?.label ?? id,
392
+ status: n?.status ?? "planned",
393
+ ...edgeLabel !== void 0 ? { edgeLabel } : {}
394
+ };
395
+ };
396
+ const laneLabel = node.lane !== void 0 ? map.lanes.find((l) => l.id === node.lane)?.label : void 0;
397
+ return {
398
+ kind: "node",
399
+ node,
400
+ layerName: layerNameOf(node.layer),
401
+ ...laneLabel !== void 0 ? { laneLabel } : {},
402
+ uses: map.edges.filter((e) => e.from === node.id).map((e) => ref(e.to, e.label)),
403
+ usedBy: map.edges.filter((e) => e.to === node.id).map((e) => ref(e.from, e.label))
404
+ };
405
+ }
406
+ function interiorPages(pages) {
407
+ const dives = /* @__PURE__ */ new Map();
408
+ const divedIntoBy = /* @__PURE__ */ new Map();
409
+ for (const [slug, map] of pages) {
410
+ const targets = /* @__PURE__ */ new Set();
411
+ for (const n of map?.nodes ?? []) {
412
+ const target = n.submap;
413
+ if (target === void 0 || target === slug) continue;
414
+ targets.add(target);
415
+ const sources = divedIntoBy.get(target) ?? /* @__PURE__ */ new Set();
416
+ sources.add(slug);
417
+ divedIntoBy.set(target, sources);
418
+ }
419
+ if (slug !== void 0) dives.set(slug, targets);
420
+ }
421
+ const reachableFrom = (start) => {
422
+ const seen = /* @__PURE__ */ new Set();
423
+ const pending = [start];
424
+ while (pending.length > 0) {
425
+ for (const target of dives.get(pending.pop()) ?? []) {
426
+ if (seen.has(target)) continue;
427
+ seen.add(target);
428
+ pending.push(target);
429
+ }
430
+ }
431
+ return seen;
432
+ };
433
+ const interior = /* @__PURE__ */ new Set();
434
+ for (const [target, sources] of divedIntoBy) {
435
+ const outward = reachableFrom(target);
436
+ for (const source of sources) {
437
+ if (source === void 0 || !outward.has(source)) {
438
+ interior.add(target);
439
+ break;
440
+ }
441
+ }
442
+ }
443
+ return interior;
444
+ }
445
+ function diveParent(entries, pageId) {
446
+ for (const [key, m] of entries) {
447
+ const node = m?.nodes.find((n) => n.submap === pageId);
448
+ if (node !== void 0) return { parent: key, label: node.label };
449
+ }
450
+ return void 0;
451
+ }
452
+ function mostRecentKey(keys, mtimeOf2) {
453
+ let best;
454
+ let bestMtime = -Infinity;
455
+ for (const key of keys) {
456
+ const mtime = mtimeOf2(key);
457
+ if (mtime !== void 0 && mtime > bestMtime) {
458
+ best = key;
459
+ bestMtime = mtime;
460
+ }
461
+ }
462
+ return best ?? keys[0];
463
+ }
464
+ function flipForSequence(map) {
465
+ if (map.kind !== "sequence") return map;
466
+ return {
467
+ ...map,
468
+ // VIOLATION: state-explicit-in-types - `-l.rank as Rank` produces a value
469
+ // the Rank brand promises cannot exist: mirroring 0..99 gives -99..0, and
470
+ // makeRank would refuse every one of them. The alternative is a second
471
+ // ordered-position type (an unbranded `order` field) threaded through the
472
+ // renderer's whole layout stage purely so this one derived map can be
473
+ // typed — a large change to express "these ranks are an order, not a
474
+ // stored value". What makes it safe is the same thing that makes it
475
+ // wrong: this map only ever reaches a renderer, which compares ranks and
476
+ // never writes them (same contract as aggregateMap).
477
+ layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
478
+ edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
479
+ };
480
+ }
481
+
482
+ // src/render/width.ts
227
483
  var WIDE_RANGES = [
228
484
  [4352, 4447],
229
485
  // Hangul Jamo
486
+ // Wide symbols scattered through the BMP — mostly emoji that predate the
487
+ // emoji planes (⌚ ⏰ ⚡ ✅ ✨ ❌ ❓ ⭐ ⬛ …).
488
+ [8986, 8987],
489
+ [9001, 9002],
490
+ [9193, 9196],
491
+ [9200, 9200],
492
+ [9203, 9203],
493
+ [9725, 9726],
494
+ [9748, 9749],
495
+ [9800, 9811],
496
+ [9855, 9855],
497
+ [9875, 9875],
498
+ [9889, 9889],
499
+ [9898, 9899],
500
+ [9917, 9918],
501
+ [9924, 9925],
502
+ [9934, 9934],
503
+ [9940, 9940],
504
+ [9962, 9962],
505
+ [9970, 9971],
506
+ [9973, 9973],
507
+ [9978, 9978],
508
+ [9981, 9981],
509
+ [9989, 9989],
510
+ [9994, 9995],
511
+ [10024, 10024],
512
+ [10060, 10060],
513
+ [10062, 10062],
514
+ [10067, 10069],
515
+ [10071, 10071],
516
+ [10133, 10135],
517
+ [10160, 10160],
518
+ [10175, 10175],
519
+ [11035, 11036],
520
+ [11088, 11088],
521
+ [11093, 11093],
230
522
  [11904, 42191],
231
523
  // CJK radicals .. Yi (covers CJK Unified Ideographs)
232
524
  [43360, 43391],
@@ -239,14 +531,41 @@ var WIDE_RANGES = [
239
531
  [65280, 65376],
240
532
  // fullwidth forms
241
533
  [65504, 65510],
534
+ [127744, 128591],
535
+ // pictographs, transport, emoticons (🚀 🎯 😀 …)
536
+ [128640, 128767],
537
+ [129280, 129535],
538
+ // supplemental symbols (🤖 🧱 …)
539
+ [129648, 129791],
540
+ // symbols extended-A
242
541
  [131072, 262141]
243
542
  // CJK extension planes
244
543
  ];
245
- function charWidth(cp) {
246
- for (const [lo, hi] of WIDE_RANGES) {
247
- if (cp >= lo && cp <= hi) return 2;
544
+ var ZERO_WIDTH_RANGES = [
545
+ [768, 879],
546
+ // combining diacritical marks (decomposed 'e' + ´)
547
+ [6832, 6911],
548
+ [7616, 7679],
549
+ [8203, 8207],
550
+ // zero-width space .. RLM, zero-width joiner among them
551
+ [8400, 8432],
552
+ // combining marks for symbols
553
+ [65024, 65039],
554
+ // variation selectors, VS16 (emoji presentation) included
555
+ [65056, 65071],
556
+ // combining half marks
557
+ [127995, 127999]
558
+ // emoji skin tone modifiers — always applied to a base
559
+ ];
560
+ function inRanges(cp, ranges) {
561
+ for (const [lo, hi] of ranges) {
562
+ if (cp >= lo && cp <= hi) return true;
248
563
  }
249
- return 1;
564
+ return false;
565
+ }
566
+ function charWidth(cp) {
567
+ if (inRanges(cp, ZERO_WIDTH_RANGES)) return 0;
568
+ return inRanges(cp, WIDE_RANGES) ? 2 : 1;
250
569
  }
251
570
  function displayWidth(text) {
252
571
  let w = 0;
@@ -288,6 +607,19 @@ function wrapWidth(s, width) {
288
607
  if (line !== "") lines.push(line);
289
608
  return lines;
290
609
  }
610
+
611
+ // src/render/canvas.ts
612
+ var SGR = {
613
+ none: "",
614
+ dim: "2",
615
+ amber: "33",
616
+ green: "32",
617
+ greenDim: "32;2",
618
+ // done, but nothing behind the claim: green, not fully lit
619
+ red: "31",
620
+ faint: "90"
621
+ };
622
+ var ANSI_RESET = "\x1B[0m";
291
623
  var UP = 1;
292
624
  var DOWN = 2;
293
625
  var LEFT = 4;
@@ -322,15 +654,6 @@ function maskChar(mask, heavyHorizontal, unicode) {
322
654
  }
323
655
  return LIGHT_BY_MASK[mask] ?? "\u253C";
324
656
  }
325
- var SGR = {
326
- none: "",
327
- dim: "2",
328
- amber: "33",
329
- green: "32",
330
- red: "31",
331
- faint: "90"
332
- };
333
- var ANSI_RESET = "\x1B[0m";
334
657
  var BORDER_JUNCTION = {
335
658
  "\u2500": { down: "\u252C", up: "\u2534" },
336
659
  "\u254C": { down: "\u252C", up: "\u2534" },
@@ -356,11 +679,17 @@ var Canvas = class {
356
679
  text(x, y, s, style, bold = false) {
357
680
  let cx = x;
358
681
  for (const ch of s) {
682
+ const w = charWidth(ch.codePointAt(0));
683
+ if (w === 0) {
684
+ const base = this.cell(Math.max(0, cx - 1), y);
685
+ const target = base.literal === "" ? this.cell(Math.max(0, cx - 2), y) : base;
686
+ target.literal = (target.literal ?? "") + ch;
687
+ continue;
688
+ }
359
689
  const c = this.cell(cx, y);
360
690
  c.literal = ch;
361
691
  c.style = style;
362
692
  c.bold = bold;
363
- const w = charWidth(ch.codePointAt(0));
364
693
  if (w === 2) {
365
694
  const phantom = this.cell(cx + 1, y);
366
695
  phantom.literal = "";
@@ -439,280 +768,69 @@ function drawPath(canvas, points, bright = false) {
439
768
  }
440
769
  }
441
770
  }
442
- var SPINNER_UNICODE = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
443
- var SPINNER_ASCII = ["|", "/", "-", "\\"];
444
- function skinFor(status, unicode) {
445
- const style = status === "planned" ? "dim" : status === "in-progress" ? "amber" : status === "done" ? "green" : "red";
446
- if (!unicode) {
447
- return status === "planned" ? { h: ".", v: ":", corners: ["+", "+", "+", "+"], style } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style };
448
- }
449
- switch (status) {
450
- case "planned":
451
- return { h: "\u254C", v: "\u254E", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
452
- case "in-progress":
453
- return { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
454
- case "done":
455
- case "regressed":
456
- return { h: "\u2501", v: "\u2503", corners: ["\u250F", "\u2513", "\u2517", "\u251B"], style };
457
- }
458
- }
459
- function glyphFor(status, opts) {
460
- const spinner = opts.unicode ? SPINNER_UNICODE : SPINNER_ASCII;
461
- switch (status) {
462
- case "planned":
463
- return opts.unicode ? "\xB7" : ".";
464
- case "in-progress":
465
- return spinner[opts.spinnerFrame % spinner.length];
466
- case "done":
467
- return opts.unicode ? "\u25A0" : "#";
468
- case "regressed":
469
- return opts.unicode ? "\u2717" : "X";
470
- }
471
- }
472
- var NODE_KIND_GLYPHS = {
473
- selector: ["?", "?"],
474
- sequence: ["\xBB", ">"],
475
- parallel: ["\u2016", "="],
476
- decorator: ["\u25CC", "o"],
477
- condition: ["\u25C7", "c"],
478
- action: ["\xB7", "."],
479
- source: ["\u25CB", "o"],
480
- transform: ["\u25D0", "%"],
481
- sink: ["\u25CF", "*"],
482
- service: ["\u25C6", "S"],
483
- db: ["\u25A4", "D"],
484
- queue: ["\u2263", "Q"],
485
- ui: ["\u25A3", "U"]
486
- };
487
- function kindGlyph(kind, unicode) {
488
- const pair = NODE_KIND_GLYPHS[kind];
489
- return pair === void 0 ? void 0 : unicode ? pair[0] : pair[1];
490
- }
491
- function isNeutralKind(map) {
492
- return map.kind !== void 0 && map.kind !== "dev";
493
- }
494
- function neutralSkin(unicode) {
495
- return unicode ? { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style: "none" } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style: "none" };
496
- }
771
+
772
+ // src/render/zoom-geometry.ts
497
773
  var BOX_H = 3;
498
774
  var BOX_GAP = 2;
499
775
  var LEFT_MARGIN = 2;
776
+ var BAR_MIN_RUN = 7;
500
777
  var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
501
778
  var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
502
779
  function zoomGeometry(zoom) {
780
+ const m = zoomMode(zoom);
781
+ const mode = m === "overview" ? "constellation" : m;
503
782
  switch (zoom) {
504
783
  case 2:
505
- return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
784
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
506
785
  case 1:
507
- return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
786
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
508
787
  case 0:
509
- return { mode: "boxes", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
788
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
510
789
  case -1:
511
- return { mode: "boxes", scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
790
+ return { mode, scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
512
791
  case -2:
513
- return { mode: "boxes", scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
792
+ return { mode, scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
514
793
  case -3:
515
- return { mode: "boxes", scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
794
+ return { mode, scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
516
795
  case -4:
517
- return { mode: "constellation", scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
796
+ return { mode, scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
518
797
  }
519
798
  }
520
- var LABEL_BUDGET_MIN = 4;
521
- function boxSpec(node, geo, unicode, neutral) {
522
- const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
523
- const badge = node.submap !== void 0 ? unicode ? " \u229E" : " +" : "";
524
- const badgeW = displayWidth(badge);
525
- const text = !neutral && glyph !== void 0 ? `${glyph} ${node.label}` : node.label;
526
- if (geo.mode === "constellation") {
527
- return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
528
- }
529
- if (geo.mode === "detail" && geo.detail !== void 0) {
530
- const budget2 = geo.detail;
531
- const innerW = Math.min(Math.max(displayWidth(text) + badgeW + 4, budget2.innerMin), budget2.innerMax);
532
- const extra = [];
533
- if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
534
- if (node.detail !== void 0) {
535
- const wrapped = wrapWidth(node.detail, innerW - 2);
536
- for (let i = 0; i < Math.min(wrapped.length, budget2.noteRows); i++) {
537
- const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
538
- extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
539
- }
540
- }
799
+ var AGGREGATE_GEO = {
800
+ mode: "boxes",
801
+ scale: 1,
802
+ pad: 0,
803
+ boxGap: 1,
804
+ breathe: 0,
805
+ titleGap: 0,
806
+ barGap: 1,
807
+ bandCounts: false
808
+ };
809
+
810
+ // src/render/routing.ts
811
+ function routeEdges(map, columns) {
812
+ const { bandIndexOf, bandBoxes, boxOf, contentWidth } = columns;
813
+ const pending = map.edges.map((e) => {
814
+ const from = boxOf.get(e.from);
815
+ const to = boxOf.get(e.to);
541
816
  return {
542
- w: innerW + 2,
543
- h: BOX_H + extra.length,
544
- label: fitWidth(text, innerW - 4 - badgeW) + badge,
545
- pad: 1,
546
- borderless: false,
547
- extra
817
+ from,
818
+ to,
819
+ fromBand: bandIndexOf.get(from.node.layer),
820
+ toBand: bandIndexOf.get(to.node.layer)
548
821
  };
549
- }
550
- const budget = Math.max(LABEL_BUDGET_MIN, Math.ceil(displayWidth(text) * geo.scale));
551
- const label = fitWidth(text, budget) + badge;
552
- return {
553
- w: displayWidth(label) + 4 + 2 * geo.pad,
554
- h: BOX_H,
555
- label,
556
- pad: geo.pad,
557
- borderless: false,
558
- extra: []
822
+ });
823
+ const gapVerticals = Array.from(
824
+ { length: Math.max(0, columns.bands.length - 1) },
825
+ () => /* @__PURE__ */ new Map()
826
+ );
827
+ const verticalFree = (gap, x, edge) => {
828
+ const owner = gapVerticals[gap]?.get(x);
829
+ return owner === void 0 || owner === edge;
559
830
  };
560
- }
561
- function renderMapWindow(map, opts, viewport) {
562
- const built = buildCanvas(map, opts);
563
- return {
564
- lines: built.canvas.emit(opts, viewport),
565
- contentWidth: built.canvas.width,
566
- contentHeight: built.canvas.height,
567
- hits: built.hits
831
+ const takeVertical = (gap, x, edge) => {
832
+ gapVerticals[gap]?.set(x, edge);
568
833
  };
569
- }
570
- function aggregateMap(map) {
571
- if (map.groups.length === 0) return void 0;
572
- const representative = /* @__PURE__ */ new Map();
573
- for (const n of map.nodes) representative.set(n.id, n.group ?? n.id);
574
- const nodes = map.groups.map((g) => {
575
- const members = map.nodes.filter((n) => n.group === g.id);
576
- const done = members.filter((n) => n.status === "done").length;
577
- return {
578
- id: g.id,
579
- // neutral kinds document structure, not progress — no member counts
580
- label: isNeutralKind(map) ? g.label : `${g.label} ${done}/${members.length}`,
581
- layer: g.layer,
582
- status: groupStatus(map, g.id)
583
- };
584
- });
585
- for (const n of map.nodes) if (n.group === void 0) nodes.push(n);
586
- const seen = /* @__PURE__ */ new Set();
587
- const edges = [];
588
- for (const e of map.edges) {
589
- const from = representative.get(e.from);
590
- const to = representative.get(e.to);
591
- if (from === to || seen.has(`${from}->${to}`)) continue;
592
- seen.add(`${from}->${to}`);
593
- edges.push({ from, to });
594
- }
595
- return {
596
- ...map.title !== void 0 ? { title: map.title } : {},
597
- ...map.kind !== void 0 ? { kind: map.kind } : {},
598
- layers: map.layers,
599
- groups: [],
600
- lanes: map.lanes,
601
- nodes,
602
- edges
603
- };
604
- }
605
- var AGGREGATE_GEO = {
606
- mode: "boxes",
607
- scale: 1,
608
- pad: 0,
609
- boxGap: 1,
610
- breathe: 0,
611
- titleGap: 0,
612
- barGap: 1,
613
- bandCounts: false
614
- };
615
- function flipForSequence(map) {
616
- if (map.kind !== "sequence") return map;
617
- return {
618
- ...map,
619
- layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
620
- edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
621
- };
622
- }
623
- function buildCanvas(map, opts) {
624
- const oriented = flipForSequence(map);
625
- const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
626
- const aggregated = plainGeo.mode === "constellation" ? aggregateMap(oriented) : void 0;
627
- return buildCanvasWith(aggregated ?? oriented, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo);
628
- }
629
- function buildCanvasWith(map, opts, geo) {
630
- const canvas = new Canvas();
631
- const neutral = isNeutralKind(map);
632
- const bands = [...map.layers].sort((a, b) => b.rank - a.rank);
633
- if (bands.length === 0) {
634
- canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
635
- canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
636
- return { canvas, hits: [] };
637
- }
638
- const bandIndexOf = new Map(bands.map((l, i) => [l.id, i]));
639
- const boxes = /* @__PURE__ */ new Map();
640
- const bandBoxes = bands.map(() => []);
641
- for (const node of map.nodes) {
642
- const band = bandIndexOf.get(node.layer);
643
- const box = { node, ...boxSpec(node, geo, opts.unicode, neutral), x: LEFT_MARGIN, y: 0 };
644
- bandBoxes[band].push(box);
645
- boxes.set(node.id, box);
646
- }
647
- const laneCount = map.lanes.length;
648
- const laneX = [];
649
- const laneW = [];
650
- if (laneCount === 0) {
651
- for (const row of bandBoxes) {
652
- let x = LEFT_MARGIN;
653
- for (const box of row) {
654
- box.x = x;
655
- x += box.w + geo.boxGap;
656
- }
657
- }
658
- } else {
659
- const laneGap = geo.boxGap + 2;
660
- const laneIndexOf = new Map(map.lanes.map((l, i) => [l.id, i]));
661
- const regions = laneCount + 1;
662
- const grouped = bandBoxes.map((row) => {
663
- const cells = Array.from({ length: regions }, () => []);
664
- for (const box of row) {
665
- const lane = box.node.lane;
666
- cells[lane !== void 0 ? laneIndexOf.get(lane) : regions - 1].push(box);
667
- }
668
- return cells;
669
- });
670
- const regionW = Array.from({ length: regions }, () => 0);
671
- for (const cells of grouped) {
672
- for (let i = 0; i < regions; i++) {
673
- const rowW = cells[i].reduce((sum, b, k) => sum + b.w + (k > 0 ? geo.boxGap : 0), 0);
674
- regionW[i] = Math.max(regionW[i], rowW);
675
- }
676
- }
677
- for (let i = 0; i < laneCount; i++) regionW[i] = Math.max(regionW[i], displayWidth(map.lanes[i].label) + 2);
678
- let x0 = LEFT_MARGIN;
679
- for (let i = 0; i < regions; i++) {
680
- laneX.push(x0);
681
- laneW.push(regionW[i]);
682
- x0 += regionW[i] + laneGap;
683
- }
684
- for (const cells of grouped) {
685
- for (let i = 0; i < regions; i++) {
686
- let x = laneX[i];
687
- for (const box of cells[i]) {
688
- box.x = x;
689
- x += box.w + geo.boxGap;
690
- }
691
- }
692
- }
693
- }
694
- const bandLabel = bands.map((l, i) => {
695
- const row = bandBoxes[i];
696
- const done = row.filter((b) => b.node.status === "done").length;
697
- return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
698
- });
699
- let contentWidth = LEFT_MARGIN;
700
- for (const row of bandBoxes) {
701
- const last = row[row.length - 1];
702
- if (last) contentWidth = Math.max(contentWidth, last.x + last.w);
703
- }
704
- for (let i = 0; i < laneCount; i++) contentWidth = Math.max(contentWidth, laneX[i] + laneW[i]);
705
- for (const label of bandLabel) contentWidth = Math.max(contentWidth, LEFT_MARGIN + displayWidth(label) + 7);
706
- const routes = map.edges.map((e) => {
707
- const fromBox = boxes.get(e.from);
708
- const toBox = boxes.get(e.to);
709
- return {
710
- fromBox,
711
- toBox,
712
- fromBand: bandIndexOf.get(fromBox.node.layer),
713
- toBand: bandIndexOf.get(toBox.node.layer)
714
- };
715
- });
716
834
  const claimedColumns = /* @__PURE__ */ new Map();
717
835
  const isFree = (box, x) => !(claimedColumns.get(box)?.has(x) ?? false);
718
836
  const claim = (box, x) => {
@@ -721,60 +839,65 @@ function buildCanvasWith(map, opts, geo) {
721
839
  set.add(x);
722
840
  return x;
723
841
  };
724
- const straightX = /* @__PURE__ */ new Map();
725
- for (const r of routes) {
842
+ for (const r of pending) {
726
843
  if (r.toBand - r.fromBand !== 1) continue;
727
- const lo = Math.max(r.fromBox.x + 1, r.toBox.x + 1);
728
- const hi = Math.min(r.fromBox.x + r.fromBox.w - 2, r.toBox.x + r.toBox.w - 2);
844
+ const lo = Math.max(r.from.x + 1, r.to.x + 1);
845
+ const hi = Math.min(r.from.x + r.from.w - 2, r.to.x + r.to.w - 2);
729
846
  if (lo > hi) continue;
730
847
  const mid = Math.floor((lo + hi) / 2);
731
- for (let d = 0; d <= hi - lo && !straightX.has(r); d++) {
848
+ for (let d = 0; d <= hi - lo && r.straightX === void 0; d++) {
732
849
  for (const x of d === 0 ? [mid] : [mid - d, mid + d]) {
733
- if (x >= lo && x <= hi && isFree(r.fromBox, x) && isFree(r.toBox, x)) {
734
- straightX.set(r, claim(r.toBox, claim(r.fromBox, x)));
850
+ if (x >= lo && x <= hi && isFree(r.from, x) && isFree(r.to, x) && verticalFree(r.fromBand, x, r)) {
851
+ r.straightX = claim(r.to, claim(r.from, x));
852
+ takeVertical(r.fromBand, x, r);
735
853
  break;
736
854
  }
737
855
  }
738
856
  }
739
857
  }
740
- const bent = routes.filter((r) => !straightX.has(r));
858
+ const bent = pending.filter((r) => r.straightX === void 0);
741
859
  const outgoing = /* @__PURE__ */ new Map();
742
860
  const incoming = /* @__PURE__ */ new Map();
743
861
  for (const r of bent) {
744
- outgoing.set(r.fromBox, [...outgoing.get(r.fromBox) ?? [], r]);
745
- incoming.set(r.toBox, [...incoming.get(r.toBox) ?? [], r]);
862
+ outgoing.set(r.from, [...outgoing.get(r.from) ?? [], r]);
863
+ incoming.set(r.to, [...incoming.get(r.to) ?? [], r]);
746
864
  }
747
- const freeSlot = (box, k, n) => {
865
+ const freeSlot = (box, k, n, edge, gap) => {
748
866
  const lo = box.x + 1;
749
867
  const hi = box.x + box.w - 2;
750
868
  const ideal = box.x + Math.min(box.w - 2, Math.max(1, Math.round((k + 1) * (box.w - 1) / (n + 1))));
751
869
  for (let d = 0; d <= hi - lo; d++) {
752
870
  for (const x of d === 0 ? [ideal] : [ideal - d, ideal + d]) {
753
- if (x >= lo && x <= hi && isFree(box, x)) return claim(box, x);
871
+ if (x >= lo && x <= hi && isFree(box, x) && verticalFree(gap, x, edge)) {
872
+ takeVertical(gap, x, edge);
873
+ return claim(box, x);
874
+ }
754
875
  }
755
876
  }
756
877
  return ideal;
757
878
  };
758
- const attach = /* @__PURE__ */ new Map();
759
879
  for (const r of bent) {
760
- const outs = outgoing.get(r.fromBox);
761
- const ins = incoming.get(r.toBox);
762
- attach.set(r, {
763
- sx: freeSlot(r.fromBox, outs.indexOf(r), outs.length),
764
- ex: freeSlot(r.toBox, ins.indexOf(r), ins.length)
765
- });
880
+ const outs = outgoing.get(r.from);
881
+ const ins = incoming.get(r.to);
882
+ r.exitX = freeSlot(r.from, outs.indexOf(r), outs.length, r, r.fromBand);
883
+ r.entryX = freeSlot(r.to, ins.indexOf(r), ins.length, r, r.toBand - 1);
766
884
  }
767
- const skipRoutes = bent.filter((r) => r.toBand - r.fromBand > 1);
768
885
  const usedDescent = /* @__PURE__ */ new Set();
769
- const descentX = /* @__PURE__ */ new Map();
770
886
  let fallbackCount = 0;
771
887
  const blockedByBox = (band, x) => bandBoxes[band].some((b) => x >= b.x && x <= b.x + b.w - 1);
772
- for (const r of skipRoutes) {
773
- const { ex } = attach.get(r);
888
+ const descentGapsFree = (r, c) => {
889
+ for (let g = r.fromBand; g <= r.toBand - 1; g++) {
890
+ if (!verticalFree(g, c, r)) return false;
891
+ }
892
+ return true;
893
+ };
894
+ for (const r of bent.filter((e) => e.toBand - e.fromBand > 1)) {
895
+ const ex = r.entryX;
774
896
  let chosen;
775
897
  for (let d = 0; d <= contentWidth && chosen === void 0; d++) {
776
898
  for (const c of d === 0 ? [ex] : [ex - d, ex + d]) {
777
899
  if (c < LEFT_MARGIN || c > contentWidth + 1 || usedDescent.has(c)) continue;
900
+ if (!descentGapsFree(r, c)) continue;
778
901
  let blocked = false;
779
902
  for (let b = r.fromBand + 1; b < r.toBand && !blocked; b++) blocked = blockedByBox(b, c);
780
903
  if (!blocked) {
@@ -785,167 +908,185 @@ function buildCanvasWith(map, opts, geo) {
785
908
  }
786
909
  if (chosen === void 0) chosen = contentWidth + 2 + fallbackCount++ * 2;
787
910
  usedDescent.add(chosen);
788
- descentX.set(r, chosen);
789
- }
790
- const totalWidth = fallbackCount > 0 ? contentWidth + 2 + fallbackCount * 2 : contentWidth;
791
- const gapCount = bands.length - 1;
792
- const gapSegments = Array.from({ length: gapCount }, () => []);
793
- const segmentOf = /* @__PURE__ */ new Map();
911
+ for (let g = r.fromBand; g <= r.toBand - 1; g++) takeVertical(g, chosen, r);
912
+ r.descentX = chosen;
913
+ }
914
+ const gapCount = Math.max(0, columns.bands.length - 1);
915
+ const gapSegments = Array.from(
916
+ { length: gapCount },
917
+ () => []
918
+ );
794
919
  for (const r of bent) {
795
- const { sx, ex } = attach.get(r);
796
- if (r.toBand - r.fromBand === 1) {
797
- const landing = { route: r, kind: "landing", lo: Math.min(sx, ex), hi: Math.max(sx, ex) };
798
- gapSegments[r.toBand - 1].push(landing);
799
- segmentOf.set(r, { landing });
920
+ const sx = r.exitX;
921
+ const ex = r.entryX;
922
+ if (r.descentX === void 0) {
923
+ gapSegments[r.toBand - 1].push({
924
+ edge: r,
925
+ kind: "landing",
926
+ segment: { lo: Math.min(sx, ex), hi: Math.max(sx, ex) }
927
+ });
800
928
  } else {
801
- const c = descentX.get(r);
802
- const exit = { route: r, kind: "exit", lo: Math.min(sx, c), hi: Math.max(sx, c) };
803
- const landing = { route: r, kind: "landing", lo: Math.min(c, ex), hi: Math.max(c, ex) };
804
- gapSegments[r.fromBand].push(exit);
805
- gapSegments[r.toBand - 1].push(landing);
806
- segmentOf.set(r, { exit, landing });
929
+ const c = r.descentX;
930
+ gapSegments[r.fromBand].push({ edge: r, kind: "exit", segment: { lo: Math.min(sx, c), hi: Math.max(sx, c) } });
931
+ gapSegments[r.toBand - 1].push({
932
+ edge: r,
933
+ kind: "landing",
934
+ segment: { lo: Math.min(c, ex), hi: Math.max(c, ex) }
935
+ });
807
936
  }
808
937
  }
809
- const segmentRow = /* @__PURE__ */ new Map();
810
- const gapRowCount = gapSegments.map((segments) => {
938
+ const exitRow = /* @__PURE__ */ new Map();
939
+ const landingRow = /* @__PURE__ */ new Map();
940
+ const gapRowCount = gapSegments.map((entries) => {
811
941
  const rowEnds = [];
812
- for (const s of [...segments].sort((a, b) => a.lo - b.lo)) {
813
- let row = rowEnds.findIndex((end) => s.lo > end + 1);
942
+ for (const e of [...entries].sort((a, b) => a.segment.lo - b.segment.lo)) {
943
+ let row = rowEnds.findIndex((end) => e.segment.lo > end + 1);
814
944
  if (row === -1) {
815
- rowEnds.push(s.hi);
945
+ rowEnds.push(e.segment.hi);
816
946
  row = rowEnds.length - 1;
817
947
  } else {
818
- rowEnds[row] = Math.max(rowEnds[row], s.hi);
948
+ rowEnds[row] = Math.max(rowEnds[row], e.segment.hi);
819
949
  }
820
- segmentRow.set(s, row);
950
+ (e.kind === "exit" ? exitRow : landingRow).set(e.edge, row);
821
951
  }
822
952
  return rowEnds.length;
823
953
  });
824
- let y = 0;
825
- if (map.title !== void 0) y += 1 + geo.titleGap;
826
- let laneHeaderY;
827
- if (laneCount > 0) {
828
- laneHeaderY = y;
829
- y += 1 + geo.barGap;
830
- }
831
- const barY = [];
832
- const gapTrackStartY = [];
833
- for (let b = 0; b < bands.length; b++) {
834
- barY.push(y);
835
- y += 1 + geo.barGap;
836
- const row = bandBoxes[b];
837
- for (const box of row) box.y = y;
838
- y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
839
- if (b < gapCount) {
840
- y += geo.breathe;
841
- gapTrackStartY.push(y);
842
- y += gapRowCount[b];
843
- y += geo.breathe;
954
+ const edges = pending.map((r) => {
955
+ const common = { from: r.from, to: r.to, fromBand: r.fromBand, toBand: r.toBand };
956
+ if (r.straightX !== void 0) return { ...common, kind: "straight", x: r.straightX };
957
+ if (r.descentX === void 0) {
958
+ return { ...common, kind: "dogleg", exitX: r.exitX, entryX: r.entryX, landingRow: landingRow.get(r) };
844
959
  }
960
+ return {
961
+ ...common,
962
+ kind: "thread",
963
+ exitX: r.exitX,
964
+ entryX: r.entryX,
965
+ descentX: r.descentX,
966
+ exitRow: exitRow.get(r),
967
+ landingRow: landingRow.get(r)
968
+ };
969
+ });
970
+ return { edges, gapRowCount, fallbackCount };
971
+ }
972
+ function edgePolyline(edge, rows) {
973
+ const from = rows.boxOf.get(edge.from.node.id);
974
+ const to = rows.boxOf.get(edge.to.node.id);
975
+ const sy = from.y + from.h - 1;
976
+ const ey = to.y;
977
+ if (edge.kind === "straight") {
978
+ return [
979
+ [edge.x, sy],
980
+ [edge.x, ey]
981
+ ];
845
982
  }
846
- const legendY = y + 1;
847
- const rowYOf = (gap, s) => gapTrackStartY[gap] + segmentRow.get(s);
848
- if (map.title !== void 0) canvas.text(LEFT_MARGIN, 0, map.title, "none", true);
849
- if (laneHeaderY !== void 0) {
850
- for (let i = 0; i < laneCount; i++) {
851
- const label = fitWidth(map.lanes[i].label, laneW[i]);
852
- const cx = laneX[i] + Math.max(0, Math.floor((laneW[i] - displayWidth(label)) / 2));
853
- canvas.text(cx, laneHeaderY, label, "faint", true);
854
- }
983
+ const landingY = rows.gapTrackStartY[edge.toBand - 1] + edge.landingRow;
984
+ if (edge.kind === "dogleg") {
985
+ return [
986
+ [edge.exitX, sy],
987
+ [edge.exitX, landingY],
988
+ [edge.entryX, landingY],
989
+ [edge.entryX, ey]
990
+ ];
855
991
  }
856
- for (let b = 0; b < bands.length; b++) {
857
- const label = bandLabel[b];
858
- for (let x = 0; x < totalWidth; x++) canvas.line(x, barY[b], LEFT | RIGHT, true);
859
- const labelStart = (fallbackCount > 0 ? contentWidth : totalWidth) - displayWidth(label);
860
- canvas.text(labelStart, barY[b], label, "none", true);
861
- }
862
- for (const box of boxes.values()) {
863
- drawBox(canvas, box, opts, neutral, opts.focus !== void 0 && box.node.id === opts.focus);
864
- }
865
- for (const r of routes) {
866
- const sy = r.fromBox.y + r.fromBox.h - 1;
867
- const ey = r.toBox.y;
868
- const bright = opts.focus !== void 0 && (r.fromBox.node.id === opts.focus || r.toBox.node.id === opts.focus);
869
- const direct = straightX.get(r);
870
- if (direct !== void 0) {
871
- drawPath(
872
- canvas,
873
- [
874
- [direct, sy],
875
- [direct, ey]
876
- ],
877
- bright
878
- );
879
- continue;
880
- }
881
- const { sx, ex } = attach.get(r);
882
- const segments = segmentOf.get(r);
883
- const landingY = rowYOf(r.toBand - 1, segments.landing);
884
- if (r.toBand - r.fromBand === 1) {
885
- drawPath(
886
- canvas,
887
- [
888
- [sx, sy],
889
- [sx, landingY],
890
- [ex, landingY],
891
- [ex, ey]
892
- ],
893
- bright
894
- );
895
- } else {
896
- const c = descentX.get(r);
897
- const exitY = rowYOf(r.fromBand, segments.exit);
898
- drawPath(
899
- canvas,
900
- [
901
- [sx, sy],
902
- [sx, exitY],
903
- [c, exitY],
904
- [c, landingY],
905
- [ex, landingY],
906
- [ex, ey]
907
- ],
908
- bright
909
- );
910
- }
992
+ const exitY = rows.gapTrackStartY[edge.fromBand] + edge.exitRow;
993
+ return [
994
+ [edge.exitX, sy],
995
+ [edge.exitX, exitY],
996
+ [edge.descentX, exitY],
997
+ [edge.descentX, landingY],
998
+ [edge.entryX, landingY],
999
+ [edge.entryX, ey]
1000
+ ];
1001
+ }
1002
+
1003
+ // src/render/skins.ts
1004
+ function styleFor(face) {
1005
+ switch (face) {
1006
+ case "planned":
1007
+ return "dim";
1008
+ case "in-progress":
1009
+ return "amber";
1010
+ case "done":
1011
+ return "green";
1012
+ case "done-unverified":
1013
+ return "greenDim";
1014
+ case "regressed":
1015
+ return "red";
911
1016
  }
912
- let lx = LEFT_MARGIN;
913
- if (neutral) {
914
- lx = canvas.text(lx, legendY, map.kind, "faint");
915
- const seen = /* @__PURE__ */ new Set();
916
- for (const n of map.nodes) {
917
- const k = n.kind;
918
- if (k === void 0 || seen.has(k) || kindGlyph(k, opts.unicode) === void 0) continue;
919
- seen.add(k);
920
- lx = canvas.text(lx, legendY, " ", "none");
921
- lx = canvas.text(lx, legendY, `${kindGlyph(k, opts.unicode)} ${k}`, "none");
922
- }
923
- } else {
924
- const legendOpts = { ...opts, spinnerFrame: 0 };
925
- const legendEntries = [
926
- ["planned", "dim"],
927
- ["in-progress", "amber"],
928
- ["done", "green"],
929
- ["regressed", "red"]
930
- ];
931
- for (const [status, style] of legendEntries) {
932
- if (lx > LEFT_MARGIN) lx = canvas.text(lx, legendY, " ", "none");
933
- lx = canvas.text(lx, legendY, `${glyphFor(status, legendOpts)} ${status}`, style);
1017
+ }
1018
+ function statusSgr(status) {
1019
+ return SGR[styleFor(status)];
1020
+ }
1021
+ function skinFor(face, unicode) {
1022
+ const style = styleFor(face);
1023
+ if (!unicode) {
1024
+ return face === "planned" ? { h: ".", v: ":", corners: ["+", "+", "+", "+"], style } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style };
1025
+ }
1026
+ switch (face) {
1027
+ case "planned":
1028
+ return { h: "\u254C", v: "\u254E", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
1029
+ case "in-progress":
1030
+ return { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style };
1031
+ // an unverified done keeps the heavy border of done — it is the same
1032
+ // claim, told with a hollow glyph and a dimmer green
1033
+ case "done":
1034
+ case "done-unverified":
1035
+ case "regressed":
1036
+ return { h: "\u2501", v: "\u2503", corners: ["\u250F", "\u2513", "\u2517", "\u251B"], style };
1037
+ }
1038
+ }
1039
+ function glyphFor(face, opts) {
1040
+ if (face === "in-progress") return spinnerGlyph(opts.spinnerFrame, opts.unicode);
1041
+ return face === "done-unverified" ? unverifiedDoneGlyph(opts.unicode) : statusGlyph(face, opts.unicode);
1042
+ }
1043
+ function neutralSkin(unicode) {
1044
+ return unicode ? { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style: "none" } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style: "none" };
1045
+ }
1046
+ function neutralGlyph(node, unicode) {
1047
+ return (node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0) ?? (unicode ? "\xB7" : ".");
1048
+ }
1049
+ function unverifiedDoneIds(declared, drawn) {
1050
+ const out = /* @__PURE__ */ new Set();
1051
+ const declaredById = new Map(declared.nodes.map((n) => [n.id, n]));
1052
+ for (const node of drawn.nodes) {
1053
+ if (node.status !== "done") continue;
1054
+ const own = declaredById.get(node.id);
1055
+ if (own !== void 0) {
1056
+ if (own.evidence === void 0) out.add(node.id);
1057
+ } else if (declared.nodes.some(
1058
+ (m) => m.group === node.id && m.status === "done" && m.evidence === void 0
1059
+ )) {
1060
+ out.add(node.id);
934
1061
  }
935
1062
  }
936
- const hits = [...boxes.values()].map((b) => ({
937
- id: b.node.id,
938
- x: b.x,
939
- y: b.y,
940
- w: b.w,
941
- h: b.h
942
- }));
943
- return { canvas, hits };
1063
+ return out;
1064
+ }
1065
+
1066
+ // src/render/draw.ts
1067
+ function drawTitle(canvas, title) {
1068
+ canvas.text(LEFT_MARGIN, 0, title, "none", true);
1069
+ }
1070
+ function drawLaneHeaders(canvas, map, columns, rows) {
1071
+ if (rows.laneHeaderY === void 0) return;
1072
+ for (let i = 0; i < map.lanes.length; i++) {
1073
+ const region = columns.lanes[i];
1074
+ const label = fitWidth(map.lanes[i].label, region.w);
1075
+ const cx = region.x + Math.max(0, Math.floor((region.w - displayWidth(label)) / 2));
1076
+ canvas.text(cx, rows.laneHeaderY, label, "faint", true);
1077
+ }
1078
+ }
1079
+ function drawBands(canvas, columns, rows, wiredWidth, totalWidth) {
1080
+ for (let b = 0; b < columns.bands.length; b++) {
1081
+ const label = columns.bandLabel[b];
1082
+ for (let x = 0; x < wiredWidth; x++) canvas.line(x, rows.barY[b], LEFT | RIGHT, true);
1083
+ canvas.text(totalWidth - displayWidth(label), rows.barY[b], label, "none", true);
1084
+ }
944
1085
  }
945
- function drawBox(canvas, box, opts, neutral, focused = false) {
1086
+ function drawBox(canvas, box, opts, neutral, face, focused = false) {
946
1087
  const { node, x, y, w } = box;
947
- const skin = neutral ? neutralSkin(opts.unicode) : skinFor(node.status, opts.unicode);
948
- const slotGlyph = neutral ? (node.kind !== void 0 ? kindGlyph(node.kind, opts.unicode) : void 0) ?? (opts.unicode ? "\xB7" : ".") : glyphFor(node.status, opts);
1088
+ const skin = neutral ? neutralSkin(opts.unicode) : skinFor(face, opts.unicode);
1089
+ const slotGlyph = neutral ? neutralGlyph(node, opts.unicode) : glyphFor(face, opts);
949
1090
  if (box.borderless) {
950
1091
  canvas.text(x + 1, y, slotGlyph, skin.style, true);
951
1092
  return;
@@ -965,65 +1106,239 @@ function drawBox(canvas, box, opts, neutral, focused = false) {
965
1106
  }
966
1107
  canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3], skin.style, focused);
967
1108
  }
968
-
969
- // src/store/store.ts
970
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
971
- import { basename, dirname, join } from "node:path";
972
- var STATE_FILE_VERSION = 1;
973
- var STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
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
- }
978
- function pageFilePath(defaultFile, page) {
979
- return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
980
- }
981
- function pageIdOfFile(defaultFile, path) {
982
- if (path === defaultFile) return void 0;
983
- const name = basename(path);
984
- return name.endsWith(".json") ? name.slice(0, -".json".length) : name;
1109
+ function drawEdges(canvas, edges, rows, opts) {
1110
+ for (const edge of edges) {
1111
+ const bright = opts.focus !== void 0 && (edge.from.node.id === opts.focus || edge.to.node.id === opts.focus);
1112
+ drawPath(canvas, edgePolyline(edge, rows), bright);
1113
+ }
985
1114
  }
986
- function listPageFiles(defaultFile) {
987
- const out = [];
988
- if (existsSync(defaultFile)) out.push(defaultFile);
989
- let entries = [];
990
- try {
991
- entries = readdirSync(join(dirname(defaultFile), PAGES_DIR_NAME));
992
- } catch {
1115
+ function drawLegend(canvas, map, opts, legendY, neutral, anyUnverified) {
1116
+ let lx = LEFT_MARGIN;
1117
+ if (neutral) {
1118
+ lx = canvas.text(lx, legendY, map.kind, "faint");
1119
+ const seen = /* @__PURE__ */ new Set();
1120
+ for (const n of map.nodes) {
1121
+ const k = n.kind;
1122
+ if (k === void 0 || seen.has(k) || kindGlyph(k, opts.unicode) === void 0) continue;
1123
+ seen.add(k);
1124
+ lx = canvas.text(lx, legendY, " ", "none");
1125
+ lx = canvas.text(lx, legendY, `${kindGlyph(k, opts.unicode)} ${k}`, "none");
1126
+ }
1127
+ return;
993
1128
  }
994
- for (const e of entries.sort()) {
995
- if (e.endsWith(".json")) out.push(join(dirname(defaultFile), PAGES_DIR_NAME, e));
1129
+ const legendOpts = { ...opts, spinnerFrame: 0 };
1130
+ const faces = ["planned", "in-progress", "done", "regressed"];
1131
+ if (anyUnverified) faces.push("done-unverified");
1132
+ for (const face of faces) {
1133
+ if (lx > LEFT_MARGIN) lx = canvas.text(lx, legendY, " ", "none");
1134
+ const word = face === "done-unverified" ? "done, no evidence" : face;
1135
+ lx = canvas.text(lx, legendY, `${glyphFor(face, legendOpts)} ${word}`, styleFor(face));
996
1136
  }
997
- return out;
998
1137
  }
999
- var FOCUS_FILE_NAME = "mellos-mapping.focus";
1000
- function focusFilePath(defaultFile) {
1001
- return join(dirname(defaultFile), FOCUS_FILE_NAME);
1138
+
1139
+ // src/render/layout.ts
1140
+ var LABEL_BUDGET_MIN = 4;
1141
+ function boxSpec(node, geo, unicode, neutral) {
1142
+ const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
1143
+ const badge = node.submap !== void 0 ? unicode ? " \u229E" : " +" : "";
1144
+ const badgeW = displayWidth(badge);
1145
+ const text = !neutral && glyph !== void 0 ? `${glyph} ${node.label}` : node.label;
1146
+ if (geo.mode === "constellation") {
1147
+ return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
1148
+ }
1149
+ if (geo.mode === "detail" && geo.detail !== void 0) {
1150
+ const budget2 = geo.detail;
1151
+ const innerW = Math.min(Math.max(displayWidth(text) + badgeW + 4, budget2.innerMin), budget2.innerMax);
1152
+ const extra = [];
1153
+ if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
1154
+ if (node.detail !== void 0) {
1155
+ const wrapped = wrapWidth(node.detail, innerW - 2);
1156
+ for (let i = 0; i < Math.min(wrapped.length, budget2.noteRows); i++) {
1157
+ const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
1158
+ extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
1159
+ }
1160
+ }
1161
+ return {
1162
+ w: innerW + 2,
1163
+ h: BOX_H + extra.length,
1164
+ label: fitWidth(text, innerW - 4 - badgeW) + badge,
1165
+ pad: 1,
1166
+ borderless: false,
1167
+ extra
1168
+ };
1169
+ }
1170
+ const budget = Math.max(LABEL_BUDGET_MIN, Math.ceil(displayWidth(text) * geo.scale));
1171
+ const label = fitWidth(text, budget) + badge;
1172
+ return {
1173
+ w: displayWidth(label) + 4 + 2 * geo.pad,
1174
+ h: BOX_H,
1175
+ label,
1176
+ pad: geo.pad,
1177
+ borderless: false,
1178
+ extra: []
1179
+ };
1002
1180
  }
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;
1181
+ function layoutColumns(map, geo, unicode, neutral) {
1182
+ const bands = [...map.layers].sort((a, b) => b.rank - a.rank);
1183
+ const bandIndexOf = new Map(bands.map((l, i) => [l.id, i]));
1184
+ const sized = /* @__PURE__ */ new Map();
1185
+ const bandSized = bands.map(() => []);
1186
+ for (const node of map.nodes) {
1187
+ const spec = { node, ...boxSpec(node, geo, unicode, neutral) };
1188
+ bandSized[bandIndexOf.get(node.layer)].push(spec);
1189
+ sized.set(node.id, spec);
1190
+ }
1191
+ const columnOf = /* @__PURE__ */ new Map();
1192
+ const lanes = [];
1193
+ if (map.lanes.length === 0) {
1194
+ for (const row of bandSized) {
1195
+ let x = LEFT_MARGIN;
1196
+ for (const spec of row) {
1197
+ columnOf.set(spec, x);
1198
+ x += spec.w + geo.boxGap;
1199
+ }
1200
+ }
1201
+ } else {
1202
+ const laneCount = map.lanes.length;
1203
+ const laneGap = geo.boxGap + 2;
1204
+ const laneIndexOf = new Map(map.lanes.map((l, i) => [l.id, i]));
1205
+ const regions = laneCount + 1;
1206
+ const grouped = bandSized.map((row) => {
1207
+ const cells = Array.from({ length: regions }, () => []);
1208
+ for (const spec of row) {
1209
+ const lane = spec.node.lane;
1210
+ cells[lane !== void 0 ? laneIndexOf.get(lane) : regions - 1].push(spec);
1211
+ }
1212
+ return cells;
1213
+ });
1214
+ const regionW = Array.from({ length: regions }, () => 0);
1215
+ for (const cells of grouped) {
1216
+ for (let i = 0; i < regions; i++) {
1217
+ const rowW = cells[i].reduce((sum, b, k) => sum + b.w + (k > 0 ? geo.boxGap : 0), 0);
1218
+ regionW[i] = Math.max(regionW[i], rowW);
1219
+ }
1220
+ }
1221
+ for (let i = 0; i < laneCount; i++) regionW[i] = Math.max(regionW[i], displayWidth(map.lanes[i].label) + 2);
1222
+ let x0 = LEFT_MARGIN;
1223
+ for (let i = 0; i < regions; i++) {
1224
+ lanes.push({ x: x0, w: regionW[i] });
1225
+ x0 += regionW[i] + laneGap;
1226
+ }
1227
+ for (const cells of grouped) {
1228
+ for (let i = 0; i < regions; i++) {
1229
+ let x = lanes[i].x;
1230
+ for (const spec of cells[i]) {
1231
+ columnOf.set(spec, x);
1232
+ x += spec.w + geo.boxGap;
1233
+ }
1234
+ }
1235
+ }
1010
1236
  }
1011
- try {
1012
- rmSync(path, { force: true });
1013
- } catch {
1237
+ const placed = /* @__PURE__ */ new Map();
1238
+ for (const [, spec] of sized) placed.set(spec, { ...spec, x: columnOf.get(spec) ?? LEFT_MARGIN });
1239
+ const bandBoxes = bandSized.map((row) => row.map((spec) => placed.get(spec)));
1240
+ const boxOf = /* @__PURE__ */ new Map();
1241
+ for (const node of map.nodes) boxOf.set(node.id, placed.get(sized.get(node.id)));
1242
+ const bandLabel = bands.map((l, i) => {
1243
+ const row = bandBoxes[i];
1244
+ const done = row.filter((b) => b.node.status === "done").length;
1245
+ return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
1246
+ });
1247
+ let contentWidth = LEFT_MARGIN + BAR_MIN_RUN;
1248
+ for (const row of bandBoxes) for (const box of row) contentWidth = Math.max(contentWidth, box.x + box.w);
1249
+ for (const lane of lanes) contentWidth = Math.max(contentWidth, lane.x + lane.w);
1250
+ return { bands, bandIndexOf, bandBoxes, boxOf, lanes, bandLabel, contentWidth };
1251
+ }
1252
+ function layoutRows(columns, geo, gapRowCount, hasTitle, hasLanes) {
1253
+ let y = 0;
1254
+ if (hasTitle) y += 1 + geo.titleGap;
1255
+ let laneHeaderY;
1256
+ if (hasLanes) {
1257
+ laneHeaderY = y;
1258
+ y += 1 + geo.barGap;
1014
1259
  }
1015
- let parsed;
1016
- try {
1017
- parsed = JSON.parse(raw);
1018
- } catch {
1019
- return void 0;
1260
+ const barY = [];
1261
+ const gapTrackStartY = [];
1262
+ const bandBoxes = [];
1263
+ const placed = /* @__PURE__ */ new Map();
1264
+ const gapCount = columns.bands.length - 1;
1265
+ for (let b = 0; b < columns.bands.length; b++) {
1266
+ barY.push(y);
1267
+ y += 1 + geo.barGap;
1268
+ const row = columns.bandBoxes[b];
1269
+ for (const box of row) placed.set(box, { ...box, y });
1270
+ bandBoxes.push(row.map((box) => placed.get(box)));
1271
+ y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
1272
+ if (b < gapCount) {
1273
+ y += geo.breathe;
1274
+ gapTrackStartY.push(y);
1275
+ y += gapRowCount[b];
1276
+ y += geo.breathe;
1277
+ }
1020
1278
  }
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;
1279
+ const boxOf = /* @__PURE__ */ new Map();
1280
+ for (const [id, box] of columns.boxOf) boxOf.set(id, placed.get(box));
1281
+ return { boxOf, bandBoxes, barY, gapTrackStartY, laneHeaderY, legendY: y + 1 };
1282
+ }
1283
+
1284
+ // src/render/render.ts
1285
+ function renderMapWindow(map, opts, viewport) {
1286
+ const built = buildCanvas(map, opts);
1287
+ return {
1288
+ lines: built.canvas.emit(opts, viewport),
1289
+ contentWidth: built.canvas.width,
1290
+ contentHeight: built.canvas.height,
1291
+ hits: built.hits
1292
+ };
1293
+ }
1294
+ function buildCanvas(map, opts) {
1295
+ const oriented = flipForSequence(map);
1296
+ const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
1297
+ const aggregated = plainGeo.mode === "constellation" ? aggregateMap(oriented) : void 0;
1298
+ const drawn = aggregated ?? oriented;
1299
+ return paint(drawn, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo, unverifiedDoneIds(oriented, drawn));
1300
+ }
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
+ }
1308
+ const neutral = isNeutralKind(map);
1309
+ const columns = layoutColumns(map, geo, opts.unicode, neutral);
1310
+ const routing = routeEdges(map, columns);
1311
+ const rows = layoutRows(columns, geo, routing.gapRowCount, map.title !== void 0, map.lanes.length > 0);
1312
+ const wiredWidth = routing.fallbackCount > 0 ? columns.contentWidth + 2 + routing.fallbackCount * 2 : columns.contentWidth;
1313
+ const totalWidth = wiredWidth + Math.max(...columns.bandLabel.map(displayWidth));
1314
+ if (map.title !== void 0) drawTitle(canvas, map.title);
1315
+ drawLaneHeaders(canvas, map, columns, rows);
1316
+ drawBands(canvas, columns, rows, wiredWidth, totalWidth);
1317
+ const faceOf = (id, status) => unverified.has(id) ? "done-unverified" : status;
1318
+ for (const box of rows.boxOf.values()) {
1319
+ const id = box.node.id;
1320
+ drawBox(canvas, box, opts, neutral, faceOf(id, box.node.status), opts.focus !== void 0 && id === opts.focus);
1321
+ }
1322
+ drawEdges(canvas, routing.edges, rows, opts);
1323
+ 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
+ return { canvas, hits };
1332
+ }
1333
+
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
+ // src/store/format.ts
1339
+ var STATE_FILE_VERSION = 1;
1340
+ function makePageId(raw) {
1341
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
1027
1342
  }
1028
1343
  function describeStoreError(e) {
1029
1344
  switch (e.kind) {
@@ -1035,111 +1350,169 @@ function describeStoreError(e) {
1035
1350
  return `map file ${e.path} has an unexpected shape: ${e.detail}`;
1036
1351
  case "invariant-violation":
1037
1352
  return `map file ${e.path} violates a structural invariant: ${describeMapError(e.violation)}`;
1353
+ case "save-failed":
1354
+ return `could not write ${e.path}: ${e.detail}`;
1355
+ case "delete-failed":
1356
+ return `could not delete ${e.path}: ${e.detail}`;
1038
1357
  }
1039
1358
  }
1040
1359
  function isRecord(v) {
1041
1360
  return typeof v === "object" && v !== null && !Array.isArray(v);
1042
1361
  }
1043
- function asArray(v) {
1044
- return Array.isArray(v) ? v : [];
1362
+ function describeValue(v) {
1363
+ if (v === void 0) return "missing";
1364
+ if (v === null) return "null";
1365
+ if (Array.isArray(v)) return "an array";
1366
+ return `a ${typeof v}`;
1045
1367
  }
1046
- function optionalString(v) {
1047
- return typeof v === "string" ? v : void 0;
1368
+ function badShape(path, where, expected, got) {
1369
+ return err({ kind: "bad-shape", path, detail: `${where} is ${describeValue(got)}, expected ${expected}` });
1370
+ }
1371
+ function arrayField(raw, key, path, presence) {
1372
+ const v = raw[key];
1373
+ if (Array.isArray(v)) return ok(v);
1374
+ if (v === void 0 && presence === "optional") return ok([]);
1375
+ return badShape(path, `"${key}"`, "an array", v);
1376
+ }
1377
+ function requiredString(rec, key, where, path) {
1378
+ const v = rec[key];
1379
+ return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
1380
+ }
1381
+ function optionalString(rec, key, where, path) {
1382
+ const v = rec[key];
1383
+ if (v === void 0) return ok(void 0);
1384
+ return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
1048
1385
  }
1049
1386
  function parseMap(raw, path) {
1050
1387
  if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
1051
1388
  if (raw["version"] !== STATE_FILE_VERSION) {
1052
1389
  return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION}` });
1053
1390
  }
1391
+ const layers = arrayField(raw, "layers", path, "required");
1392
+ if (!layers.ok) return layers;
1393
+ const nodes = arrayField(raw, "nodes", path, "required");
1394
+ if (!nodes.ok) return nodes;
1395
+ const edges = arrayField(raw, "edges", path, "required");
1396
+ if (!edges.ok) return edges;
1397
+ const lanes = arrayField(raw, "lanes", path, "optional");
1398
+ if (!lanes.ok) return lanes;
1399
+ const groups = arrayField(raw, "groups", path, "optional");
1400
+ if (!groups.ok) return groups;
1054
1401
  let map = EMPTY_MAP;
1055
- const title = optionalString(raw["title"]);
1056
- if (title !== void 0) map = setTitle(map, title);
1057
- const rawKind = optionalString(raw["kind"]);
1058
- if (rawKind !== void 0) {
1059
- const kind = makeMapKind(rawKind);
1402
+ const title = optionalString(raw, "title", "map", path);
1403
+ if (!title.ok) return title;
1404
+ if (title.value !== void 0) map = setTitle(map, title.value);
1405
+ const rawKind = optionalString(raw, "kind", "map", path);
1406
+ if (!rawKind.ok) return rawKind;
1407
+ if (rawKind.value !== void 0) {
1408
+ const kind = makeMapKind(rawKind.value);
1060
1409
  if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
1061
1410
  map = setKind(map, kind.value);
1062
1411
  }
1063
- for (const [i, rawLayer] of asArray(raw["layers"]).entries()) {
1064
- if (!isRecord(rawLayer)) return err({ kind: "bad-shape", path, detail: `layers[${i}] is not an object` });
1065
- const id = makeLayerId(String(rawLayer["id"] ?? ""));
1412
+ for (const [i, rawLayer] of layers.value.entries()) {
1413
+ const where = `layers[${i}]`;
1414
+ if (!isRecord(rawLayer)) return badShape(path, where, "an object", rawLayer);
1415
+ const rawId = requiredString(rawLayer, "id", where, path);
1416
+ if (!rawId.ok) return rawId;
1417
+ const id = makeLayerId(rawId.value);
1066
1418
  if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1067
- const name = optionalString(rawLayer["name"]);
1068
- const rank = rawLayer["rank"];
1069
- if (name === void 0 || typeof rank !== "number" || !Number.isInteger(rank)) {
1070
- return err({ kind: "bad-shape", path, detail: `layers[${i}] needs a string name and an integer rank` });
1071
- }
1072
- const next = declareLayer(map, { id: id.value, name, rank });
1419
+ const name = requiredString(rawLayer, "name", where, path);
1420
+ if (!name.ok) return name;
1421
+ const rawRank = rawLayer["rank"];
1422
+ if (typeof rawRank !== "number") return badShape(path, `${where}.rank`, "a number", rawRank);
1423
+ const rank = makeRank(rawRank);
1424
+ if (!rank.ok) return err({ kind: "invariant-violation", path, violation: rank.error });
1425
+ const next = declareLayer(map, { id: id.value, name: name.value, rank: rank.value });
1073
1426
  if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
1074
1427
  map = next.value;
1075
1428
  }
1076
- for (const [i, rawLane] of asArray(raw["lanes"]).entries()) {
1077
- if (!isRecord(rawLane)) return err({ kind: "bad-shape", path, detail: `lanes[${i}] is not an object` });
1078
- const id = makeLaneId(String(rawLane["id"] ?? ""));
1429
+ for (const [i, rawLane] of lanes.value.entries()) {
1430
+ const where = `lanes[${i}]`;
1431
+ if (!isRecord(rawLane)) return badShape(path, where, "an object", rawLane);
1432
+ const rawId = requiredString(rawLane, "id", where, path);
1433
+ if (!rawId.ok) return rawId;
1434
+ const id = makeLaneId(rawId.value);
1079
1435
  if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1080
- const label = optionalString(rawLane["label"]);
1081
- if (label === void 0) return err({ kind: "bad-shape", path, detail: `lanes[${i}] needs a string label` });
1082
- const declared = declareLane(map, { id: id.value, label });
1436
+ const label = requiredString(rawLane, "label", where, path);
1437
+ if (!label.ok) return label;
1438
+ const declared = declareLane(map, { id: id.value, label: label.value });
1083
1439
  if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1084
1440
  map = declared.value;
1085
1441
  }
1086
- for (const [i, rawGroup] of asArray(raw["groups"]).entries()) {
1087
- if (!isRecord(rawGroup)) return err({ kind: "bad-shape", path, detail: `groups[${i}] is not an object` });
1088
- const id = makeGroupId(String(rawGroup["id"] ?? ""));
1442
+ for (const [i, rawGroup] of groups.value.entries()) {
1443
+ const where = `groups[${i}]`;
1444
+ if (!isRecord(rawGroup)) return badShape(path, where, "an object", rawGroup);
1445
+ const rawId = requiredString(rawGroup, "id", where, path);
1446
+ if (!rawId.ok) return rawId;
1447
+ const id = makeGroupId(rawId.value);
1089
1448
  if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1090
- const layer = makeLayerId(String(rawGroup["layer"] ?? ""));
1449
+ const rawLayer = requiredString(rawGroup, "layer", where, path);
1450
+ if (!rawLayer.ok) return rawLayer;
1451
+ const layer = makeLayerId(rawLayer.value);
1091
1452
  if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
1092
- const label = optionalString(rawGroup["label"]);
1093
- if (label === void 0) return err({ kind: "bad-shape", path, detail: `groups[${i}] needs a string label` });
1094
- const declared = declareGroup(map, { id: id.value, label, layer: layer.value });
1453
+ const label = requiredString(rawGroup, "label", where, path);
1454
+ if (!label.ok) return label;
1455
+ const declared = declareGroup(map, { id: id.value, label: label.value, layer: layer.value });
1095
1456
  if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1096
1457
  map = declared.value;
1097
1458
  }
1098
- for (const [i, rawNode] of asArray(raw["nodes"]).entries()) {
1099
- if (!isRecord(rawNode)) return err({ kind: "bad-shape", path, detail: `nodes[${i}] is not an object` });
1100
- const id = makeNodeId(String(rawNode["id"] ?? ""));
1459
+ for (const [i, rawNode] of nodes.value.entries()) {
1460
+ const where = `nodes[${i}]`;
1461
+ if (!isRecord(rawNode)) return badShape(path, where, "an object", rawNode);
1462
+ const rawId = requiredString(rawNode, "id", where, path);
1463
+ if (!rawId.ok) return rawId;
1464
+ const id = makeNodeId(rawId.value);
1101
1465
  if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1102
- const layer = makeLayerId(String(rawNode["layer"] ?? ""));
1466
+ const rawLayer = requiredString(rawNode, "layer", where, path);
1467
+ if (!rawLayer.ok) return rawLayer;
1468
+ const layer = makeLayerId(rawLayer.value);
1103
1469
  if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
1104
- const status = makeNodeStatus(String(rawNode["status"] ?? ""));
1470
+ const rawStatus = requiredString(rawNode, "status", where, path);
1471
+ if (!rawStatus.ok) return rawStatus;
1472
+ const status = makeNodeStatus(rawStatus.value);
1105
1473
  if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
1106
- const label = optionalString(rawNode["label"]);
1107
- if (label === void 0) return err({ kind: "bad-shape", path, detail: `nodes[${i}] needs a string label` });
1108
- const detail = optionalString(rawNode["detail"]);
1109
- const rawGroup = optionalString(rawNode["group"]);
1474
+ const label = requiredString(rawNode, "label", where, path);
1475
+ if (!label.ok) return label;
1476
+ const detail = optionalString(rawNode, "detail", where, path);
1477
+ if (!detail.ok) return detail;
1478
+ const rawGroup = optionalString(rawNode, "group", where, path);
1479
+ if (!rawGroup.ok) return rawGroup;
1110
1480
  let group;
1111
- if (rawGroup !== void 0) {
1112
- const made = makeGroupId(rawGroup);
1481
+ if (rawGroup.value !== void 0) {
1482
+ const made = makeGroupId(rawGroup.value);
1113
1483
  if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1114
1484
  group = made.value;
1115
1485
  }
1116
- const rawNodeKind = optionalString(rawNode["kind"]);
1486
+ const rawNodeKind = optionalString(rawNode, "kind", where, path);
1487
+ if (!rawNodeKind.ok) return rawNodeKind;
1117
1488
  let nodeKind;
1118
- if (rawNodeKind !== void 0) {
1119
- const made = makeNodeKind(rawNodeKind);
1489
+ if (rawNodeKind.value !== void 0) {
1490
+ const made = makeNodeKind(rawNodeKind.value);
1120
1491
  if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1121
1492
  nodeKind = made.value;
1122
1493
  }
1123
- const rawLane = optionalString(rawNode["lane"]);
1494
+ const rawLane = optionalString(rawNode, "lane", where, path);
1495
+ if (!rawLane.ok) return rawLane;
1124
1496
  let lane;
1125
- if (rawLane !== void 0) {
1126
- const made = makeLaneId(rawLane);
1497
+ if (rawLane.value !== void 0) {
1498
+ const made = makeLaneId(rawLane.value);
1127
1499
  if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1128
1500
  lane = made.value;
1129
1501
  }
1130
- const rawSubmap = optionalString(rawNode["submap"]);
1502
+ const rawSubmap = optionalString(rawNode, "submap", where, path);
1503
+ if (!rawSubmap.ok) return rawSubmap;
1131
1504
  let submap;
1132
- if (rawSubmap !== void 0) {
1133
- const made = makeSubmapRef(rawSubmap);
1505
+ if (rawSubmap.value !== void 0) {
1506
+ const made = makeSubmapRef(rawSubmap.value);
1134
1507
  if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1135
1508
  submap = made.value;
1136
1509
  }
1137
1510
  const declared = declareNode(map, {
1138
1511
  id: id.value,
1139
- label,
1512
+ label: label.value,
1140
1513
  layer: layer.value,
1141
1514
  status: status.value,
1142
- ...detail !== void 0 ? { detail } : {},
1515
+ ...detail.value !== void 0 ? { detail: detail.value } : {},
1143
1516
  ...group !== void 0 ? { group } : {},
1144
1517
  ...nodeKind !== void 0 ? { kind: nodeKind } : {},
1145
1518
  ...lane !== void 0 ? { lane } : {},
@@ -1147,25 +1520,210 @@ function parseMap(raw, path) {
1147
1520
  });
1148
1521
  if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1149
1522
  map = declared.value;
1150
- const evidence = optionalString(rawNode["evidence"]);
1151
- if (evidence !== void 0) {
1152
- const updated = updateNode(map, { id: id.value, evidence });
1523
+ const evidence = optionalString(rawNode, "evidence", where, path);
1524
+ if (!evidence.ok) return evidence;
1525
+ if (evidence.value !== void 0) {
1526
+ const updated = updateNode(map, { id: id.value, evidence: evidence.value });
1153
1527
  if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
1154
1528
  map = updated.value;
1155
1529
  }
1156
1530
  }
1157
- for (const [i, rawEdge] of asArray(raw["edges"]).entries()) {
1158
- if (!isRecord(rawEdge)) return err({ kind: "bad-shape", path, detail: `edges[${i}] is not an object` });
1159
- const from = makeNodeId(String(rawEdge["from"] ?? ""));
1531
+ for (const [i, rawEdge] of edges.value.entries()) {
1532
+ const where = `edges[${i}]`;
1533
+ if (!isRecord(rawEdge)) return badShape(path, where, "an object", rawEdge);
1534
+ const rawFrom = requiredString(rawEdge, "from", where, path);
1535
+ if (!rawFrom.ok) return rawFrom;
1536
+ const from = makeNodeId(rawFrom.value);
1160
1537
  if (!from.ok) return err({ kind: "invariant-violation", path, violation: from.error });
1161
- const to = makeNodeId(String(rawEdge["to"] ?? ""));
1538
+ const rawTo = requiredString(rawEdge, "to", where, path);
1539
+ if (!rawTo.ok) return rawTo;
1540
+ const to = makeNodeId(rawTo.value);
1162
1541
  if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
1163
- const linked = linkNodes(map, from.value, to.value, optionalString(rawEdge["label"]));
1542
+ const label = optionalString(rawEdge, "label", where, path);
1543
+ if (!label.ok) return label;
1544
+ const linked = linkNodes(map, from.value, to.value, label.value);
1164
1545
  if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
1165
1546
  map = linked.value;
1166
1547
  }
1167
1548
  return ok(map);
1168
1549
  }
1550
+
1551
+ // src/store/store.ts
1552
+ var RENAME_MAX_ATTEMPTS = 10;
1553
+ var RENAME_BACKOFF_STEP_MS = 10;
1554
+ var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOENT"]);
1555
+ function sleepSync(ms) {
1556
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1557
+ }
1558
+ function discardTemp(tmp) {
1559
+ try {
1560
+ rmSync(tmp, { force: true });
1561
+ } catch {
1562
+ }
1563
+ }
1564
+ function errnoOf(e) {
1565
+ return e.code ?? e.message;
1566
+ }
1567
+ function writeFileAtomic(path, contents) {
1568
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
1569
+ try {
1570
+ mkdirSync(dirname(path), { recursive: true });
1571
+ writeFileSync(tmp, contents, "utf8");
1572
+ } catch (e) {
1573
+ discardTemp(tmp);
1574
+ return err({ kind: "save-failed", path, detail: `writing the temp file failed: ${errnoOf(e)}` });
1575
+ }
1576
+ let attempt = 1;
1577
+ for (; ; ) {
1578
+ try {
1579
+ renameSync(tmp, path);
1580
+ return ok(void 0);
1581
+ } catch (e) {
1582
+ const code = errnoOf(e);
1583
+ if (!TRANSIENT_RENAME_CODES.has(code) || attempt >= RENAME_MAX_ATTEMPTS) {
1584
+ discardTemp(tmp);
1585
+ return err({ kind: "save-failed", path, detail: `${code} after ${attempt} attempt(s)` });
1586
+ }
1587
+ sleepSync(attempt * RENAME_BACKOFF_STEP_MS);
1588
+ attempt += 1;
1589
+ }
1590
+ }
1591
+ }
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
+ }
1598
+ var STORE_DIR_NAME = ".mellos";
1599
+ var STATE_FILE_RELATIVE_PATH = join(STORE_DIR_NAME, "map.json");
1600
+ var PAGES_DIR_NAME = "pages";
1601
+ function pageFilePath(defaultFile, page) {
1602
+ return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
1603
+ }
1604
+ function pageIdOfFile(defaultFile, path) {
1605
+ if (path === defaultFile) return void 0;
1606
+ const name = basename(path);
1607
+ return name.endsWith(".json") ? name.slice(0, -".json".length) : name;
1608
+ }
1609
+ function listPageFiles(defaultFile) {
1610
+ const out = [];
1611
+ if (existsSync(defaultFile)) out.push(defaultFile);
1612
+ let entries = [];
1613
+ try {
1614
+ entries = readdirSync(join(dirname(defaultFile), PAGES_DIR_NAME));
1615
+ } catch {
1616
+ }
1617
+ for (const e of entries.sort()) {
1618
+ if (e.endsWith(".json")) out.push(join(dirname(defaultFile), PAGES_DIR_NAME, e));
1619
+ }
1620
+ return out;
1621
+ }
1622
+ function deletePageFile(path) {
1623
+ try {
1624
+ rmSync(path, { force: true });
1625
+ return ok(void 0);
1626
+ } catch (e) {
1627
+ return err({ kind: "delete-failed", path, detail: errnoOf(e) });
1628
+ }
1629
+ }
1630
+ var FOCUS_FILE_NAME = "focus";
1631
+ function focusFilePath(defaultFile) {
1632
+ return join(dirname(defaultFile), FOCUS_FILE_NAME);
1633
+ }
1634
+ function takeFocusRequest(defaultFile) {
1635
+ const path = focusFilePath(defaultFile);
1636
+ let raw;
1637
+ try {
1638
+ raw = readFileSync(path, "utf8");
1639
+ } catch {
1640
+ return void 0;
1641
+ }
1642
+ try {
1643
+ rmSync(path, { force: true });
1644
+ } catch {
1645
+ }
1646
+ let parsed;
1647
+ try {
1648
+ parsed = JSON.parse(raw);
1649
+ } catch {
1650
+ return void 0;
1651
+ }
1652
+ if (typeof parsed !== "object" || parsed === null) return void 0;
1653
+ const page = parsed.page;
1654
+ if (page === void 0 || page === null) return { page: void 0 };
1655
+ if (typeof page !== "string") return void 0;
1656
+ const id = makePageId(page);
1657
+ return id.ok ? { page: id.value } : void 0;
1658
+ }
1659
+ var QUIT_FILE_NAME = "quit";
1660
+ function quitFilePath(defaultFile) {
1661
+ return join(dirname(defaultFile), QUIT_FILE_NAME);
1662
+ }
1663
+ function takeQuitRequest(defaultFile) {
1664
+ const path = quitFilePath(defaultFile);
1665
+ let raw;
1666
+ try {
1667
+ raw = readFileSync(path, "utf8");
1668
+ } catch {
1669
+ return false;
1670
+ }
1671
+ sweepQuitRequest(defaultFile);
1672
+ let parsed;
1673
+ try {
1674
+ parsed = JSON.parse(stripBom(raw));
1675
+ } catch {
1676
+ return false;
1677
+ }
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
+ `);
1699
+ }
1700
+ function retireViewer(defaultFile, pid) {
1701
+ try {
1702
+ rmSync(viewerFilePath(defaultFile, pid), { force: true });
1703
+ } catch {
1704
+ }
1705
+ }
1706
+ var CONFIG_FILE_NAME = "config.json";
1707
+ function configFilePath(defaultFile) {
1708
+ return join(dirname(defaultFile), CONFIG_FILE_NAME);
1709
+ }
1710
+ var LEGACY_STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
1711
+ var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
1712
+ var LEGACY_CONFIG_FILE_NAME = "mellos-mapping.config.json";
1713
+ 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));
1720
+ 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));
1725
+ return true;
1726
+ }
1169
1727
  function loadMapFile(path) {
1170
1728
  let text;
1171
1729
  try {
@@ -1177,7 +1735,7 @@ function loadMapFile(path) {
1177
1735
  }
1178
1736
  let raw;
1179
1737
  try {
1180
- raw = JSON.parse(text);
1738
+ raw = JSON.parse(stripBom(text));
1181
1739
  } catch (e) {
1182
1740
  return err({ kind: "malformed-json", path, detail: e.message });
1183
1741
  }
@@ -1188,14 +1746,18 @@ function loadMapFile(path) {
1188
1746
  var KEY_H_STEP = 4;
1189
1747
  var KEY_V_STEP = 2;
1190
1748
  var WHEEL_V_STEP = 3;
1749
+ var WHEEL_H_STEP = 4;
1191
1750
  var MOTION = 32;
1192
1751
  var WHEEL = 64;
1193
1752
  var SHIFT = 4;
1194
1753
  var BUTTON_BITS = 3;
1754
+ var WHEEL_BITS = 3;
1195
1755
  var SGR_MOUSE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
1196
1756
  var ARROW = /^\x1b\[([ABCD])/;
1197
1757
  var SHIFT_TAB = /^\x1b\[Z/;
1198
- var PARTIAL_ESCAPE = /(?:\x1b|\x1b\[|\x1b\[<[\d;]*)$/;
1758
+ var CSI_SEQUENCE = /^\x1b\[[0-9;:<=>?]*[ -/]*[@-~]/;
1759
+ var SS3_SEQUENCE = /^\x1bO[@-~]/;
1760
+ var PARTIAL_ESCAPE = /^(?:\x1b\[[0-9;:<=>?]*[ -/]*|\x1bO)$/;
1199
1761
  var ARROW_PAN = {
1200
1762
  A: { dx: 0, dy: -KEY_V_STEP },
1201
1763
  B: { dx: 0, dy: KEY_V_STEP },
@@ -1210,8 +1772,17 @@ var KEY_PAN = {
1210
1772
  };
1211
1773
  function mouseEvent(code, x, y, final) {
1212
1774
  if (code & WHEEL) {
1213
- const down = (code & 1) !== 0;
1214
- return code & SHIFT ? { kind: "pan", dx: 0, dy: (down ? 1 : -1) * WHEEL_V_STEP } : { kind: "zoom", delta: down ? -1 : 1, at: { x, y } };
1775
+ switch (code & WHEEL_BITS) {
1776
+ case 0:
1777
+ case 1: {
1778
+ const down = (code & 1) !== 0;
1779
+ return code & SHIFT ? { kind: "pan", dx: 0, dy: (down ? 1 : -1) * WHEEL_V_STEP } : { kind: "zoom", delta: down ? -1 : 1, at: { x, y } };
1780
+ }
1781
+ default: {
1782
+ const right = (code & 1) !== 0;
1783
+ return { kind: "pan", dx: (right ? 1 : -1) * WHEEL_H_STEP, dy: 0 };
1784
+ }
1785
+ }
1215
1786
  }
1216
1787
  const buttons = code & BUTTON_BITS;
1217
1788
  if (final === "m") return buttons === 0 ? { kind: "mouse-up", x, y } : void 0;
@@ -1223,7 +1794,6 @@ function mouseEvent(code, x, y, final) {
1223
1794
  return buttons === 0 ? { kind: "mouse-down", x, y } : void 0;
1224
1795
  }
1225
1796
  function parseInput(chunk) {
1226
- if (chunk === "\x1B") return { events: [{ kind: "clear" }], rest: "" };
1227
1797
  const events = [];
1228
1798
  let i = 0;
1229
1799
  while (i < chunk.length) {
@@ -1248,18 +1818,24 @@ function parseInput(chunk) {
1248
1818
  i += shiftTab[0].length;
1249
1819
  continue;
1250
1820
  }
1251
- const partial = PARTIAL_ESCAPE.exec(slice);
1252
- if (partial && partial.index === 0) {
1821
+ const sequence = CSI_SEQUENCE.exec(slice) ?? SS3_SEQUENCE.exec(slice);
1822
+ if (sequence) {
1823
+ i += sequence[0].length;
1824
+ continue;
1825
+ }
1826
+ if (PARTIAL_ESCAPE.test(slice)) {
1253
1827
  return { events, rest: slice };
1254
1828
  }
1255
1829
  const ch = chunk[i];
1256
- if (ch === "q" || ch === "Q" || ch === "" || ch === "") events.push({ kind: "quit" });
1830
+ if (ch === "\x1B") events.push({ kind: "clear" });
1831
+ else if (ch === "q" || ch === "Q" || ch === "" || ch === "") events.push({ kind: "quit" });
1257
1832
  else if (ch === "0") events.push({ kind: "reset" });
1258
1833
  else if (ch === "+" || ch === "=") events.push({ kind: "zoom", delta: 1 });
1259
1834
  else if (ch === "-") events.push({ kind: "zoom", delta: -1 });
1260
1835
  else if (ch === " ") events.push({ kind: "next-page" });
1261
1836
  else if (ch === "\x7F" || ch === "\b") events.push({ kind: "back" });
1262
1837
  else if (ch === "f" || ch === "F") events.push({ kind: "follow-toggle" });
1838
+ else if (ch === "x" || ch === "X") events.push({ kind: "delete-page" });
1263
1839
  else if (ch >= "1" && ch <= "9") events.push({ kind: "page", index: ch.charCodeAt(0) - "1".charCodeAt(0) });
1264
1840
  else if (KEY_PAN[ch]) events.push({ kind: "pan", ...KEY_PAN[ch] });
1265
1841
  i += 1;
@@ -1267,28 +1843,209 @@ function parseInput(chunk) {
1267
1843
  return { events, rest: "" };
1268
1844
  }
1269
1845
 
1846
+ // src/watch/pane-state.ts
1847
+ function describePageFault(fault) {
1848
+ return fault.kind === "unreadable" ? `cannot read ${fault.path}: ${fault.detail}` : describeStoreError(fault);
1849
+ }
1850
+ function isTransient(fault) {
1851
+ return fault.kind === "malformed-json";
1852
+ }
1853
+ function mapOf(entry) {
1854
+ if (entry === void 0 || entry.state.kind === "absent") return void 0;
1855
+ return entry.state.kind === "loaded" ? entry.state.map : entry.state.lastGood;
1856
+ }
1857
+ function mtimeOf(entry) {
1858
+ return entry === void 0 || entry.state.kind === "absent" ? void 0 : entry.state.mtimeMs;
1859
+ }
1860
+ function initialPaneState(follow, requestedFile) {
1861
+ return {
1862
+ pages: [],
1863
+ activeFile: void 0,
1864
+ pendingFocusFile: requestedFile,
1865
+ follow,
1866
+ diveStack: [],
1867
+ scanned: false,
1868
+ pendingDelete: void 0
1869
+ };
1870
+ }
1871
+ function entryOf(state, file) {
1872
+ return file === void 0 ? void 0 : state.pages.find((p) => p.file === file);
1873
+ }
1874
+ function mapsOf(state) {
1875
+ return new Map(state.pages.map((p) => [p.file, mapOf(p)]));
1876
+ }
1877
+ function filesOf(state) {
1878
+ return state.pages.map((p) => p.file);
1879
+ }
1880
+ function markViewed(state, file) {
1881
+ if (file === void 0 || !state.pages.some((p) => p.file === file && p.fresh)) return state;
1882
+ return { ...state, pages: state.pages.map((p) => p.file === file ? { ...p, fresh: false } : p) };
1883
+ }
1884
+ function userSwitch(state, file) {
1885
+ const followTurnedOff = state.follow;
1886
+ return {
1887
+ state: markViewed(
1888
+ // Looking elsewhere withdraws an armed deletion: it was aimed at the
1889
+ // page that was on screen, and the confirming press must never land on
1890
+ // whichever page took its place.
1891
+ { ...state, activeFile: file, pendingFocusFile: void 0, follow: false, pendingDelete: void 0 },
1892
+ file
1893
+ ),
1894
+ followTurnedOff
1895
+ };
1896
+ }
1897
+ function disarmDelete(state) {
1898
+ return state.pendingDelete === void 0 ? state : { ...state, pendingDelete: void 0 };
1899
+ }
1900
+ function requestDelete(state, now, windowMs) {
1901
+ const file = state.activeFile;
1902
+ if (file === void 0) return { state: disarmDelete(state), request: { kind: "none" } };
1903
+ if (entryOf(state, file)?.state.kind === "absent") {
1904
+ return { state: disarmDelete(state), request: { kind: "absent", file } };
1905
+ }
1906
+ const armed = state.pendingDelete;
1907
+ if (armed !== void 0 && armed.file === file && now <= armed.until) {
1908
+ return { state: { ...state, pendingDelete: void 0 }, request: { kind: "confirmed", file } };
1909
+ }
1910
+ const until = now + windowMs;
1911
+ return { state: { ...state, pendingDelete: { file, until } }, request: { kind: "armed", file, until } };
1912
+ }
1913
+ function toggleFollow(state) {
1914
+ return { ...state, follow: !state.follow };
1915
+ }
1916
+ function pushDive(state, file) {
1917
+ return { ...state, diveStack: [...state.diveStack, file] };
1918
+ }
1919
+ function popDive(state) {
1920
+ const files = new Set(filesOf(state));
1921
+ const stack = [...state.diveStack];
1922
+ while (stack.length > 0) {
1923
+ const parent = stack.pop();
1924
+ if (files.has(parent)) return { state: { ...state, diveStack: stack }, parent };
1925
+ }
1926
+ return { state: { ...state, diveStack: [] }, parent: void 0 };
1927
+ }
1928
+ function scan(state, input) {
1929
+ const first = !state.scanned;
1930
+ const previousActive = state.activeFile;
1931
+ const pages = [];
1932
+ const freshened = [];
1933
+ const changed = [];
1934
+ for (const file of input.files) {
1935
+ const held = entryOf(state, file);
1936
+ const mtimeMs = input.mtimeAt(file);
1937
+ if (mtimeMs === void 0) {
1938
+ pages.push(held ?? { file, state: { kind: "absent" }, fresh: false });
1939
+ continue;
1940
+ }
1941
+ const settled = held !== void 0 && mtimeOf(held) === mtimeMs && !(held.state.kind === "faulted" && held.state.transient);
1942
+ if (settled) {
1943
+ pages.push(held);
1944
+ continue;
1945
+ }
1946
+ const loaded = input.load(file);
1947
+ if (loaded.ok) {
1948
+ if (!first) changed.push(file);
1949
+ const fresh = !first && file !== previousActive;
1950
+ if (fresh) freshened.push(file);
1951
+ pages.push({ file, state: { kind: "loaded", map: loaded.value, mtimeMs }, fresh });
1952
+ continue;
1953
+ }
1954
+ pages.push({
1955
+ file,
1956
+ state: {
1957
+ kind: "faulted",
1958
+ fault: loaded.error,
1959
+ lastGood: mapOf(held),
1960
+ mtimeMs,
1961
+ transient: isTransient(loaded.error)
1962
+ },
1963
+ fresh: held?.fresh ?? false
1964
+ });
1965
+ }
1966
+ let pendingFocusFile = state.pendingFocusFile;
1967
+ if (input.focusRequest !== void 0 && !(first && pendingFocusFile !== void 0)) {
1968
+ pendingFocusFile = input.focusRequest;
1969
+ }
1970
+ let activeFile = previousActive;
1971
+ let requestApplied = false;
1972
+ if (pendingFocusFile !== void 0 && input.files.includes(pendingFocusFile)) {
1973
+ activeFile = pendingFocusFile;
1974
+ pendingFocusFile = void 0;
1975
+ requestApplied = true;
1976
+ }
1977
+ const mtimeIn = (file) => mtimeOf(pages.find((p) => p.file === file));
1978
+ if (state.follow && !requestApplied && changed.length > 0 && !input.engaged) {
1979
+ activeFile = mostRecentKey(changed, mtimeIn) ?? activeFile;
1980
+ }
1981
+ if (activeFile === void 0 || !input.files.includes(activeFile)) {
1982
+ activeFile = mostRecentKey(input.files, mtimeIn);
1983
+ }
1984
+ const files = new Set(input.files);
1985
+ const next = {
1986
+ pages,
1987
+ activeFile,
1988
+ pendingFocusFile,
1989
+ follow: state.follow,
1990
+ diveStack: state.diveStack.filter((f) => files.has(f)),
1991
+ scanned: true,
1992
+ // An armed deletion belongs to the page it was armed on. If the scan
1993
+ // moved the view (follow, a request, a page that vanished), the request
1994
+ // is stale — the confirming press must never hit a page that merely
1995
+ // arrived under the cursor.
1996
+ pendingDelete: state.pendingDelete?.file === activeFile ? state.pendingDelete : void 0
1997
+ };
1998
+ return { state: markViewed(next, activeFile), freshened };
1999
+ }
2000
+
1270
2001
  // src/watch/watch.ts
2002
+ function describeArgsError(e) {
2003
+ switch (e.kind) {
2004
+ case "unknown-flag":
2005
+ return `unknown flag "${e.flag}"`;
2006
+ case "missing-value":
2007
+ return `${e.flag} needs a value`;
2008
+ case "invalid-value":
2009
+ return `${e.flag} got "${e.raw}" (expected: ${e.rule})`;
2010
+ }
2011
+ }
2012
+ var USAGE = "usage: mellos-mapping-watch [--file <map.json>] [--page <slug>] [--interval <ms>] [--ascii] [--no-color] [--no-mouse] [--no-follow]";
1271
2013
  function parseArgs(argv, cwd) {
1272
2014
  let file = join2(cwd, STATE_FILE_RELATIVE_PATH);
1273
- let intervalMs = 250;
2015
+ let intervalMs = POLL_INTERVAL_DEFAULT_MS;
1274
2016
  let unicode = true;
1275
2017
  let color = true;
1276
2018
  let mouse = true;
1277
2019
  let page;
1278
2020
  let follow = true;
2021
+ const valueOf = (flag, raw) => raw === void 0 || raw.startsWith("--") ? err({ kind: "missing-value", flag }) : ok(raw);
1279
2022
  for (let i = 0; i < argv.length; i++) {
1280
- switch (argv[i]) {
1281
- case "--file":
1282
- file = argv[++i] ?? file;
2023
+ const flag = argv[i];
2024
+ switch (flag) {
2025
+ case "--file": {
2026
+ const value = valueOf(flag, argv[++i]);
2027
+ if (!value.ok) return value;
2028
+ file = value.value;
1283
2029
  break;
2030
+ }
1284
2031
  case "--page": {
1285
- const parsed = makePageId(argv[++i] ?? "");
1286
- if (parsed.ok) page = parsed.value;
2032
+ const value = valueOf(flag, argv[++i]);
2033
+ 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;
1287
2037
  break;
1288
2038
  }
1289
- case "--interval":
1290
- intervalMs = Math.max(50, Number(argv[++i]) || intervalMs);
2039
+ case "--interval": {
2040
+ const value = valueOf(flag, argv[++i]);
2041
+ if (!value.ok) return value;
2042
+ const ms = Number(value.value);
2043
+ if (!Number.isFinite(ms) || ms <= 0) {
2044
+ return err({ kind: "invalid-value", flag, raw: value.value, rule: "a positive number of milliseconds" });
2045
+ }
2046
+ intervalMs = Math.max(POLL_INTERVAL_MIN_MS, ms);
1291
2047
  break;
2048
+ }
1292
2049
  case "--ascii":
1293
2050
  unicode = false;
1294
2051
  break;
@@ -1302,10 +2059,24 @@ function parseArgs(argv, cwd) {
1302
2059
  follow = false;
1303
2060
  break;
1304
2061
  default:
1305
- break;
2062
+ return err({ kind: "unknown-flag", flag });
1306
2063
  }
1307
2064
  }
1308
- return { file, intervalMs, unicode, color, mouse, page, follow };
2065
+ return ok({ file, intervalMs, unicode, color, mouse, page, follow });
2066
+ }
2067
+ function readPage(file) {
2068
+ try {
2069
+ return loadMapFile(file);
2070
+ } catch (e) {
2071
+ return err({ kind: "unreadable", path: file, detail: e.code ?? e.message });
2072
+ }
2073
+ }
2074
+ function renderWindow(map, opts, viewport) {
2075
+ try {
2076
+ return ok(renderMapWindow(map, opts, viewport));
2077
+ } catch (e) {
2078
+ return err(`this map could not be drawn: ${e.message}`);
2079
+ }
1309
2080
  }
1310
2081
  function dividerRow(width, unicode, follow) {
1311
2082
  const grip = unicode ? " \u22EF " : " ~ ";
@@ -1319,18 +2090,6 @@ function dividerRow(width, unicode, follow) {
1319
2090
  }
1320
2091
  return bar;
1321
2092
  }
1322
- function mostRecentPageFile(files, mtimeOf) {
1323
- let best;
1324
- let bestMtime = -Infinity;
1325
- for (const file of files) {
1326
- const mtime = mtimeOf(file);
1327
- if (mtime !== void 0 && mtime > bestMtime) {
1328
- best = file;
1329
- bestMtime = mtime;
1330
- }
1331
- }
1332
- return best ?? files[0];
1333
- }
1334
2093
  var HIDE_CURSOR = "\x1B[?25l";
1335
2094
  var SHOW_CURSOR = "\x1B[?25h";
1336
2095
  var CLEAR_ALL = "\x1B[H\x1B[2J";
@@ -1339,9 +2098,23 @@ var ERASE_LINE_END = "\x1B[K";
1339
2098
  var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
1340
2099
  var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
1341
2100
  var RESET = "\x1B[0m";
2101
+ function terminalRestoreSequence(mouseActive) {
2102
+ return (mouseActive ? MOUSE_OFF : "") + SHOW_CURSOR + RESET + "\n";
2103
+ }
1342
2104
  var PANEL_CONTENT_ROWS = 6;
1343
2105
  var PANEL_ROWS_MIN = 2;
1344
2106
  var MAP_ROWS_MIN = 4;
2107
+ var POLL_INTERVAL_DEFAULT_MS = 250;
2108
+ var POLL_INTERVAL_MIN_MS = 50;
2109
+ var SPLASH_FRAME_MS = 80;
2110
+ var FLASH_ACK_MS = 2500;
2111
+ var FLASH_NOTICE_MS = 3e3;
2112
+ var FLASH_BACKGROUND_NEWS_MS = 4e3;
2113
+ var DOUBLE_CLICK_MS = 450;
2114
+ var CONFIRM_WINDOW_MS = 3e3;
2115
+ var FALLBACK_COLUMNS = 100;
2116
+ var FALLBACK_ROWS = 30;
2117
+ var DEFAULT_PAGE_TAB_LABEL = "main";
1345
2118
  function usableColumns(cols) {
1346
2119
  return Math.max(1, cols - 1);
1347
2120
  }
@@ -1352,18 +2125,6 @@ function clampPanelRows(wanted, totalRows, tabRows) {
1352
2125
  function panelRowsFromDividerY(termY, totalRows, tabRows) {
1353
2126
  return clampPanelRows(totalRows - termY - 1, totalRows, tabRows);
1354
2127
  }
1355
- var STATUS_GLYPH = {
1356
- planned: ["\xB7", "."],
1357
- "in-progress": ["\u283F", "*"],
1358
- done: ["\u25A0", "#"],
1359
- regressed: ["\u2717", "X"]
1360
- };
1361
- var STATUS_SGR = {
1362
- planned: "2",
1363
- "in-progress": "33",
1364
- done: "32",
1365
- regressed: "31"
1366
- };
1367
2128
  function anchorOffsets(anchor, offset, before, after) {
1368
2129
  if (anchor) {
1369
2130
  return {
@@ -1377,14 +2138,19 @@ function anchorOffsets(anchor, offset, before, after) {
1377
2138
  };
1378
2139
  }
1379
2140
  var TAB_INDICATOR_W = 3;
1380
- function pageTabRow(tabs, width, unicode, scroll = 0) {
2141
+ var CLOSE_TAB_TEXT = { unicode: "\xD7 ", ascii: "x " };
2142
+ var CLOSE_TAB_SGR = "90";
2143
+ function pageTabRow(tabs, width, unicode, scroll = 0, closable = false) {
1381
2144
  const texts = tabs.map((tab) => {
1382
2145
  const marker = tab.active ? unicode ? "\u25CF" : "*" : unicode ? "\u25CB" : "o";
1383
- const glyph = STATUS_GLYPH[tab.status][unicode ? 0 : 1];
2146
+ const glyph = statusGlyph(tab.status, unicode);
1384
2147
  return tab.neutral === true ? ` ${marker} ${tab.title} ` : ` ${marker} ${glyph} ${tab.title} `;
1385
2148
  });
1386
- const sgrOf = (tab) => tab.neutral === true ? tab.active ? "1" : tab.fresh ? "36" : "90" : tab.active ? `${STATUS_SGR[tab.status]};1` : tab.fresh ? STATUS_SGR[tab.status] : "90";
1387
- const widths = texts.map(displayWidth);
2149
+ const sgrOf = (tab) => tab.neutral === true ? tab.active ? "1" : tab.fresh ? "36" : "90" : tab.active ? `${statusSgr(tab.status)};1` : tab.fresh ? statusSgr(tab.status) : "90";
2150
+ const closeText = CLOSE_TAB_TEXT[unicode ? "unicode" : "ascii"];
2151
+ const closeW = displayWidth(closeText);
2152
+ const closeOf = (i) => closable && tabs[i].active ? closeW : 0;
2153
+ const widths = texts.map((t, i) => displayWidth(t) + closeOf(i));
1388
2154
  const count = tabs.length;
1389
2155
  let lo = 0;
1390
2156
  let hi = count - 1;
@@ -1404,37 +2170,37 @@ function pageTabRow(tabs, width, unicode, scroll = 0) {
1404
2170
  if (lo > 0) push(unicode ? " \u2039 " : " < ", "90", { kind: "scroll", delta: -1 });
1405
2171
  const tail = hi < count - 1 ? TAB_INDICATOR_W : 0;
1406
2172
  for (let i = lo; i <= hi; i++) {
1407
- push(fitWidth(texts[i], Math.max(1, width - (col - 1) - tail)), sgrOf(tabs[i]), { kind: "switch", index: i });
2173
+ const close = closeOf(i);
2174
+ push(fitWidth(texts[i], Math.max(1, width - (col - 1) - tail - close)), sgrOf(tabs[i]), {
2175
+ kind: "switch",
2176
+ index: i
2177
+ });
2178
+ if (close > 0 && col - 1 + close + tail <= width) push(closeText, CLOSE_TAB_SGR, { kind: "delete" });
1408
2179
  }
1409
2180
  if (hi < count - 1) push(unicode ? " \u203A " : " > ", "90", { kind: "scroll", delta: 1 });
1410
2181
  return segments;
1411
2182
  }
1412
- function tabScrollFor(tabs, width, unicode, scroll, index) {
2183
+ function tabScrollFor(tabs, width, unicode, scroll, index, closable = false) {
1413
2184
  if (index <= scroll) return Math.max(0, index);
1414
- const visibleAt = (s2) => pageTabRow(tabs, width, unicode, s2).some((seg) => seg.action.kind === "switch" && seg.action.index === index);
2185
+ const visibleAt = (s2) => pageTabRow(tabs, width, unicode, s2, closable).some(
2186
+ (seg) => seg.action.kind === "switch" && seg.action.index === index
2187
+ );
1415
2188
  let s = Math.max(0, Math.min(scroll, tabs.length - 1));
1416
2189
  while (s < index && !visibleAt(s)) s++;
1417
2190
  return s;
1418
2191
  }
1419
- function topLevelFiles(defaultFile, files, mapOf) {
1420
- const refs = /* @__PURE__ */ new Set();
1421
- for (const m of mapOf.values()) {
1422
- for (const n of m?.nodes ?? []) if (n.submap !== void 0) refs.add(n.submap);
1423
- }
2192
+ function topLevelFiles(defaultFile, files, mapOf2) {
2193
+ const interior = interiorPages(files.map((f) => [pageIdOfFile(defaultFile, f), mapOf2.get(f)]));
1424
2194
  return files.filter((f) => {
1425
2195
  const id = pageIdOfFile(defaultFile, f);
1426
- return id === void 0 || !refs.has(id);
2196
+ return id === void 0 || !interior.has(id);
1427
2197
  });
1428
2198
  }
1429
- function diveOrigin(defaultFile, file, files, mapOf) {
2199
+ function diveOrigin(defaultFile, file, files, mapOf2) {
1430
2200
  const id = pageIdOfFile(defaultFile, file);
1431
2201
  if (id === void 0) return void 0;
1432
- for (const f of files) {
1433
- if (f === file) continue;
1434
- const node = mapOf.get(f)?.nodes.find((n) => n.submap === id);
1435
- if (node !== void 0) return { parent: f, label: node.label };
1436
- }
1437
- return void 0;
2202
+ const entries = files.filter((f) => f !== file).map((f) => [f, mapOf2.get(f)]);
2203
+ return diveParent(entries, id);
1438
2204
  }
1439
2205
  function nearestHit(hits, cx, cy) {
1440
2206
  let best;
@@ -1449,37 +2215,23 @@ function nearestHit(hits, cx, cy) {
1449
2215
  return best;
1450
2216
  }
1451
2217
  function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_ROWS) {
1452
- const g = (s) => STATUS_GLYPH[s][unicode ? 0 : 1];
2218
+ const g = (s) => statusGlyph(s, unicode);
1453
2219
  const pinMark = pinned ? unicode ? " \u2299 pinned" : " * pinned" : "";
1454
- const group = map.groups.find((gr) => gr.id === focusId);
1455
- if (group) {
1456
- const members = map.nodes.filter((n) => n.group === group.id);
1457
- const memberIds = new Set(members.map((n) => n.id));
1458
- const status = groupStatus(map, group.id);
1459
- const layerName2 = map.layers.find((l) => l.id === group.layer)?.name ?? group.layer;
2220
+ const focus = focusInfo(map, focusId);
2221
+ if (focus === void 0) return void 0;
2222
+ const refText = (r) => `${g(r.status)} ${r.label}${r.edgeLabel !== void 0 ? ` (${r.edgeLabel})` : ""}`;
2223
+ if (focus.kind === "group") {
2224
+ const { group, status, layerName: layerName2, members } = focus;
1460
2225
  const [right2, left2] = unicode ? ["\u2192", "\u2190"] : ["->", "<-"];
1461
- const repLabel = (id) => {
1462
- const n = map.nodes.find((x) => x.id === id);
1463
- const owner = n.group !== void 0 ? map.groups.find((gr) => gr.id === n.group) : void 0;
1464
- return owner !== void 0 ? `${g(groupStatus(map, owner.id))} ${owner.label}` : `${g(n.status)} ${n.label}`;
1465
- };
1466
- const uses2 = [
1467
- ...new Set(
1468
- map.edges.filter((e) => memberIds.has(e.from) && !memberIds.has(e.to)).map((e) => repLabel(e.to))
1469
- )
1470
- ];
1471
- const usedBy2 = [
1472
- ...new Set(
1473
- map.edges.filter((e) => memberIds.has(e.to) && !memberIds.has(e.from)).map((e) => repLabel(e.from))
1474
- )
1475
- ];
2226
+ const uses2 = focus.uses.map(refText);
2227
+ const usedBy2 = focus.usedBy.map(refText);
1476
2228
  const lines2 = [
1477
2229
  {
1478
2230
  text: fitWidth(
1479
2231
  `${g(status)} ${group.label} [${group.id}] \xB7 ${layerName2} \xB7 ${status} \xB7 ${members.length} member(s)${pinMark}`,
1480
2232
  width
1481
2233
  ),
1482
- sgr: `${STATUS_SGR[status]};1`
2234
+ sgr: `${statusSgr(status)};1`
1483
2235
  },
1484
2236
  {
1485
2237
  text: fitWidth(`members: ${members.map((n) => `${g(n.status)} ${n.label}`).join(" ") || "\u2014"}`, width),
@@ -1491,21 +2243,13 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
1491
2243
  while (lines2.length < rows) lines2.push({ text: "", sgr: "" });
1492
2244
  return lines2.slice(0, rows);
1493
2245
  }
1494
- const node = map.nodes.find((n) => n.id === focusId);
1495
- if (!node) return void 0;
2246
+ const { node, layerName, laneLabel } = focus;
1496
2247
  const neutral = isNeutralKind(map);
1497
- const layerName = map.layers.find((l) => l.id === node.layer)?.name ?? node.layer;
1498
2248
  const [right, left] = unicode ? ["\u2192", "\u2190"] : ["->", "<-"];
1499
- const withGlyph = (id) => {
1500
- const n = map.nodes.find((x) => x.id === id);
1501
- return n ? `${g(n.status)} ${n.label}` : id;
1502
- };
1503
- const withEdgeLabel = (base, label) => label !== void 0 ? `${base} (${label})` : base;
1504
- const uses = map.edges.filter((e) => e.from === node.id).map((e) => withEdgeLabel(withGlyph(e.to), e.label));
1505
- const usedBy = map.edges.filter((e) => e.to === node.id).map((e) => withEdgeLabel(withGlyph(e.from), e.label));
1506
- const pin = pinned ? unicode ? " \u2299 pinned" : " * pinned" : "";
2249
+ const uses = focus.uses.map(refText);
2250
+ const usedBy = focus.usedBy.map(refText);
2251
+ const pin = pinMark;
1507
2252
  const headGlyph = neutral ? (node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0) ?? (unicode ? "\xB7" : ".") : g(node.status);
1508
- const laneLabel = node.lane !== void 0 ? map.lanes.find((l) => l.id === node.lane)?.label : void 0;
1509
2253
  const headParts = [
1510
2254
  `${headGlyph} ${node.label} [${node.id}]`,
1511
2255
  layerName,
@@ -1518,7 +2262,7 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
1518
2262
  const lines = [
1519
2263
  {
1520
2264
  text: fitWidth(`${headParts.join(" \xB7 ")}${pin}`, width),
1521
- sgr: neutral ? "1" : `${STATUS_SGR[node.status]};1`
2265
+ sgr: neutral ? "1" : `${statusSgr(node.status)};1`
1522
2266
  },
1523
2267
  { text: fitWidth(`evidence: ${node.evidence ?? "\u2014"}`, width), sgr: "90" },
1524
2268
  { text: fitWidth(`${usesWord} ${right} ${uses.join(" ") || "\u2014"}`, width), sgr: "" },
@@ -1535,17 +2279,6 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
1535
2279
  }
1536
2280
  return lines.slice(0, rows);
1537
2281
  }
1538
- var WATER_ROWS = 7;
1539
- var WATER_COLS_MAX = 60;
1540
- var SPLASH_SHADES = {
1541
- unicode: ["\u2591", "\u2591", "\u2592", "\u2592", "\u2593", "\u2593", "\u2588", "\u2588"],
1542
- ascii: [".", ".", ":", ":", "=", "=", "#", "#"]
1543
- };
1544
- var WAVE_RAMP = [17, 18, 19, 61, 24, 25, 31, 37, 44, 45, 51, 87, 123, 159, 195];
1545
- var SPINNER_FRAMES = {
1546
- unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
1547
- ascii: ["|", "/", "-", "\\"]
1548
- };
1549
2282
  function elapsedLabel(ms) {
1550
2283
  const s = Math.max(0, Math.floor(ms / 1e3));
1551
2284
  const m = Math.floor(s / 60);
@@ -1565,95 +2298,19 @@ function waitingInfo(s, width) {
1565
2298
  lines.push("the map appears at the first mmap_declare");
1566
2299
  return lines.map((l) => fitWidth(l, w));
1567
2300
  }
1568
- var WAVE_INTERVAL = 18;
1569
- var WAVE_LIFETIME = 64;
1570
- var WAVE_SPEED = 0.9;
1571
- var WAVE_ENVELOPE = 10;
1572
- var WAVE_NUMBER = 0.42;
1573
- var WAVE_LEVELS = 7;
1574
- var WAVE_GAIN = 4.5;
1575
- function waveHash(n) {
1576
- let h = Math.imul(n + 1, 2654435761) >>> 0;
1577
- h ^= h >>> 15;
1578
- h = Math.imul(h, 2246822519) >>> 0;
1579
- h ^= h >>> 13;
1580
- return h >>> 0;
1581
- }
1582
- var WAVE_CORNERS = [
1583
- [0, 0],
1584
- [1, 0],
1585
- [0, 1],
1586
- [1, 1]
1587
- ];
1588
- function liveRipples(frame, width, height) {
1589
- const out = [];
1590
- const first = Math.floor((frame - WAVE_LIFETIME - WAVE_INTERVAL) / WAVE_INTERVAL);
1591
- const last = Math.floor(frame / WAVE_INTERVAL);
1592
- for (let n = Math.max(0, first); n <= last; n++) {
1593
- const h = waveHash(n);
1594
- const age = frame - (n * WAVE_INTERVAL + h % WAVE_INTERVAL);
1595
- if (age < 0 || age > WAVE_LIFETIME) continue;
1596
- const [fx, fy] = WAVE_CORNERS[h % WAVE_CORNERS.length];
1597
- out.push({
1598
- ox: fx * (width - 1),
1599
- oy: fy * (height - 1),
1600
- r: age * WAVE_SPEED,
1601
- fade: 1 - age / WAVE_LIFETIME
1602
- });
1603
- }
1604
- return out;
1605
- }
1606
- function waveAt(ripples, x, y) {
1607
- let value = 0;
1608
- for (const w of ripples) {
1609
- const front = Math.hypot(x - w.ox, (y - w.oy) * 2) - w.r;
1610
- value += Math.cos(front * WAVE_NUMBER) * Math.exp(-(front * front) / (2 * WAVE_ENVELOPE ** 2)) * w.fade;
1611
- }
1612
- return value;
1613
- }
1614
- function waveLevel(value) {
1615
- return Math.max(-WAVE_LEVELS, Math.min(WAVE_LEVELS, Math.round(value * WAVE_GAIN)));
1616
- }
1617
2301
  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;
1620
- const mode = unicode ? "unicode" : "ascii";
1621
- const shades = SPLASH_SHADES[mode];
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)));
1626
- let out = "";
1627
- for (let i = 0; i < fieldW; ) {
1628
- const level = levels[i];
1629
- let 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
- }
1636
- i = j;
1637
- }
1638
- return out;
1639
- };
2302
+ if (width < 24 || height < info.length + 3) return void 0;
1640
2303
  const dim = (s) => color ? `\x1B[90m${s}${RESET}` : s;
1641
- const spinner = SPINNER_FRAMES[mode];
2304
+ const spinner = SPINNER_FRAMES[unicode ? "unicode" : "ascii"];
1642
2305
  const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
1643
2306
  const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
1644
2307
  const infoWidth = Math.max(0, ...info.map((l) => displayWidth(l)));
1645
2308
  const infoIndent = " ".repeat(Math.max(0, Math.floor((width - infoWidth) / 2)));
1646
- const block = [
1647
- ...Array.from({ length: WATER_ROWS }, (_, y) => indent + paintRow(y)),
1648
- "",
1649
- statusIndent + dim(status),
1650
- "",
1651
- ...info.map((l) => infoIndent + dim(l))
1652
- ];
2309
+ const block = [statusIndent + dim(status), "", ...info.map((l) => infoIndent + dim(l))];
1653
2310
  return [...Array.from({ length: Math.max(0, Math.floor((height - block.length) / 2)) }, () => ""), ...block];
1654
2311
  }
1655
2312
  function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
1656
- const g = (s) => STATUS_GLYPH[s][unicode ? 0 : 1];
2313
+ const g = (s) => statusGlyph(s, unicode);
1657
2314
  const count = (s) => map.nodes.filter((n) => n.status === s).length;
1658
2315
  const statuses = ["done", "in-progress", "planned", "regressed"];
1659
2316
  const counts = statuses.filter((s) => count(s) > 0).map((s) => `${g(s)} ${count(s)} ${s}`).join(" ");
@@ -1670,8 +2327,20 @@ function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
1670
2327
  while (lines.length < rows) lines.push({ text: "", sgr: "" });
1671
2328
  return lines.slice(0, rows);
1672
2329
  }
2330
+ function viewerReportOf(pane, defaultFile) {
2331
+ const shown = pane.activeFile ?? pane.pendingFocusFile ?? defaultFile;
2332
+ return { page: pageIdOfFile(defaultFile, shown), follow: pane.follow };
2333
+ }
1673
2334
  function main() {
1674
- const cfg = parseArgs(process.argv.slice(2), process.cwd());
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;
2342
+ if (migrateLegacyStore(cfg.file)) console.error("mellos-mapping: moved the legacy .claude map store to .mellos/ \u2014 commit the move.");
2343
+ sweepQuitRequest(cfg.file);
1675
2344
  const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
1676
2345
  const mouseActive = interactive && cfg.mouse;
1677
2346
  let lastFrame = "";
@@ -1683,13 +2352,11 @@ function main() {
1683
2352
  let notice = standbyNotice;
1684
2353
  let lastCols = process.stdout.columns ?? 0;
1685
2354
  let lastRows = process.stdout.rows ?? 0;
1686
- let pageFiles = [cfg.file];
1687
- const pageData = /* @__PURE__ */ new Map();
2355
+ let pane = initialPaneState(
2356
+ cfg.follow,
2357
+ cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page)
2358
+ );
1688
2359
  const pageViews = /* @__PURE__ */ new Map();
1689
- let activeFile;
1690
- let firstScan = true;
1691
- let pendingFocusFile = cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page);
1692
- let follow = cfg.follow;
1693
2360
  let lastTabSegments = [];
1694
2361
  let tabScroll = 0;
1695
2362
  let offsetX = 0;
@@ -1705,62 +2372,72 @@ function main() {
1705
2372
  let panelContentRows = PANEL_CONTENT_ROWS;
1706
2373
  let dividerDrag = false;
1707
2374
  let lastClick;
1708
- const diveStack = [];
1709
2375
  let flash;
1710
2376
  let lastTabFiles = [];
1711
- const maps = () => new Map([...pageData].map(([f, e]) => [f, e.map]));
1712
- const topFiles = () => topLevelFiles(cfg.file, pageFiles, maps());
1713
- const inSubmap = () => activeFile !== void 0 && !topFiles().includes(activeFile);
2377
+ const topFiles = () => topLevelFiles(cfg.file, filesOf(pane), mapsOf(pane));
2378
+ const inSubmap = () => pane.activeFile !== void 0 && !topFiles().includes(pane.activeFile);
1714
2379
  const tabRows = () => topFiles().length > 1 || inSubmap() ? 1 : 0;
1715
2380
  const climbBack = () => {
1716
- let parent = diveStack.pop();
1717
- while (parent !== void 0 && !pageFiles.includes(parent)) parent = diveStack.pop();
1718
- if (parent === void 0 && activeFile !== void 0) {
1719
- parent = diveOrigin(cfg.file, activeFile, pageFiles, maps())?.parent;
1720
- }
1721
- if (parent !== void 0 && parent !== activeFile) {
2381
+ const climbed = popDive(pane);
2382
+ pane = climbed.state;
2383
+ const parent = climbed.parent ?? (pane.activeFile !== void 0 ? diveOrigin(cfg.file, pane.activeFile, filesOf(pane), mapsOf(pane))?.parent : void 0);
2384
+ if (parent !== void 0 && parent !== pane.activeFile) {
1722
2385
  handSwitch(parent);
1723
2386
  return true;
1724
2387
  }
1725
2388
  return false;
1726
2389
  };
1727
- const viewWidth = () => usableColumns(process.stdout.columns ?? 100);
1728
- const viewHeight = () => Math.max(1, (process.stdout.rows ?? 30) - (1 + panelContentRows) - 1 - tabRows());
2390
+ const viewWidth = () => usableColumns(process.stdout.columns ?? FALLBACK_COLUMNS);
2391
+ const viewHeight = () => Math.max(1, (process.stdout.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
1729
2392
  const dividerY = () => tabRows() + viewHeight() + 1;
1730
2393
  const pageTabsOf = (files) => files.map((f) => {
1731
- const m = pageData.get(f)?.map;
2394
+ const entry = entryOf(pane, f);
2395
+ const m = mapOf(entry);
1732
2396
  return {
1733
- title: m?.title ?? (pageIdOfFile(cfg.file, f) ?? "main"),
2397
+ title: m?.title ?? (pageIdOfFile(cfg.file, f) ?? DEFAULT_PAGE_TAB_LABEL),
1734
2398
  status: m !== void 0 ? mapStatus(m) : "planned",
1735
- active: f === activeFile,
1736
- fresh: pageData.get(f)?.fresh ?? false,
2399
+ active: f === pane.activeFile,
2400
+ fresh: entry?.fresh ?? false,
1737
2401
  neutral: m !== void 0 && isNeutralKind(m)
1738
2402
  };
1739
2403
  });
1740
- const switchPage = (file) => {
1741
- if (activeFile !== void 0) pageViews.set(activeFile, { offsetX, offsetY, zoom, selectedId });
1742
- activeFile = file;
2404
+ const noticeFor = (file) => {
2405
+ const entry = entryOf(pane, file);
2406
+ if (entry === void 0 || entry.state.kind === "absent") {
2407
+ return file === void 0 || file === cfg.file ? standbyNotice : `waiting for ${file} ...`;
2408
+ }
2409
+ if (entry.state.kind === "loaded") return "";
2410
+ return entry.state.transient && entry.state.lastGood !== void 0 ? "" : describePageFault(entry.state.fault);
2411
+ };
2412
+ const adoptPage = () => {
2413
+ map = mapOf(entryOf(pane, pane.activeFile));
2414
+ notice = noticeFor(pane.activeFile);
2415
+ };
2416
+ const adoptView = (previous) => {
2417
+ const file = pane.activeFile;
2418
+ if (file === void 0 || file === previous) return;
2419
+ if (previous !== void 0) pageViews.set(previous, { offsetX, offsetY, zoom, selectedId });
1743
2420
  const view = pageViews.get(file);
1744
2421
  offsetX = view?.offsetX ?? 0;
1745
2422
  offsetY = view?.offsetY ?? 0;
1746
2423
  zoom = view?.zoom ?? ZOOM_DEFAULT;
1747
2424
  selectedId = view?.selectedId;
1748
2425
  hoverId = void 0;
1749
- const entry = pageData.get(file);
1750
- if (entry !== void 0 && entry.fresh) pageData.set(file, { ...entry, fresh: false });
1751
- map = entry?.map;
1752
- notice = map !== void 0 ? "" : entry?.error ?? (file === cfg.file ? standbyNotice : `waiting for ${file} ...`);
1753
2426
  const top = topFiles();
1754
2427
  const tabIndex = top.indexOf(file);
1755
- if (tabIndex >= 0) tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex);
2428
+ if (tabIndex >= 0) {
2429
+ tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex, mouseActive);
2430
+ }
1756
2431
  };
1757
2432
  const handSwitch = (file) => {
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 };
2433
+ const previous = pane.activeFile;
2434
+ const switched = userSwitch(pane, file);
2435
+ pane = switched.state;
2436
+ if (switched.followTurnedOff) {
2437
+ flash = { text: "auto-follow off \u2014 press f to re-enable", until: Date.now() + FLASH_NOTICE_MS };
1762
2438
  }
1763
- switchPage(file);
2439
+ adoptView(previous);
2440
+ adoptPage();
1764
2441
  };
1765
2442
  const hitTest = (termX, termY) => {
1766
2443
  const sx = termX - 1;
@@ -1771,41 +2448,57 @@ function main() {
1771
2448
  const cy = sy + offsetY;
1772
2449
  return lastHits.find((h) => cx >= h.x && cx < h.x + h.w && cy >= h.y && cy < h.y + h.h)?.id;
1773
2450
  };
1774
- process.stdout.write(HIDE_CURSOR + CLEAR_ALL + (mouseActive ? MOUSE_ON : ""));
1775
- const restore = () => {
1776
- process.stdout.write((mouseActive ? MOUSE_OFF : "") + SHOW_CURSOR + "\n");
1777
- process.exit(0);
2451
+ const publishPresence = () => {
2452
+ publishViewer(cfg.file, process.pid, viewerReportOf(pane, cfg.file));
1778
2453
  };
1779
- process.on("SIGINT", restore);
1780
- process.on("SIGTERM", restore);
1781
- const paint = () => {
1782
- const cols = process.stdout.columns ?? 100;
2454
+ process.stdout.write(HIDE_CURSOR + CLEAR_ALL + (mouseActive ? MOUSE_ON : ""));
2455
+ process.on("exit", () => {
2456
+ process.stdout.write(terminalRestoreSequence(mouseActive));
2457
+ retireViewer(cfg.file, process.pid);
2458
+ });
2459
+ const quit = () => process.exit(0);
2460
+ process.on("SIGINT", quit);
2461
+ process.on("SIGTERM", quit);
2462
+ process.on("uncaughtException", (e) => {
2463
+ process.stderr.write(`
2464
+ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2465
+ `);
2466
+ process.exit(1);
2467
+ });
2468
+ const paint2 = () => {
2469
+ const cols = process.stdout.columns ?? FALLBACK_COLUMNS;
1783
2470
  const viewW = viewWidth();
1784
- panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ?? 30, tabRows());
2471
+ panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ?? FALLBACK_ROWS, tabRows());
1785
2472
  const viewH = viewHeight();
1786
2473
  const focus = hoverId ?? selectedId;
1787
2474
  let body;
1788
2475
  let panned = "";
1789
2476
  let pannable = false;
1790
2477
  if (map !== void 0) {
1791
- const windowed = renderMapWindow(
2478
+ const rendered = renderWindow(
1792
2479
  map,
1793
2480
  { color: cfg.color, unicode: cfg.unicode, spinnerFrame, focus, zoom },
1794
2481
  { x: offsetX, y: offsetY, width: viewW, height: viewH }
1795
2482
  );
1796
- const maxX = Math.max(0, windowed.contentWidth - viewW);
1797
- const maxY = Math.max(0, windowed.contentHeight - viewH);
1798
- if (offsetX > maxX || offsetY > maxY || offsetX < 0 || offsetY < 0) {
1799
- offsetX = Math.min(Math.max(0, offsetX), maxX);
1800
- offsetY = Math.min(Math.max(0, offsetY), maxY);
1801
- paint();
1802
- return;
2483
+ if (!rendered.ok) {
2484
+ body = ["", fitWidth(` ! ${rendered.error}`, viewW), ""];
2485
+ lastHits = [];
2486
+ } else {
2487
+ const windowed = rendered.value;
2488
+ const maxX = Math.max(0, windowed.contentWidth - viewW);
2489
+ 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);
2493
+ paint2();
2494
+ return;
2495
+ }
2496
+ pannable = maxX > 0 || maxY > 0;
2497
+ body = windowed.lines;
2498
+ lastHits = windowed.hits;
2499
+ lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
2500
+ if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
1803
2501
  }
1804
- pannable = maxX > 0 || maxY > 0;
1805
- body = windowed.lines;
1806
- lastHits = windowed.hits;
1807
- lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
1808
- if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
1809
2502
  } else {
1810
2503
  const info = waitingInfo(
1811
2504
  {
@@ -1813,7 +2506,9 @@ function main() {
1813
2506
  pagesDir: join2(dirname2(cfg.file), PAGES_DIR_NAME),
1814
2507
  intervalMs: cfg.intervalMs,
1815
2508
  elapsedMs: interactive ? Date.now() - startedAt : void 0,
1816
- broken: [...pageData.values()].flatMap((e) => e.map === void 0 && e.error !== void 0 ? [e.error] : [])
2509
+ broken: pane.pages.flatMap(
2510
+ (p) => p.state.kind === "faulted" && p.state.lastGood === void 0 ? [describePageFault(p.state.fault)] : []
2511
+ )
1817
2512
  },
1818
2513
  Math.max(1, viewW - 2)
1819
2514
  );
@@ -1831,7 +2526,7 @@ function main() {
1831
2526
  } else {
1832
2527
  panel = mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
1833
2528
  }
1834
- const separator = dividerRow(viewW, cfg.unicode, follow);
2529
+ const separator = dividerRow(viewW, cfg.unicode, pane.follow);
1835
2530
  const panelRows = [
1836
2531
  cfg.color ? `\x1B[90m${separator}${RESET}` : separator,
1837
2532
  ...panel.map(
@@ -1840,12 +2535,12 @@ function main() {
1840
2535
  ];
1841
2536
  let tabLine;
1842
2537
  lastTabFiles = topFiles();
1843
- if (inSubmap() && activeFile !== void 0) {
1844
- const stackParent = [...diveStack].reverse().find((f) => pageFiles.includes(f));
1845
- const scanned = diveOrigin(cfg.file, activeFile, pageFiles, maps());
1846
- const parentFile = stackParent ?? scanned?.parent;
1847
- const parentTitle = parentFile !== void 0 ? pageData.get(parentFile)?.map?.title ?? (pageIdOfFile(cfg.file, parentFile) ?? "main") : "main";
1848
- const nodeLabel = scanned?.label ?? map?.title ?? "";
2538
+ if (inSubmap() && pane.activeFile !== void 0) {
2539
+ const stackParent = pane.diveStack[pane.diveStack.length - 1];
2540
+ const origin = diveOrigin(cfg.file, pane.activeFile, filesOf(pane), mapsOf(pane));
2541
+ const parentFile = stackParent ?? origin?.parent;
2542
+ const parentTitle = parentFile !== void 0 ? mapOf(entryOf(pane, parentFile))?.title ?? (pageIdOfFile(cfg.file, parentFile) ?? DEFAULT_PAGE_TAB_LABEL) : DEFAULT_PAGE_TAB_LABEL;
2543
+ const nodeLabel = origin?.label ?? map?.title ?? "";
1849
2544
  const crumbHead = ` ${cfg.unicode ? "\u232B" : "<"} ${parentTitle} ${cfg.unicode ? "\u25B8" : ">"} `;
1850
2545
  const head = { text: crumbHead, sgr: "90", lo: 1, hi: displayWidth(crumbHead), action: { kind: "back" } };
1851
2546
  const tailText = fitWidth(`${nodeLabel} `, Math.max(1, viewW - displayWidth(crumbHead)));
@@ -1859,14 +2554,14 @@ function main() {
1859
2554
  lastTabSegments = [head, tail];
1860
2555
  tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
1861
2556
  } else if (tabRows() > 0) {
1862
- const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll);
2557
+ const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll, mouseActive);
1863
2558
  lastTabSegments = segments;
1864
2559
  tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
1865
2560
  } else {
1866
2561
  lastTabSegments = [];
1867
2562
  }
1868
2563
  const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(zoom)}`;
1869
- 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 q quit";
2564
+ 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";
1870
2565
  const footerText = fitWidth(` ${hint}${panned}`, viewW);
1871
2566
  const footer = cfg.color ? `\x1B[90m${footerText}${RESET}` : footerText;
1872
2567
  let frame = HOME;
@@ -1884,89 +2579,87 @@ function main() {
1884
2579
  lastRows = process.stdout.rows ?? lastRows;
1885
2580
  lastFrame = "";
1886
2581
  process.stdout.write(CLEAR_ALL);
1887
- paint();
2582
+ paint2();
1888
2583
  };
1889
2584
  const tick = () => {
2585
+ if (takeQuitRequest(cfg.file)) quit();
1890
2586
  if ((process.stdout.columns ?? lastCols) !== lastCols || (process.stdout.rows ?? lastRows) !== lastRows) {
1891
2587
  handleResize();
1892
2588
  }
1893
2589
  const discovered = listPageFiles(cfg.file);
1894
- pageFiles = discovered.length > 0 ? discovered : [cfg.file];
1895
- for (const known of [...pageData.keys()]) {
1896
- if (!pageFiles.includes(known)) {
1897
- pageData.delete(known);
1898
- pageViews.delete(known);
1899
- }
1900
- }
1901
- const changedFiles = [];
1902
- for (const file of pageFiles) {
1903
- let mtimeMs;
1904
- try {
1905
- mtimeMs = statSync(file).mtimeMs;
1906
- } catch {
1907
- continue;
1908
- }
1909
- const entry = pageData.get(file);
1910
- if (mtimeMs === entry?.mtimeMs) continue;
1911
- const loaded = loadMapFile(file);
1912
- if (loaded.ok) {
1913
- if (!firstScan) changedFiles.push(file);
1914
- const becameFresh = !firstScan && file !== activeFile;
1915
- pageData.set(file, { map: loaded.value, mtimeMs, fresh: becameFresh });
1916
- if (file === activeFile) {
1917
- map = loaded.value;
1918
- notice = "";
1919
- } else if (becameFresh && !topFiles().includes(file)) {
1920
- const title = loaded.value.title ?? (pageIdOfFile(cfg.file, file) ?? "?");
1921
- flash = { text: `${cfg.unicode ? "\u229E " : ""}${title} updated`, until: Date.now() + 4e3 };
1922
- }
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);
1931
- } else {
1932
- pageData.set(file, { map: entry?.map, mtimeMs, fresh: entry?.fresh ?? false, error: describeStoreError(loaded.error) });
1933
- if (file === activeFile) notice = describeStoreError(loaded.error);
1934
- }
1935
- }
2590
+ const files = discovered.length > 0 ? discovered : [cfg.file];
1936
2591
  const request = takeFocusRequest(cfg.file);
1937
- if (request !== void 0 && !(firstScan && pendingFocusFile !== void 0)) {
1938
- pendingFocusFile = pageFilePath(cfg.file, request.page);
2592
+ const previous = pane.activeFile;
2593
+ const scanned = scan(pane, {
2594
+ files,
2595
+ mtimeAt: (file) => {
2596
+ try {
2597
+ return statSync2(file).mtimeMs;
2598
+ } catch {
2599
+ return void 0;
2600
+ }
2601
+ },
2602
+ load: readPage,
2603
+ focusRequest: request === void 0 ? void 0 : pageFilePath(cfg.file, request.page),
2604
+ // a drag in progress holds auto-follow off: the user is engaged with
2605
+ // THIS page, and a missed switch is re-triggered by the next save
2606
+ engaged: dragAnchor !== void 0
2607
+ });
2608
+ pane = scanned.state;
2609
+ for (const known of [...pageViews.keys()]) {
2610
+ if (!files.includes(known)) pageViews.delete(known);
1939
2611
  }
1940
- let requestApplied = false;
1941
- if (pendingFocusFile !== void 0 && pageFiles.includes(pendingFocusFile)) {
1942
- if (pendingFocusFile !== activeFile) switchPage(pendingFocusFile);
1943
- pendingFocusFile = void 0;
1944
- requestApplied = true;
2612
+ adoptView(previous);
2613
+ adoptPage();
2614
+ if (flash?.confirm === true && pane.pendingDelete === void 0) flash = void 0;
2615
+ const top = topFiles();
2616
+ for (const file of scanned.freshened) {
2617
+ if (top.includes(file)) continue;
2618
+ const title = mapOf(entryOf(pane, file))?.title ?? (pageIdOfFile(cfg.file, file) ?? "?");
2619
+ flash = { text: `${cfg.unicode ? "\u229E " : ""}${title} updated`, until: Date.now() + FLASH_BACKGROUND_NEWS_MS };
1945
2620
  }
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);
2621
+ if (pane.pages.some((p) => mapOf(p)?.nodes.some((n) => n.status === "in-progress"))) spinnerFrame++;
2622
+ if (flash !== void 0 && Date.now() > flash.until) flash = void 0;
2623
+ paint2();
2624
+ };
2625
+ const pageName = (file) => pageIdOfFile(cfg.file, file) ?? DEFAULT_PAGE_TAB_LABEL;
2626
+ const askDelete = () => {
2627
+ const now = Date.now();
2628
+ const asked = requestDelete(pane, now, CONFIRM_WINDOW_MS);
2629
+ pane = asked.state;
2630
+ if (asked.request.kind === "none") return;
2631
+ if (asked.request.kind === "absent") {
2632
+ flash = {
2633
+ text: `nothing to delete \u2014 ${pageName(asked.request.file)} has no file`,
2634
+ until: now + FLASH_NOTICE_MS
2635
+ };
2636
+ return;
1949
2637
  }
1950
- firstScan = false;
1951
- if (activeFile === void 0 || !pageFiles.includes(activeFile)) {
1952
- switchPage(mostRecentPageFile(pageFiles, (f) => pageData.get(f)?.mtimeMs));
2638
+ if (asked.request.kind === "armed") {
2639
+ flash = {
2640
+ text: `press x again to delete ${pageName(asked.request.file)} \u2014 its file is removed`,
2641
+ until: asked.request.until,
2642
+ confirm: true
2643
+ };
2644
+ return;
1953
2645
  }
1954
- if ([...pageData.values()].some((p) => p.map?.nodes.some((n) => n.status === "in-progress"))) spinnerFrame++;
1955
- if (flash !== void 0 && Date.now() > flash.until) flash = void 0;
1956
- paint();
2646
+ const file = asked.request.file;
2647
+ const removed = deletePageFile(file);
2648
+ flash = removed.ok ? { text: `deleted ${pageName(file)}`, until: now + FLASH_ACK_MS } : { text: describeStoreError(removed.error), until: now + FLASH_NOTICE_MS };
2649
+ tick();
1957
2650
  };
1958
2651
  if (interactive) {
1959
2652
  process.stdin.setRawMode(true);
1960
2653
  process.stdin.resume();
1961
2654
  process.stdin.setEncoding("utf8");
1962
2655
  process.stdin.on("data", (chunk) => {
1963
- const parsed = parseInput(pendingInput + chunk);
1964
- pendingInput = parsed.rest;
2656
+ const parsed2 = parseInput(pendingInput + chunk);
2657
+ pendingInput = parsed2.rest;
1965
2658
  let dirty = false;
1966
- for (const event of parsed.events) {
2659
+ for (const event of parsed2.events) {
1967
2660
  switch (event.kind) {
1968
2661
  case "quit":
1969
- restore();
2662
+ quit();
1970
2663
  return;
1971
2664
  case "reset":
1972
2665
  offsetX = 0;
@@ -1975,7 +2668,10 @@ function main() {
1975
2668
  dirty = true;
1976
2669
  break;
1977
2670
  case "clear":
1978
- if (selectedId !== void 0) selectedId = void 0;
2671
+ if (pane.pendingDelete !== void 0) {
2672
+ pane = disarmDelete(pane);
2673
+ flash = void 0;
2674
+ } else if (selectedId !== void 0) selectedId = void 0;
1979
2675
  else climbBack();
1980
2676
  dirty = true;
1981
2677
  break;
@@ -1995,11 +2691,16 @@ function main() {
1995
2691
  const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + viewWidth() / 2, offsetY + viewHeight() / 2)?.id;
1996
2692
  const before = lastHits.find((h) => h.id === anchorId);
1997
2693
  zoom = next;
1998
- const sized = renderMapWindow(
2694
+ const measured = renderWindow(
1999
2695
  map,
2000
2696
  { color: false, unicode: cfg.unicode, spinnerFrame: 0, zoom },
2001
2697
  { x: 0, y: 0, width: 0, height: 0 }
2002
2698
  );
2699
+ if (!measured.ok) {
2700
+ dirty = true;
2701
+ break;
2702
+ }
2703
+ const sized = measured.value;
2003
2704
  const after = before === void 0 ? void 0 : sized.hits.find((h) => h.id === before.id);
2004
2705
  const moved = anchorOffsets(
2005
2706
  before !== void 0 && after !== void 0 ? { before, after } : void 0,
@@ -2030,7 +2731,7 @@ function main() {
2030
2731
  break;
2031
2732
  case "mouse-drag":
2032
2733
  if (dividerDrag) {
2033
- const next = panelRowsFromDividerY(event.y, process.stdout.rows ?? 30, tabRows());
2734
+ const next = panelRowsFromDividerY(event.y, process.stdout.rows ?? FALLBACK_ROWS, tabRows());
2034
2735
  if (next !== panelContentRows) {
2035
2736
  panelContentRows = next;
2036
2737
  dirty = true;
@@ -2060,22 +2761,25 @@ function main() {
2060
2761
  climbBack();
2061
2762
  } else if (tabHit.action.kind === "scroll") {
2062
2763
  tabScroll = Math.max(0, Math.min(tabScroll + tabHit.action.delta, lastTabFiles.length - 1));
2764
+ } else if (tabHit.action.kind === "delete") {
2765
+ askDelete();
2063
2766
  } else {
2064
2767
  const target = lastTabFiles[tabHit.action.index];
2065
- if (target !== void 0 && target !== activeFile) handSwitch(target);
2768
+ if (target !== void 0 && target !== pane.activeFile) handSwitch(target);
2066
2769
  }
2067
2770
  } else {
2068
2771
  const id = hitTest(event.x, event.y);
2069
2772
  const now = Date.now();
2070
- if (id !== void 0 && lastClick?.id === id && now - lastClick.at <= 450) {
2773
+ if (id !== void 0 && lastClick?.id === id && now - lastClick.at <= DOUBLE_CLICK_MS) {
2071
2774
  const submap = map?.nodes.find((n) => n.id === id)?.submap;
2072
- if (submap !== void 0 && activeFile !== void 0) {
2775
+ if (submap !== void 0 && pane.activeFile !== void 0) {
2073
2776
  const target = pageFilePath(cfg.file, submap);
2074
- if (pageFiles.includes(target) && target !== activeFile) {
2075
- diveStack.push(activeFile);
2777
+ const files = filesOf(pane);
2778
+ if (files.includes(target) && target !== pane.activeFile) {
2779
+ pane = pushDive(pane, pane.activeFile);
2076
2780
  handSwitch(target);
2077
- } else if (!pageFiles.includes(target)) {
2078
- flash = { text: `submap "${submap}" has no page yet`, until: now + 2500 };
2781
+ } else if (!files.includes(target)) {
2782
+ flash = { text: `submap "${submap}" has no page yet`, until: now + FLASH_ACK_MS };
2079
2783
  }
2080
2784
  }
2081
2785
  lastClick = void 0;
@@ -2092,11 +2796,11 @@ function main() {
2092
2796
  case "next-page":
2093
2797
  case "prev-page": {
2094
2798
  const top = topFiles();
2095
- if (top.length > 0 && activeFile !== void 0) {
2096
- const current = top.indexOf(activeFile);
2799
+ if (top.length > 0 && pane.activeFile !== void 0) {
2800
+ const current = top.indexOf(pane.activeFile);
2097
2801
  const step = event.kind === "next-page" ? 1 : -1;
2098
2802
  const target = top[(current + step + top.length) % top.length];
2099
- if (target !== activeFile) {
2803
+ if (target !== pane.activeFile) {
2100
2804
  handSwitch(target);
2101
2805
  dirty = true;
2102
2806
  }
@@ -2105,7 +2809,7 @@ function main() {
2105
2809
  }
2106
2810
  case "page": {
2107
2811
  const target = topFiles()[event.index];
2108
- if (target !== void 0 && target !== activeFile) {
2812
+ if (target !== void 0 && target !== pane.activeFile) {
2109
2813
  handSwitch(target);
2110
2814
  dirty = true;
2111
2815
  }
@@ -2115,24 +2819,30 @@ function main() {
2115
2819
  if (climbBack()) dirty = true;
2116
2820
  break;
2117
2821
  case "follow-toggle":
2118
- follow = !follow;
2119
- flash = { text: follow ? "auto-follow on" : "auto-follow off", until: Date.now() + 2500 };
2822
+ pane = toggleFollow(pane);
2823
+ flash = { text: pane.follow ? "auto-follow on" : "auto-follow off", until: Date.now() + FLASH_ACK_MS };
2824
+ dirty = true;
2825
+ break;
2826
+ case "delete-page":
2827
+ askDelete();
2120
2828
  dirty = true;
2121
2829
  break;
2122
2830
  }
2123
2831
  }
2124
- if (dirty) paint();
2832
+ if (dirty) paint2();
2125
2833
  });
2126
2834
  process.stdout.on("resize", handleResize);
2127
2835
  }
2128
2836
  tick();
2129
2837
  setInterval(tick, cfg.intervalMs);
2838
+ publishPresence();
2839
+ setInterval(publishPresence, VIEWER_HEARTBEAT_MS);
2130
2840
  if (interactive) {
2131
2841
  setInterval(() => {
2132
2842
  if (map !== void 0) return;
2133
2843
  splashTick++;
2134
- paint();
2135
- }, 80);
2844
+ paint2();
2845
+ }, SPLASH_FRAME_MS);
2136
2846
  }
2137
2847
  }
2138
2848
  function launchedAsEntry(argv1, moduleUrl) {
@@ -2148,30 +2858,30 @@ if (launchedAsEntry(process.argv[1], import.meta.url)) {
2148
2858
  }
2149
2859
  export {
2150
2860
  PANEL_ROWS_MIN,
2151
- WAVE_LEVELS,
2152
- WAVE_NUMBER,
2861
+ USAGE,
2153
2862
  anchorOffsets,
2154
2863
  clampPanelRows,
2864
+ describeArgsError,
2865
+ describePageFault,
2155
2866
  diveOrigin,
2156
2867
  dividerRow,
2157
2868
  elapsedLabel,
2158
2869
  fitWidth,
2159
2870
  launchedAsEntry,
2160
- liveRipples,
2161
2871
  mapPanel,
2162
- mostRecentPageFile,
2163
2872
  nearestHit,
2164
2873
  nodePanel,
2165
2874
  pageTabRow,
2166
2875
  panelRowsFromDividerY,
2167
2876
  parseArgs,
2877
+ readPage,
2878
+ renderWindow,
2168
2879
  splashFrame,
2169
2880
  tabScrollFor,
2881
+ terminalRestoreSequence,
2170
2882
  topLevelFiles,
2171
2883
  usableColumns,
2884
+ viewerReportOf,
2172
2885
  waitingInfo,
2173
- waveAt,
2174
- waveHash,
2175
- waveLevel,
2176
2886
  wrapWidth
2177
2887
  };