mellos-mapping 0.12.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/README.zh-CN.md +8 -3
- package/dist/server.mjs +12 -10
- package/dist/watch.mjs +233 -50
- package/package.json +3 -2
- package/scripts/open-pane.mjs +249 -0
package/README.md
CHANGED
|
@@ -125,7 +125,10 @@ session's working directory instead: the state file lands in your project,
|
|
|
125
125
|
same as under Claude Code. The registered path is version-specific — re-run
|
|
126
126
|
the script after updating the plugin.
|
|
127
127
|
|
|
128
|
-
To watch the live pane beside a Codex session, run
|
|
128
|
+
To watch the live pane beside a Codex session on Windows, run
|
|
129
|
+
`node <plugin root>/scripts/open-pane.mjs <project dir>` — it splits the
|
|
130
|
+
terminal window hosting the session (or falls back to a dedicated
|
|
131
|
+
"mellos-mapping" window; `--window` picks that on purpose). Elsewhere run
|
|
129
132
|
`node <plugin root>/dist/watch.mjs` from the project directory in a second
|
|
130
133
|
terminal (or any terminal split).
|
|
131
134
|
|
|
@@ -154,7 +157,10 @@ get the four `mmap_*` tools and the pane, and bring their own prompting.
|
|
|
154
157
|
declare the ghost design and keep the map current as it works.
|
|
155
158
|
2. Run `/mellos-mapping:mmap` to open the live pane (Windows Terminal split
|
|
156
159
|
on Windows, tmux split inside tmux, or a printed command to run in any
|
|
157
|
-
second terminal).
|
|
160
|
+
second terminal). On Windows the pane opens in the terminal window
|
|
161
|
+
hosting YOUR session, even with several windows open; pass `--window` to
|
|
162
|
+
put the map in its own dedicated window instead. Prefer `--ascii` if
|
|
163
|
+
your font lacks box-drawing glyphs.
|
|
158
164
|
3. Watch nodes light up from the bottom. Interrupt when the picture worries
|
|
159
165
|
you — that is what it is for.
|
|
160
166
|
|
package/README.zh-CN.md
CHANGED
|
@@ -115,8 +115,11 @@ node ~/.codex/plugins/cache/mellos-mapping/mellos-mapping/<版本>/scripts/codex
|
|
|
115
115
|
项目里,与 Claude Code 下行为一致。注册的是版本相关的绝对路径——插件
|
|
116
116
|
更新后重跑一次脚本即可。
|
|
117
117
|
|
|
118
|
-
要在 Codex
|
|
119
|
-
|
|
118
|
+
要在 Codex 会话旁边看实况面板,Windows 上运行
|
|
119
|
+
`node <插件根>/scripts/open-pane.mjs <项目目录>`——它会在承载本会话的
|
|
120
|
+
终端窗口里分屏(识别不到就确定性地开到专属的 "mellos-mapping" 窗口;
|
|
121
|
+
`--window` 则是主动选择专属窗口)。其他环境在项目目录下的第二个终端
|
|
122
|
+
(或任意分屏)运行 `node <插件根>/dist/watch.mjs`。
|
|
120
123
|
|
|
121
124
|
## 任意 MCP 客户端
|
|
122
125
|
|
|
@@ -143,7 +146,9 @@ npx -y -p mellos-mapping mellos-mapping-watch
|
|
|
143
146
|
设计,并在工作过程中保持地图与现实一致。
|
|
144
147
|
2. 运行 `/mellos-mapping:mmap` 打开实况分屏(Windows 上是 Windows
|
|
145
148
|
Terminal 分屏,tmux 里是 tmux 分屏,其他环境会打印一条命令让你在
|
|
146
|
-
|
|
149
|
+
第二个终端里运行)。Windows 上即使开着多个终端窗口,分屏也会落在
|
|
150
|
+
**你的会话所在的窗口**;想让地图独占一个窗口就加 `--window`。字体
|
|
151
|
+
缺少制表符字形时用 `--ascii`。
|
|
147
152
|
3. 看着节点从底部一路亮起。图让你不安的时候就打断它——这正是它存在的
|
|
148
153
|
意义。
|
|
149
154
|
|
package/dist/server.mjs
CHANGED
|
@@ -21390,7 +21390,7 @@ function removeLayer(map, id) {
|
|
|
21390
21390
|
|
|
21391
21391
|
// src/render/render.ts
|
|
21392
21392
|
var ZOOM_MIN = -4;
|
|
21393
|
-
var ZOOM_MAX =
|
|
21393
|
+
var ZOOM_MAX = 2;
|
|
21394
21394
|
var ZOOM_DEFAULT = 0;
|
|
21395
21395
|
function clampZoom(n) {
|
|
21396
21396
|
return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.round(n)));
|
|
@@ -21668,10 +21668,14 @@ function neutralSkin(unicode) {
|
|
|
21668
21668
|
var BOX_H = 3;
|
|
21669
21669
|
var BOX_GAP = 2;
|
|
21670
21670
|
var LEFT_MARGIN = 2;
|
|
21671
|
+
var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
|
|
21672
|
+
var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
|
|
21671
21673
|
function zoomGeometry(zoom) {
|
|
21672
21674
|
switch (zoom) {
|
|
21675
|
+
case 2:
|
|
21676
|
+
return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
|
|
21673
21677
|
case 1:
|
|
21674
|
-
return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
|
|
21678
|
+
return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
|
|
21675
21679
|
case 0:
|
|
21676
21680
|
return { mode: "boxes", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
|
|
21677
21681
|
case -1:
|
|
@@ -21684,9 +21688,6 @@ function zoomGeometry(zoom) {
|
|
|
21684
21688
|
return { mode: "constellation", scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
|
|
21685
21689
|
}
|
|
21686
21690
|
}
|
|
21687
|
-
var DETAIL_INNER_MIN = 22;
|
|
21688
|
-
var DETAIL_INNER_MAX = 32;
|
|
21689
|
-
var DETAIL_NOTE_ROWS = 3;
|
|
21690
21691
|
var LABEL_BUDGET_MIN = 4;
|
|
21691
21692
|
function boxSpec(node, geo, unicode, neutral) {
|
|
21692
21693
|
const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
|
|
@@ -21696,14 +21697,15 @@ function boxSpec(node, geo, unicode, neutral) {
|
|
|
21696
21697
|
if (geo.mode === "constellation") {
|
|
21697
21698
|
return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
|
|
21698
21699
|
}
|
|
21699
|
-
if (geo.mode === "detail") {
|
|
21700
|
-
const
|
|
21700
|
+
if (geo.mode === "detail" && geo.detail !== void 0) {
|
|
21701
|
+
const budget2 = geo.detail;
|
|
21702
|
+
const innerW = Math.min(Math.max(displayWidth(text2) + badgeW + 4, budget2.innerMin), budget2.innerMax);
|
|
21701
21703
|
const extra = [];
|
|
21702
21704
|
if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
|
|
21703
21705
|
if (node.detail !== void 0) {
|
|
21704
21706
|
const wrapped = wrapWidth(node.detail, innerW - 2);
|
|
21705
|
-
for (let i = 0; i < Math.min(wrapped.length,
|
|
21706
|
-
const cut = i ===
|
|
21707
|
+
for (let i = 0; i < Math.min(wrapped.length, budget2.noteRows); i++) {
|
|
21708
|
+
const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
|
|
21707
21709
|
extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
|
|
21708
21710
|
}
|
|
21709
21711
|
}
|
|
@@ -22513,7 +22515,7 @@ function summarize(map) {
|
|
|
22513
22515
|
|
|
22514
22516
|
// src/server/server.ts
|
|
22515
22517
|
var SERVER_NAME = "mellos-mapping";
|
|
22516
|
-
var SERVER_VERSION = "0.
|
|
22518
|
+
var SERVER_VERSION = "0.15.0";
|
|
22517
22519
|
var ID = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "lowercase letters, digits and dashes, 1-64 chars").describe("stable kebab-case identifier");
|
|
22518
22520
|
var PAGE = ID.optional().describe(
|
|
22519
22521
|
"page (parallel map) this call targets; omit for the default page. One effort = one page: start a NEW effort on its own page named after the effort, so concurrent sessions never write over each other and the pane can switch between pages."
|
package/dist/watch.mjs
CHANGED
|
@@ -201,13 +201,15 @@ function updateNode(map, input) {
|
|
|
201
201
|
|
|
202
202
|
// src/render/render.ts
|
|
203
203
|
var ZOOM_MIN = -4;
|
|
204
|
-
var ZOOM_MAX =
|
|
204
|
+
var ZOOM_MAX = 2;
|
|
205
205
|
var ZOOM_DEFAULT = 0;
|
|
206
206
|
function clampZoom(n) {
|
|
207
207
|
return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.round(n)));
|
|
208
208
|
}
|
|
209
209
|
function zoomLabel(zoom) {
|
|
210
210
|
switch (zoom) {
|
|
211
|
+
case 2:
|
|
212
|
+
return "detail+";
|
|
211
213
|
case 1:
|
|
212
214
|
return "detail";
|
|
213
215
|
case 0:
|
|
@@ -495,10 +497,14 @@ function neutralSkin(unicode) {
|
|
|
495
497
|
var BOX_H = 3;
|
|
496
498
|
var BOX_GAP = 2;
|
|
497
499
|
var LEFT_MARGIN = 2;
|
|
500
|
+
var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
|
|
501
|
+
var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
|
|
498
502
|
function zoomGeometry(zoom) {
|
|
499
503
|
switch (zoom) {
|
|
504
|
+
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 };
|
|
500
506
|
case 1:
|
|
501
|
-
return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
|
|
507
|
+
return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
|
|
502
508
|
case 0:
|
|
503
509
|
return { mode: "boxes", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
|
|
504
510
|
case -1:
|
|
@@ -511,9 +517,6 @@ function zoomGeometry(zoom) {
|
|
|
511
517
|
return { mode: "constellation", scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
|
|
512
518
|
}
|
|
513
519
|
}
|
|
514
|
-
var DETAIL_INNER_MIN = 22;
|
|
515
|
-
var DETAIL_INNER_MAX = 32;
|
|
516
|
-
var DETAIL_NOTE_ROWS = 3;
|
|
517
520
|
var LABEL_BUDGET_MIN = 4;
|
|
518
521
|
function boxSpec(node, geo, unicode, neutral) {
|
|
519
522
|
const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
|
|
@@ -523,14 +526,15 @@ function boxSpec(node, geo, unicode, neutral) {
|
|
|
523
526
|
if (geo.mode === "constellation") {
|
|
524
527
|
return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
|
|
525
528
|
}
|
|
526
|
-
if (geo.mode === "detail") {
|
|
527
|
-
const
|
|
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);
|
|
528
532
|
const extra = [];
|
|
529
533
|
if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
|
|
530
534
|
if (node.detail !== void 0) {
|
|
531
535
|
const wrapped = wrapWidth(node.detail, innerW - 2);
|
|
532
|
-
for (let i = 0; i < Math.min(wrapped.length,
|
|
533
|
-
const cut = i ===
|
|
536
|
+
for (let i = 0; i < Math.min(wrapped.length, budget2.noteRows); i++) {
|
|
537
|
+
const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
|
|
534
538
|
extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
|
|
535
539
|
}
|
|
536
540
|
}
|
|
@@ -1175,7 +1179,7 @@ var KEY_PAN = {
|
|
|
1175
1179
|
function mouseEvent(code, x, y, final) {
|
|
1176
1180
|
if (code & WHEEL) {
|
|
1177
1181
|
const down = (code & 1) !== 0;
|
|
1178
|
-
return code & SHIFT ? { kind: "pan", dx: 0, dy: (down ? 1 : -1) * WHEEL_V_STEP } : { kind: "zoom", delta: down ? -1 : 1 };
|
|
1182
|
+
return code & SHIFT ? { kind: "pan", dx: 0, dy: (down ? 1 : -1) * WHEEL_V_STEP } : { kind: "zoom", delta: down ? -1 : 1, at: { x, y } };
|
|
1179
1183
|
}
|
|
1180
1184
|
const buttons = code & BUTTON_BITS;
|
|
1181
1185
|
if (final === "m") return buttons === 0 ? { kind: "mouse-up", x, y } : void 0;
|
|
@@ -1271,6 +1275,9 @@ var RESET = "\x1B[0m";
|
|
|
1271
1275
|
var PANEL_CONTENT_ROWS = 6;
|
|
1272
1276
|
var PANEL_ROWS_MIN = 2;
|
|
1273
1277
|
var MAP_ROWS_MIN = 4;
|
|
1278
|
+
function usableColumns(cols) {
|
|
1279
|
+
return Math.max(1, cols - 1);
|
|
1280
|
+
}
|
|
1274
1281
|
function clampPanelRows(wanted, totalRows, tabRows) {
|
|
1275
1282
|
const largest = totalRows - tabRows - MAP_ROWS_MIN - 2;
|
|
1276
1283
|
return Math.max(PANEL_ROWS_MIN, Math.min(wanted, largest));
|
|
@@ -1302,27 +1309,46 @@ function anchorOffsets(anchor, offset, before, after) {
|
|
|
1302
1309
|
y: before.h > 0 ? Math.round(offset.y * after.h / before.h) : 0
|
|
1303
1310
|
};
|
|
1304
1311
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
for (const [index, tab] of tabs.entries()) {
|
|
1309
|
-
const room = width - (col - 1);
|
|
1310
|
-
if (room <= 3) break;
|
|
1312
|
+
var TAB_INDICATOR_W = 3;
|
|
1313
|
+
function pageTabRow(tabs, width, unicode, scroll = 0) {
|
|
1314
|
+
const texts = tabs.map((tab) => {
|
|
1311
1315
|
const marker = tab.active ? unicode ? "\u25CF" : "*" : unicode ? "\u25CB" : "o";
|
|
1312
1316
|
const glyph = STATUS_GLYPH[tab.status][unicode ? 0 : 1];
|
|
1313
|
-
|
|
1317
|
+
return tab.neutral === true ? ` ${marker} ${tab.title} ` : ` ${marker} ${glyph} ${tab.title} `;
|
|
1318
|
+
});
|
|
1319
|
+
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";
|
|
1320
|
+
const widths = texts.map(displayWidth);
|
|
1321
|
+
const count = tabs.length;
|
|
1322
|
+
let lo = 0;
|
|
1323
|
+
let hi = count - 1;
|
|
1324
|
+
if (widths.reduce((a, b) => a + b, 0) > width) {
|
|
1325
|
+
lo = Math.max(0, Math.min(scroll, count - 1));
|
|
1326
|
+
hi = lo;
|
|
1327
|
+
const cost = (l, h) => widths.slice(l, h + 1).reduce((a, b) => a + b, 0) + (l > 0 ? TAB_INDICATOR_W : 0) + (h < count - 1 ? TAB_INDICATOR_W : 0);
|
|
1328
|
+
while (hi + 1 < count && cost(lo, hi + 1) <= width) hi++;
|
|
1329
|
+
}
|
|
1330
|
+
const segments = [];
|
|
1331
|
+
let col = 1;
|
|
1332
|
+
const push = (text, sgr, action) => {
|
|
1314
1333
|
const w = displayWidth(text);
|
|
1315
|
-
segments.push({
|
|
1316
|
-
text,
|
|
1317
|
-
sgr: tab.neutral === true ? tab.active ? "1" : tab.fresh ? "36" : "90" : tab.active ? `${STATUS_SGR[tab.status]};1` : tab.fresh ? STATUS_SGR[tab.status] : "90",
|
|
1318
|
-
lo: col,
|
|
1319
|
-
hi: col + w - 1,
|
|
1320
|
-
index
|
|
1321
|
-
});
|
|
1334
|
+
segments.push({ text, sgr, lo: col, hi: col + w - 1, action });
|
|
1322
1335
|
col += w;
|
|
1336
|
+
};
|
|
1337
|
+
if (lo > 0) push(unicode ? " \u2039 " : " < ", "90", { kind: "scroll", delta: -1 });
|
|
1338
|
+
const tail = hi < count - 1 ? TAB_INDICATOR_W : 0;
|
|
1339
|
+
for (let i = lo; i <= hi; i++) {
|
|
1340
|
+
push(fitWidth(texts[i], Math.max(1, width - (col - 1) - tail)), sgrOf(tabs[i]), { kind: "switch", index: i });
|
|
1323
1341
|
}
|
|
1342
|
+
if (hi < count - 1) push(unicode ? " \u203A " : " > ", "90", { kind: "scroll", delta: 1 });
|
|
1324
1343
|
return segments;
|
|
1325
1344
|
}
|
|
1345
|
+
function tabScrollFor(tabs, width, unicode, scroll, index) {
|
|
1346
|
+
if (index <= scroll) return Math.max(0, index);
|
|
1347
|
+
const visibleAt = (s2) => pageTabRow(tabs, width, unicode, s2).some((seg) => seg.action.kind === "switch" && seg.action.index === index);
|
|
1348
|
+
let s = Math.max(0, Math.min(scroll, tabs.length - 1));
|
|
1349
|
+
while (s < index && !visibleAt(s)) s++;
|
|
1350
|
+
return s;
|
|
1351
|
+
}
|
|
1326
1352
|
function topLevelFiles(defaultFile, files, mapOf) {
|
|
1327
1353
|
const refs = /* @__PURE__ */ new Set();
|
|
1328
1354
|
for (const m of mapOf.values()) {
|
|
@@ -1442,6 +1468,131 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
|
|
|
1442
1468
|
}
|
|
1443
1469
|
return lines.slice(0, rows);
|
|
1444
1470
|
}
|
|
1471
|
+
var SPLASH_FONT = {
|
|
1472
|
+
M: ["# #", "## ##", "# # #", "# #", "# #"],
|
|
1473
|
+
E: ["####", "#", "###", "#", "####"],
|
|
1474
|
+
L: ["#", "#", "#", "#", "####"],
|
|
1475
|
+
O: [" ###", "# #", "# #", "# #", " ###"],
|
|
1476
|
+
S: [" ####", "#", " ###", " #", "####"],
|
|
1477
|
+
A: [" ###", "# #", "#####", "# #", "# #"],
|
|
1478
|
+
P: ["####", "# #", "####", "#", "#"],
|
|
1479
|
+
I: ["###", " #", " #", " #", "###"],
|
|
1480
|
+
N: ["# #", "## #", "# # #", "# ##", "# #"],
|
|
1481
|
+
G: [" ####", "#", "# ##", "# #", " ###"]
|
|
1482
|
+
};
|
|
1483
|
+
var SPLASH_ROWS = 5;
|
|
1484
|
+
var SPLASH_SHADES = {
|
|
1485
|
+
unicode: ["\u2591", "\u2591", "\u2592", "\u2592", "\u2593", "\u2593", "\u2588", "\u2588"],
|
|
1486
|
+
ascii: [".", ".", ":", ":", "=", "=", "#", "#"]
|
|
1487
|
+
};
|
|
1488
|
+
var WAVE_RAMP = [17, 18, 19, 61, 24, 25, 31, 37, 44, 45, 51, 87, 123, 159, 195];
|
|
1489
|
+
var SPINNER_FRAMES = {
|
|
1490
|
+
unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
|
|
1491
|
+
ascii: ["|", "/", "-", "\\"]
|
|
1492
|
+
};
|
|
1493
|
+
function wordArt(word) {
|
|
1494
|
+
const glyphs = [...word.toUpperCase()].map((ch) => SPLASH_FONT[ch]).filter((g) => g !== void 0);
|
|
1495
|
+
const widths = glyphs.map((g) => Math.max(...g.map((r) => r.length)));
|
|
1496
|
+
const rows = [];
|
|
1497
|
+
for (let r = 0; r < SPLASH_ROWS; r++) {
|
|
1498
|
+
rows.push(glyphs.map((g, i) => (g[r] ?? "").padEnd(widths[i], " ")).join(" "));
|
|
1499
|
+
}
|
|
1500
|
+
return rows;
|
|
1501
|
+
}
|
|
1502
|
+
function splashArt() {
|
|
1503
|
+
const words = [wordArt("MELLOS"), wordArt("MAPPING")];
|
|
1504
|
+
const width = Math.max(...words.flat().map((r) => r.length));
|
|
1505
|
+
const centered = words.map((rows) => {
|
|
1506
|
+
const own = Math.max(...rows.map((r) => r.length));
|
|
1507
|
+
return rows.map((r) => " ".repeat(Math.floor((width - own) / 2)) + r);
|
|
1508
|
+
});
|
|
1509
|
+
return [...centered[0], "", ...centered[1]];
|
|
1510
|
+
}
|
|
1511
|
+
var WAVE_INTERVAL = 18;
|
|
1512
|
+
var WAVE_LIFETIME = 64;
|
|
1513
|
+
var WAVE_SPEED = 0.9;
|
|
1514
|
+
var WAVE_ENVELOPE = 10;
|
|
1515
|
+
var WAVE_NUMBER = 0.42;
|
|
1516
|
+
var WAVE_LEVELS = 7;
|
|
1517
|
+
var WAVE_GAIN = 4.5;
|
|
1518
|
+
function waveHash(n) {
|
|
1519
|
+
let h = Math.imul(n + 1, 2654435761) >>> 0;
|
|
1520
|
+
h ^= h >>> 15;
|
|
1521
|
+
h = Math.imul(h, 2246822519) >>> 0;
|
|
1522
|
+
h ^= h >>> 13;
|
|
1523
|
+
return h >>> 0;
|
|
1524
|
+
}
|
|
1525
|
+
var WAVE_CORNERS = [
|
|
1526
|
+
[0, 0],
|
|
1527
|
+
[1, 0],
|
|
1528
|
+
[0, 1],
|
|
1529
|
+
[1, 1]
|
|
1530
|
+
];
|
|
1531
|
+
function liveRipples(frame, width, height) {
|
|
1532
|
+
const out = [];
|
|
1533
|
+
const first = Math.floor((frame - WAVE_LIFETIME - WAVE_INTERVAL) / WAVE_INTERVAL);
|
|
1534
|
+
const last = Math.floor(frame / WAVE_INTERVAL);
|
|
1535
|
+
for (let n = Math.max(0, first); n <= last; n++) {
|
|
1536
|
+
const h = waveHash(n);
|
|
1537
|
+
const age = frame - (n * WAVE_INTERVAL + h % WAVE_INTERVAL);
|
|
1538
|
+
if (age < 0 || age > WAVE_LIFETIME) continue;
|
|
1539
|
+
const [fx, fy] = WAVE_CORNERS[h % WAVE_CORNERS.length];
|
|
1540
|
+
out.push({
|
|
1541
|
+
ox: fx * (width - 1),
|
|
1542
|
+
oy: fy * (height - 1),
|
|
1543
|
+
r: age * WAVE_SPEED,
|
|
1544
|
+
fade: 1 - age / WAVE_LIFETIME
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
return out;
|
|
1548
|
+
}
|
|
1549
|
+
function waveAt(ripples, x, y) {
|
|
1550
|
+
let value = 0;
|
|
1551
|
+
for (const w of ripples) {
|
|
1552
|
+
const front = Math.hypot(x - w.ox, (y - w.oy) * 2) - w.r;
|
|
1553
|
+
value += Math.cos(front * WAVE_NUMBER) * Math.exp(-(front * front) / (2 * WAVE_ENVELOPE ** 2)) * w.fade;
|
|
1554
|
+
}
|
|
1555
|
+
return value;
|
|
1556
|
+
}
|
|
1557
|
+
function waveLevel(value) {
|
|
1558
|
+
return Math.max(-WAVE_LEVELS, Math.min(WAVE_LEVELS, Math.round(value * WAVE_GAIN)));
|
|
1559
|
+
}
|
|
1560
|
+
function splashFrame(notice, frame, width, height, unicode, color) {
|
|
1561
|
+
const art = splashArt();
|
|
1562
|
+
const artWidth = Math.max(...art.map((r) => r.length));
|
|
1563
|
+
if (width < artWidth + 2 || height < art.length + 2) return void 0;
|
|
1564
|
+
const mode = unicode ? "unicode" : "ascii";
|
|
1565
|
+
const shades = SPLASH_SHADES[mode];
|
|
1566
|
+
const solid = shades[shades.length - 1];
|
|
1567
|
+
const indent = " ".repeat(Math.floor((width - artWidth) / 2));
|
|
1568
|
+
const ripples = liveRipples(frame, artWidth, art.length);
|
|
1569
|
+
const inkOf = (x, y) => {
|
|
1570
|
+
const level = waveLevel(waveAt(ripples, x, y));
|
|
1571
|
+
return color ? `38;5;${WAVE_RAMP[WAVE_LEVELS + level]}` : shades[Math.abs(level)];
|
|
1572
|
+
};
|
|
1573
|
+
const paintRow = (row, y) => {
|
|
1574
|
+
const cells = [...row].map((ch, x) => ch === "#" ? inkOf(x, y) : void 0);
|
|
1575
|
+
let out = "";
|
|
1576
|
+
for (let i = 0; i < cells.length; ) {
|
|
1577
|
+
const cell = cells[i];
|
|
1578
|
+
let j = i;
|
|
1579
|
+
while (j < cells.length && cells[j] === cell) j++;
|
|
1580
|
+
if (cell === void 0) out += " ".repeat(j - i);
|
|
1581
|
+
else out += color ? `\x1B[${cell}m${solid.repeat(j - i)}${RESET}` : cell.repeat(j - i);
|
|
1582
|
+
i = j;
|
|
1583
|
+
}
|
|
1584
|
+
return out;
|
|
1585
|
+
};
|
|
1586
|
+
const spinner = SPINNER_FRAMES[mode];
|
|
1587
|
+
const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
|
|
1588
|
+
const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
|
|
1589
|
+
const block = [
|
|
1590
|
+
...art.map((row, y) => row.trim() === "" ? "" : indent + paintRow(row, y)),
|
|
1591
|
+
"",
|
|
1592
|
+
statusIndent + (color ? `\x1B[90m${status}${RESET}` : status)
|
|
1593
|
+
];
|
|
1594
|
+
return [...Array.from({ length: Math.max(0, Math.floor((height - block.length) / 2)) }, () => ""), ...block];
|
|
1595
|
+
}
|
|
1445
1596
|
function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
|
|
1446
1597
|
const g = (s) => STATUS_GLYPH[s][unicode ? 0 : 1];
|
|
1447
1598
|
const count = (s) => map.nodes.filter((n) => n.status === s).length;
|
|
@@ -1466,6 +1617,7 @@ function main() {
|
|
|
1466
1617
|
const mouseActive = interactive && cfg.mouse;
|
|
1467
1618
|
let lastFrame = "";
|
|
1468
1619
|
let spinnerFrame = 0;
|
|
1620
|
+
let splashTick = 0;
|
|
1469
1621
|
let map;
|
|
1470
1622
|
let notice = `waiting for ${cfg.file} ...`;
|
|
1471
1623
|
let lastCols = process.stdout.columns ?? 0;
|
|
@@ -1476,6 +1628,7 @@ function main() {
|
|
|
1476
1628
|
let activeFile;
|
|
1477
1629
|
let firstScan = true;
|
|
1478
1630
|
let lastTabSegments = [];
|
|
1631
|
+
let tabScroll = 0;
|
|
1479
1632
|
let offsetX = 0;
|
|
1480
1633
|
let offsetY = 0;
|
|
1481
1634
|
let zoom = ZOOM_DEFAULT;
|
|
@@ -1508,8 +1661,19 @@ function main() {
|
|
|
1508
1661
|
}
|
|
1509
1662
|
return false;
|
|
1510
1663
|
};
|
|
1664
|
+
const viewWidth = () => usableColumns(process.stdout.columns ?? 100);
|
|
1511
1665
|
const viewHeight = () => Math.max(1, (process.stdout.rows ?? 30) - (1 + panelContentRows) - 1 - tabRows());
|
|
1512
1666
|
const dividerY = () => tabRows() + viewHeight() + 1;
|
|
1667
|
+
const pageTabsOf = (files) => files.map((f) => {
|
|
1668
|
+
const m = pageData.get(f)?.map;
|
|
1669
|
+
return {
|
|
1670
|
+
title: m?.title ?? (pageIdOfFile(cfg.file, f) ?? "main"),
|
|
1671
|
+
status: m !== void 0 ? mapStatus(m) : "planned",
|
|
1672
|
+
active: f === activeFile,
|
|
1673
|
+
fresh: pageData.get(f)?.fresh ?? false,
|
|
1674
|
+
neutral: m !== void 0 && isNeutralKind(m)
|
|
1675
|
+
};
|
|
1676
|
+
});
|
|
1513
1677
|
const switchPage = (file) => {
|
|
1514
1678
|
if (activeFile !== void 0) pageViews.set(activeFile, { offsetX, offsetY, zoom, selectedId });
|
|
1515
1679
|
activeFile = file;
|
|
@@ -1523,10 +1687,14 @@ function main() {
|
|
|
1523
1687
|
if (entry !== void 0 && entry.fresh) pageData.set(file, { ...entry, fresh: false });
|
|
1524
1688
|
map = entry?.map;
|
|
1525
1689
|
notice = map === void 0 ? `waiting for ${file} ...` : "";
|
|
1690
|
+
const top = topFiles();
|
|
1691
|
+
const tabIndex = top.indexOf(file);
|
|
1692
|
+
if (tabIndex >= 0) tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex);
|
|
1526
1693
|
};
|
|
1527
1694
|
const hitTest = (termX, termY) => {
|
|
1528
1695
|
const sx = termX - 1;
|
|
1529
1696
|
const sy = termY - 1 - tabRows();
|
|
1697
|
+
if (sx < 0 || sx >= viewWidth()) return void 0;
|
|
1530
1698
|
if (sy < 0 || sy >= viewHeight()) return void 0;
|
|
1531
1699
|
const cx = sx + offsetX;
|
|
1532
1700
|
const cy = sy + offsetY;
|
|
@@ -1541,6 +1709,7 @@ function main() {
|
|
|
1541
1709
|
process.on("SIGTERM", restore);
|
|
1542
1710
|
const paint = () => {
|
|
1543
1711
|
const cols = process.stdout.columns ?? 100;
|
|
1712
|
+
const viewW = viewWidth();
|
|
1544
1713
|
panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ?? 30, tabRows());
|
|
1545
1714
|
const viewH = viewHeight();
|
|
1546
1715
|
const focus = hoverId ?? selectedId;
|
|
@@ -1551,9 +1720,9 @@ function main() {
|
|
|
1551
1720
|
const windowed = renderMapWindow(
|
|
1552
1721
|
map,
|
|
1553
1722
|
{ color: cfg.color, unicode: cfg.unicode, spinnerFrame, focus, zoom },
|
|
1554
|
-
{ x: offsetX, y: offsetY, width:
|
|
1723
|
+
{ x: offsetX, y: offsetY, width: viewW, height: viewH }
|
|
1555
1724
|
);
|
|
1556
|
-
const maxX = Math.max(0, windowed.contentWidth -
|
|
1725
|
+
const maxX = Math.max(0, windowed.contentWidth - viewW);
|
|
1557
1726
|
const maxY = Math.max(0, windowed.contentHeight - viewH);
|
|
1558
1727
|
if (offsetX > maxX || offsetY > maxY || offsetX < 0 || offsetY < 0) {
|
|
1559
1728
|
offsetX = Math.min(Math.max(0, offsetX), maxX);
|
|
@@ -1567,10 +1736,10 @@ function main() {
|
|
|
1567
1736
|
lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
|
|
1568
1737
|
if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
|
|
1569
1738
|
} else {
|
|
1570
|
-
body =
|
|
1739
|
+
body = (interactive ? splashFrame(notice, splashTick, viewW, viewH, cfg.unicode, cfg.color) : void 0) ?? [fitWidth(notice, viewW)];
|
|
1571
1740
|
}
|
|
1572
1741
|
if (notice !== "" && map !== void 0) {
|
|
1573
|
-
body[body.length - 1] = fitWidth(` ${notice}`,
|
|
1742
|
+
body[body.length - 1] = fitWidth(` ${notice}`, viewW);
|
|
1574
1743
|
}
|
|
1575
1744
|
const panelWidth = Math.max(10, cols - 2);
|
|
1576
1745
|
let panel;
|
|
@@ -1582,9 +1751,9 @@ function main() {
|
|
|
1582
1751
|
panel = mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
|
|
1583
1752
|
}
|
|
1584
1753
|
const grip = cfg.unicode ? " \u22EF " : " ~ ";
|
|
1585
|
-
const bar = (cfg.unicode ? "\u2500" : "-").repeat(
|
|
1586
|
-
const gripAt = Math.max(0, Math.floor((
|
|
1587
|
-
const separator =
|
|
1754
|
+
const bar = (cfg.unicode ? "\u2500" : "-").repeat(viewW);
|
|
1755
|
+
const gripAt = Math.max(0, Math.floor((viewW - grip.length) / 2));
|
|
1756
|
+
const separator = viewW > grip.length + 2 ? bar.slice(0, gripAt) + grip + bar.slice(gripAt + grip.length) : bar;
|
|
1588
1757
|
const panelRows = [
|
|
1589
1758
|
cfg.color ? `\x1B[90m${separator}${RESET}` : separator,
|
|
1590
1759
|
...panel.map(
|
|
@@ -1600,29 +1769,19 @@ function main() {
|
|
|
1600
1769
|
const parentTitle = parentFile !== void 0 ? pageData.get(parentFile)?.map?.title ?? (pageIdOfFile(cfg.file, parentFile) ?? "main") : "main";
|
|
1601
1770
|
const nodeLabel = scanned?.label ?? map?.title ?? "";
|
|
1602
1771
|
const crumbHead = ` ${cfg.unicode ? "\u232B" : "<"} ${parentTitle} ${cfg.unicode ? "\u25B8" : ">"} `;
|
|
1603
|
-
const head = { text: crumbHead, sgr: "90", lo: 1, hi: displayWidth(crumbHead),
|
|
1604
|
-
const tailText = fitWidth(`${nodeLabel} `, Math.max(1,
|
|
1772
|
+
const head = { text: crumbHead, sgr: "90", lo: 1, hi: displayWidth(crumbHead), action: { kind: "back" } };
|
|
1773
|
+
const tailText = fitWidth(`${nodeLabel} `, Math.max(1, viewW - displayWidth(crumbHead)));
|
|
1605
1774
|
const tail = {
|
|
1606
1775
|
text: tailText,
|
|
1607
1776
|
sgr: "1",
|
|
1608
1777
|
lo: head.hi + 1,
|
|
1609
1778
|
hi: head.hi + displayWidth(tailText),
|
|
1610
|
-
|
|
1779
|
+
action: { kind: "back" }
|
|
1611
1780
|
};
|
|
1612
1781
|
lastTabSegments = [head, tail];
|
|
1613
1782
|
tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
|
|
1614
1783
|
} else if (tabRows() > 0) {
|
|
1615
|
-
const
|
|
1616
|
-
const m = pageData.get(f)?.map;
|
|
1617
|
-
return {
|
|
1618
|
-
title: m?.title ?? (pageIdOfFile(cfg.file, f) ?? "main"),
|
|
1619
|
-
status: m !== void 0 ? mapStatus(m) : "planned",
|
|
1620
|
-
active: f === activeFile,
|
|
1621
|
-
fresh: pageData.get(f)?.fresh ?? false,
|
|
1622
|
-
neutral: m !== void 0 && isNeutralKind(m)
|
|
1623
|
-
};
|
|
1624
|
-
});
|
|
1625
|
-
const segments = pageTabRow(tabs, cols, cfg.unicode);
|
|
1784
|
+
const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll);
|
|
1626
1785
|
lastTabSegments = segments;
|
|
1627
1786
|
tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
|
|
1628
1787
|
} else {
|
|
@@ -1630,7 +1789,7 @@ function main() {
|
|
|
1630
1789
|
}
|
|
1631
1790
|
const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(zoom)}`;
|
|
1632
1791
|
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";
|
|
1633
|
-
const footerText = fitWidth(` ${hint}${panned}`,
|
|
1792
|
+
const footerText = fitWidth(` ${hint}${panned}`, viewW);
|
|
1634
1793
|
const footer = cfg.color ? `\x1B[90m${footerText}${RESET}` : footerText;
|
|
1635
1794
|
let frame = HOME;
|
|
1636
1795
|
if (tabLine !== void 0) frame += tabLine + ERASE_LINE_END + "\n";
|
|
@@ -1723,10 +1882,14 @@ function main() {
|
|
|
1723
1882
|
dirty = true;
|
|
1724
1883
|
break;
|
|
1725
1884
|
case "zoom": {
|
|
1885
|
+
if (event.at !== void 0 && event.at.y === 1 && tabRows() > 0 && !inSubmap()) {
|
|
1886
|
+
tabScroll = Math.max(0, Math.min(tabScroll + (event.delta === 1 ? -1 : 1), topFiles().length - 1));
|
|
1887
|
+
dirty = true;
|
|
1888
|
+
break;
|
|
1889
|
+
}
|
|
1726
1890
|
const next = clampZoom(zoom + event.delta);
|
|
1727
1891
|
if (next === zoom || map === void 0) break;
|
|
1728
|
-
const
|
|
1729
|
-
const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + cols / 2, offsetY + viewHeight() / 2)?.id;
|
|
1892
|
+
const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + viewWidth() / 2, offsetY + viewHeight() / 2)?.id;
|
|
1730
1893
|
const before = lastHits.find((h) => h.id === anchorId);
|
|
1731
1894
|
zoom = next;
|
|
1732
1895
|
const sized = renderMapWindow(
|
|
@@ -1790,10 +1953,12 @@ function main() {
|
|
|
1790
1953
|
if (press && !press.moved) {
|
|
1791
1954
|
const tabHit = tabRows() > 0 && event.y === 1 ? lastTabSegments.find((s) => event.x >= s.lo && event.x <= s.hi) : void 0;
|
|
1792
1955
|
if (tabHit !== void 0) {
|
|
1793
|
-
if (tabHit.
|
|
1956
|
+
if (tabHit.action.kind === "back") {
|
|
1794
1957
|
climbBack();
|
|
1958
|
+
} else if (tabHit.action.kind === "scroll") {
|
|
1959
|
+
tabScroll = Math.max(0, Math.min(tabScroll + tabHit.action.delta, lastTabFiles.length - 1));
|
|
1795
1960
|
} else {
|
|
1796
|
-
const target = lastTabFiles[tabHit.index];
|
|
1961
|
+
const target = lastTabFiles[tabHit.action.index];
|
|
1797
1962
|
if (target !== void 0 && target !== activeFile) switchPage(target);
|
|
1798
1963
|
}
|
|
1799
1964
|
} else {
|
|
@@ -1854,6 +2019,13 @@ function main() {
|
|
|
1854
2019
|
}
|
|
1855
2020
|
tick();
|
|
1856
2021
|
setInterval(tick, cfg.intervalMs);
|
|
2022
|
+
if (interactive) {
|
|
2023
|
+
setInterval(() => {
|
|
2024
|
+
if (map !== void 0) return;
|
|
2025
|
+
splashTick++;
|
|
2026
|
+
paint();
|
|
2027
|
+
}, 80);
|
|
2028
|
+
}
|
|
1857
2029
|
}
|
|
1858
2030
|
function launchedAsEntry(argv1, moduleUrl) {
|
|
1859
2031
|
if (argv1 === void 0) return false;
|
|
@@ -1868,17 +2040,28 @@ if (launchedAsEntry(process.argv[1], import.meta.url)) {
|
|
|
1868
2040
|
}
|
|
1869
2041
|
export {
|
|
1870
2042
|
PANEL_ROWS_MIN,
|
|
2043
|
+
WAVE_LEVELS,
|
|
2044
|
+
WAVE_NUMBER,
|
|
1871
2045
|
anchorOffsets,
|
|
1872
2046
|
clampPanelRows,
|
|
1873
2047
|
diveOrigin,
|
|
1874
2048
|
fitWidth,
|
|
1875
2049
|
launchedAsEntry,
|
|
2050
|
+
liveRipples,
|
|
1876
2051
|
mapPanel,
|
|
1877
2052
|
nearestHit,
|
|
1878
2053
|
nodePanel,
|
|
1879
2054
|
pageTabRow,
|
|
1880
2055
|
panelRowsFromDividerY,
|
|
1881
2056
|
parseArgs,
|
|
2057
|
+
splashArt,
|
|
2058
|
+
splashFrame,
|
|
2059
|
+
tabScrollFor,
|
|
1882
2060
|
topLevelFiles,
|
|
2061
|
+
usableColumns,
|
|
2062
|
+
waveAt,
|
|
2063
|
+
waveHash,
|
|
2064
|
+
waveLevel,
|
|
2065
|
+
wordArt,
|
|
1883
2066
|
wrapWidth
|
|
1884
2067
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mellos-mapping",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"mcpName": "io.github.GuangminJu/mellos-mapping",
|
|
5
5
|
"description": "A live layered dependency map for bottom-up development — MCP server + terminal pane. Ghost the design first, then light nodes up from the bottom as they are built and verified.",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"dist",
|
|
35
35
|
"README.zh-CN.md",
|
|
36
|
-
"scripts/codex-register.mjs"
|
|
36
|
+
"scripts/codex-register.mjs",
|
|
37
|
+
"scripts/open-pane.mjs"
|
|
37
38
|
],
|
|
38
39
|
"engines": {
|
|
39
40
|
"node": ">=18"
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Open the live map pane in the RIGHT Windows Terminal window.
|
|
4
|
+
*
|
|
5
|
+
* node scripts/open-pane.mjs <project-dir> [--window] [--ascii] [--force]
|
|
6
|
+
*
|
|
7
|
+
* Why this exists: the agent's shell runs on a hidden console (no WT_SESSION),
|
|
8
|
+
* so a bare `wt -w 0 sp` targets the MOST RECENTLY USED terminal window — with
|
|
9
|
+
* several windows open the map lands wherever the user last clicked, not
|
|
10
|
+
* beside the conversation. Verified on Windows 11 / WT single-process mode:
|
|
11
|
+
* all windows share one WindowsTerminal.exe pid, and the console UI is
|
|
12
|
+
* attached by default-terminal delegation, so neither pid-matching nor the
|
|
13
|
+
* process tree can name the hosting window.
|
|
14
|
+
*
|
|
15
|
+
* What does work — and what this script does, verifying every step:
|
|
16
|
+
*
|
|
17
|
+
* 1. Walk this process's ancestors and AttachConsole to each in turn; write
|
|
18
|
+
* a nonce into that console's title. The WT window whose title lights up
|
|
19
|
+
* is the one hosting this session. The nonce only shows when the session
|
|
20
|
+
* tab is the window's ACTIVE tab — exactly the precondition under which
|
|
21
|
+
* `sp` would split beside the conversation, so identification doubles as
|
|
22
|
+
* the go/no-go check.
|
|
23
|
+
* 2. Bring that window to the foreground (plain SetForegroundWindow, then
|
|
24
|
+
* the Alt-key unlock, then AttachThreadInput — Windows' foreground lock
|
|
25
|
+
* denies the plain call from background processes), verifying with
|
|
26
|
+
* GetForegroundWindow after each attempt.
|
|
27
|
+
* 3. Only then `wt -w 0 sp` — "most recently used" is now provably ours.
|
|
28
|
+
*
|
|
29
|
+
* Any step failing falls back to a dedicated window named "mellos-mapping":
|
|
30
|
+
* deterministic, never a random window. `--window` picks that mode outright
|
|
31
|
+
* (explicit choice for users who want the map separate from the chat).
|
|
32
|
+
*
|
|
33
|
+
* Known race, accepted: if the user focuses a DIFFERENT terminal window in
|
|
34
|
+
* the ~1s between our focus-verify and wt reading its MRU state, the split
|
|
35
|
+
* can still land there. The window is at least one the user is actively in.
|
|
36
|
+
*/
|
|
37
|
+
import { spawnSync } from 'node:child_process';
|
|
38
|
+
import { existsSync } from 'node:fs';
|
|
39
|
+
import { dirname, join, resolve } from 'node:path';
|
|
40
|
+
import { fileURLToPath } from 'node:url';
|
|
41
|
+
|
|
42
|
+
const argv = process.argv.slice(2);
|
|
43
|
+
const flags = new Set(argv.filter((a) => a.startsWith('--')));
|
|
44
|
+
const positional = argv.filter((a) => !a.startsWith('--'));
|
|
45
|
+
|
|
46
|
+
if (positional.length !== 1) {
|
|
47
|
+
console.error('usage: node scripts/open-pane.mjs <project-dir> [--window] [--ascii] [--force]');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (process.platform !== 'win32') {
|
|
51
|
+
console.error('open-pane.mjs is Windows Terminal-only — use the tmux/manual route from the command doc.');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const projectDir = resolve(positional[0]);
|
|
56
|
+
if (!existsSync(projectDir)) {
|
|
57
|
+
console.error(`project directory does not exist: ${projectDir}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
62
|
+
const watchPath = join(pluginRoot, 'dist', 'watch.mjs');
|
|
63
|
+
if (!existsSync(watchPath)) {
|
|
64
|
+
console.error(`watcher not found (is the plugin built?): ${watchPath}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const mapFile = join(projectDir, '.claude', 'mellos-mapping.json');
|
|
68
|
+
|
|
69
|
+
function runPowerShell(script) {
|
|
70
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
71
|
+
const r = spawnSync(
|
|
72
|
+
'powershell.exe',
|
|
73
|
+
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded],
|
|
74
|
+
{ encoding: 'utf8', timeout: 30_000, windowsHide: true },
|
|
75
|
+
);
|
|
76
|
+
return r.stdout ?? '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// One watcher per map file is enough — watch.mjs redraws on change for every
|
|
80
|
+
// viewer of the same file, and piling up panes on repeated /mmap is noise.
|
|
81
|
+
// Matches only watchers this script started (they carry --file <mapFile> on
|
|
82
|
+
// their command line); --force bypasses.
|
|
83
|
+
function watcherAlreadyRunning() {
|
|
84
|
+
const token = mapFile.replace(/'/g, "''");
|
|
85
|
+
const out = runPowerShell(
|
|
86
|
+
`$ErrorActionPreference = 'SilentlyContinue'\n` +
|
|
87
|
+
`$w = @(Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*watch.mjs*' -and $_.CommandLine -like '*${token}*' })\n` +
|
|
88
|
+
`Write-Output "WATCHERS=$($w.Count)"`,
|
|
89
|
+
);
|
|
90
|
+
const m = out.match(/WATCHERS=(\d+)/);
|
|
91
|
+
return m !== null && Number(m[1]) > 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Prints IDENT=<hwnd|0> and, when identified, FOCUS=<1|0>.
|
|
95
|
+
const IDENTIFY_AND_FOCUS = String.raw`
|
|
96
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
97
|
+
Add-Type @"
|
|
98
|
+
using System;
|
|
99
|
+
using System.Text;
|
|
100
|
+
using System.Runtime.InteropServices;
|
|
101
|
+
public class MmapWin {
|
|
102
|
+
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lp);
|
|
103
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lp);
|
|
104
|
+
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
105
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassName(IntPtr hWnd, StringBuilder sb, int max);
|
|
106
|
+
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder sb, int max);
|
|
107
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
108
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
109
|
+
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd);
|
|
110
|
+
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
|
|
111
|
+
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
|
112
|
+
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
|
113
|
+
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, IntPtr dwExtraInfo);
|
|
114
|
+
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
115
|
+
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
|
|
116
|
+
[DllImport("kernel32.dll")] public static extern bool FreeConsole();
|
|
117
|
+
[DllImport("kernel32.dll")] public static extern bool AttachConsole(uint pid);
|
|
118
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern bool SetConsoleTitle(string title);
|
|
119
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern uint GetConsoleTitle(StringBuilder sb, uint size);
|
|
120
|
+
}
|
|
121
|
+
"@
|
|
122
|
+
function Get-WtWindows {
|
|
123
|
+
$wins = New-Object System.Collections.ArrayList
|
|
124
|
+
$cb = {
|
|
125
|
+
param($h, $lp)
|
|
126
|
+
if ([MmapWin]::IsWindowVisible($h)) {
|
|
127
|
+
$cls = New-Object System.Text.StringBuilder 256
|
|
128
|
+
[void][MmapWin]::GetClassName($h, $cls, 256)
|
|
129
|
+
if ($cls.ToString() -eq 'CASCADIA_HOSTING_WINDOW_CLASS') {
|
|
130
|
+
$t = New-Object System.Text.StringBuilder 512
|
|
131
|
+
[void][MmapWin]::GetWindowText($h, $t, 512)
|
|
132
|
+
[void]$wins.Add(@{ hwnd = $h.ToInt64(); title = $t.ToString() })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return $true
|
|
136
|
+
}
|
|
137
|
+
[void][MmapWin]::EnumWindows($cb, [IntPtr]::Zero)
|
|
138
|
+
return ,$wins
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$ancestors = @()
|
|
142
|
+
$p = $PID
|
|
143
|
+
for ($i = 0; $i -lt 12 -and $p; $i++) {
|
|
144
|
+
$proc = Get-CimInstance Win32_Process -Filter "ProcessId=$p"
|
|
145
|
+
if (-not $proc) { break }
|
|
146
|
+
if ($i -gt 0) { $ancestors += [uint32]$proc.ProcessId }
|
|
147
|
+
$p = $proc.ParentProcessId
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# The agent CLI (claude/codex/...) is some ancestor holding the console that a
|
|
151
|
+
# WT window renders; hidden-console ancestors just never light a window up.
|
|
152
|
+
$nonce = "__NONCE__"
|
|
153
|
+
$hwnd = [IntPtr]::Zero
|
|
154
|
+
foreach ($apid in $ancestors) {
|
|
155
|
+
[void][MmapWin]::FreeConsole()
|
|
156
|
+
if (-not [MmapWin]::AttachConsole($apid)) { continue }
|
|
157
|
+
$sb = New-Object System.Text.StringBuilder 1024
|
|
158
|
+
[void][MmapWin]::GetConsoleTitle($sb, 1024)
|
|
159
|
+
$orig = $sb.ToString()
|
|
160
|
+
for ($i = 0; $i -lt 6 -and $hwnd -eq [IntPtr]::Zero; $i++) {
|
|
161
|
+
[void][MmapWin]::SetConsoleTitle($nonce)
|
|
162
|
+
Start-Sleep -Milliseconds 60
|
|
163
|
+
foreach ($w in (Get-WtWindows)) {
|
|
164
|
+
if ($w.title -like "*$nonce*") { $hwnd = [IntPtr]$w.hwnd; break }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
Start-Sleep -Milliseconds 100
|
|
168
|
+
[void][MmapWin]::SetConsoleTitle($orig)
|
|
169
|
+
Start-Sleep -Milliseconds 200
|
|
170
|
+
[void][MmapWin]::FreeConsole()
|
|
171
|
+
if ($hwnd -ne [IntPtr]::Zero) { break }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if ($hwnd -eq [IntPtr]::Zero) { Write-Output 'IDENT=0'; exit 0 }
|
|
175
|
+
Write-Output "IDENT=$($hwnd.ToInt64())"
|
|
176
|
+
|
|
177
|
+
if ([MmapWin]::IsIconic($hwnd)) { [void][MmapWin]::ShowWindow($hwnd, 9) }
|
|
178
|
+
$focused = $false
|
|
179
|
+
[void][MmapWin]::SetForegroundWindow($hwnd)
|
|
180
|
+
Start-Sleep -Milliseconds 150
|
|
181
|
+
if ([MmapWin]::GetForegroundWindow() -eq $hwnd) { $focused = $true }
|
|
182
|
+
if (-not $focused) {
|
|
183
|
+
[MmapWin]::keybd_event(0x12, 0, 0, [IntPtr]::Zero)
|
|
184
|
+
[void][MmapWin]::SetForegroundWindow($hwnd)
|
|
185
|
+
[MmapWin]::keybd_event(0x12, 0, 2, [IntPtr]::Zero)
|
|
186
|
+
Start-Sleep -Milliseconds 150
|
|
187
|
+
if ([MmapWin]::GetForegroundWindow() -eq $hwnd) { $focused = $true }
|
|
188
|
+
}
|
|
189
|
+
if (-not $focused) {
|
|
190
|
+
$fgpid = 0
|
|
191
|
+
$fgThread = [MmapWin]::GetWindowThreadProcessId([MmapWin]::GetForegroundWindow(), [ref]$fgpid)
|
|
192
|
+
$myThread = [MmapWin]::GetCurrentThreadId()
|
|
193
|
+
[void][MmapWin]::AttachThreadInput($myThread, $fgThread, $true)
|
|
194
|
+
[void][MmapWin]::BringWindowToTop($hwnd)
|
|
195
|
+
[void][MmapWin]::SetForegroundWindow($hwnd)
|
|
196
|
+
[void][MmapWin]::AttachThreadInput($myThread, $fgThread, $false)
|
|
197
|
+
Start-Sleep -Milliseconds 150
|
|
198
|
+
if ([MmapWin]::GetForegroundWindow() -eq $hwnd) { $focused = $true }
|
|
199
|
+
}
|
|
200
|
+
Write-Output "FOCUS=$(if ($focused) { 1 } else { 0 })"
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
function paneCommand() {
|
|
204
|
+
const cmd = ['--title', 'mellos map', '-d', projectDir, 'node', watchPath, '--file', mapFile];
|
|
205
|
+
if (flags.has('--ascii')) cmd.push('--ascii');
|
|
206
|
+
return cmd;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function openWt(args, what) {
|
|
210
|
+
const r = spawnSync('wt', args, { stdio: 'ignore', timeout: 15_000, windowsHide: true });
|
|
211
|
+
if (r.status !== 0) {
|
|
212
|
+
console.error(`wt failed to ${what} (exit ${r.status ?? 'timeout'}) — is Windows Terminal installed?`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function openDedicatedWindow(reason) {
|
|
218
|
+
openWt(['-w', 'mellos-mapping', 'nt', ...paneCommand()], 'open the dedicated window');
|
|
219
|
+
console.log(`MMAP_PANE mode=window name=mellos-mapping reason=${reason}`);
|
|
220
|
+
console.log('Map opened in the dedicated "mellos-mapping" window.');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!flags.has('--force') && watcherAlreadyRunning()) {
|
|
224
|
+
console.log('MMAP_PANE already-open');
|
|
225
|
+
console.log(`A watcher for ${mapFile} is already running — not opening another pane (use --force to override).`);
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (flags.has('--window')) {
|
|
230
|
+
openDedicatedWindow('requested');
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const nonce = `MMAP-NONCE-${process.pid}`;
|
|
235
|
+
const out = runPowerShell(IDENTIFY_AND_FOCUS.replaceAll('__NONCE__', nonce));
|
|
236
|
+
const ident = out.match(/IDENT=(\d+)/)?.[1] ?? '0';
|
|
237
|
+
const focused = /FOCUS=1/.test(out);
|
|
238
|
+
|
|
239
|
+
if (ident === '0') {
|
|
240
|
+
openDedicatedWindow('session-window-not-identified');
|
|
241
|
+
} else if (!focused) {
|
|
242
|
+
openDedicatedWindow('session-window-focus-denied');
|
|
243
|
+
} else {
|
|
244
|
+
// The identified window is foreground right now, so "most recently used"
|
|
245
|
+
// is deterministically it (see the race note in the header).
|
|
246
|
+
openWt(['-w', '0', 'sp', '-V', '--size', '0.42', ...paneCommand()], 'split the session window');
|
|
247
|
+
console.log(`MMAP_PANE mode=split hwnd=${ident}`);
|
|
248
|
+
console.log('Map opened beside this conversation (vertical split).');
|
|
249
|
+
}
|