mellos-mapping 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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). Prefer `--ascii` if your font lacks box-drawing glyphs.
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
- 运行 `node <插件根>/dist/watch.mjs`。
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
- 第二个终端里运行)。字体缺少制表符字形时用 `--ascii`。
149
+ 第二个终端里运行)。Windows 上即使开着多个终端窗口,分屏也会落在
150
+ **你的会话所在的窗口**;想让地图独占一个窗口就加 `--window`。字体
151
+ 缺少制表符字形时用 `--ascii`。
147
152
  3. 看着节点从底部一路亮起。图让你不安的时候就打断它——这正是它存在的
148
153
  意义。
149
154
 
package/dist/server.mjs CHANGED
@@ -22513,7 +22513,7 @@ function summarize(map) {
22513
22513
 
22514
22514
  // src/server/server.ts
22515
22515
  var SERVER_NAME = "mellos-mapping";
22516
- var SERVER_VERSION = "0.12.0";
22516
+ var SERVER_VERSION = "0.14.0";
22517
22517
  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
22518
  var PAGE = ID.optional().describe(
22519
22519
  "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
@@ -1442,6 +1442,131 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
1442
1442
  }
1443
1443
  return lines.slice(0, rows);
1444
1444
  }
1445
+ var SPLASH_FONT = {
1446
+ M: ["# #", "## ##", "# # #", "# #", "# #"],
1447
+ E: ["####", "#", "###", "#", "####"],
1448
+ L: ["#", "#", "#", "#", "####"],
1449
+ O: [" ###", "# #", "# #", "# #", " ###"],
1450
+ S: [" ####", "#", " ###", " #", "####"],
1451
+ A: [" ###", "# #", "#####", "# #", "# #"],
1452
+ P: ["####", "# #", "####", "#", "#"],
1453
+ I: ["###", " #", " #", " #", "###"],
1454
+ N: ["# #", "## #", "# # #", "# ##", "# #"],
1455
+ G: [" ####", "#", "# ##", "# #", " ###"]
1456
+ };
1457
+ var SPLASH_ROWS = 5;
1458
+ var SPLASH_SHADES = {
1459
+ unicode: ["\u2591", "\u2591", "\u2592", "\u2592", "\u2593", "\u2593", "\u2588", "\u2588"],
1460
+ ascii: [".", ".", ":", ":", "=", "=", "#", "#"]
1461
+ };
1462
+ var WAVE_RAMP = [17, 18, 19, 61, 24, 25, 31, 37, 44, 45, 51, 87, 123, 159, 195];
1463
+ var SPINNER_FRAMES = {
1464
+ unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
1465
+ ascii: ["|", "/", "-", "\\"]
1466
+ };
1467
+ function wordArt(word) {
1468
+ const glyphs = [...word.toUpperCase()].map((ch) => SPLASH_FONT[ch]).filter((g) => g !== void 0);
1469
+ const widths = glyphs.map((g) => Math.max(...g.map((r) => r.length)));
1470
+ const rows = [];
1471
+ for (let r = 0; r < SPLASH_ROWS; r++) {
1472
+ rows.push(glyphs.map((g, i) => (g[r] ?? "").padEnd(widths[i], " ")).join(" "));
1473
+ }
1474
+ return rows;
1475
+ }
1476
+ function splashArt() {
1477
+ const words = [wordArt("MELLOS"), wordArt("MAPPING")];
1478
+ const width = Math.max(...words.flat().map((r) => r.length));
1479
+ const centered = words.map((rows) => {
1480
+ const own = Math.max(...rows.map((r) => r.length));
1481
+ return rows.map((r) => " ".repeat(Math.floor((width - own) / 2)) + r);
1482
+ });
1483
+ return [...centered[0], "", ...centered[1]];
1484
+ }
1485
+ var WAVE_INTERVAL = 18;
1486
+ var WAVE_LIFETIME = 64;
1487
+ var WAVE_SPEED = 0.9;
1488
+ var WAVE_ENVELOPE = 10;
1489
+ var WAVE_NUMBER = 0.42;
1490
+ var WAVE_LEVELS = 7;
1491
+ var WAVE_GAIN = 4.5;
1492
+ function waveHash(n) {
1493
+ let h = Math.imul(n + 1, 2654435761) >>> 0;
1494
+ h ^= h >>> 15;
1495
+ h = Math.imul(h, 2246822519) >>> 0;
1496
+ h ^= h >>> 13;
1497
+ return h >>> 0;
1498
+ }
1499
+ var WAVE_CORNERS = [
1500
+ [0, 0],
1501
+ [1, 0],
1502
+ [0, 1],
1503
+ [1, 1]
1504
+ ];
1505
+ function liveRipples(frame, width, height) {
1506
+ const out = [];
1507
+ const first = Math.floor((frame - WAVE_LIFETIME - WAVE_INTERVAL) / WAVE_INTERVAL);
1508
+ const last = Math.floor(frame / WAVE_INTERVAL);
1509
+ for (let n = Math.max(0, first); n <= last; n++) {
1510
+ const h = waveHash(n);
1511
+ const age = frame - (n * WAVE_INTERVAL + h % WAVE_INTERVAL);
1512
+ if (age < 0 || age > WAVE_LIFETIME) continue;
1513
+ const [fx, fy] = WAVE_CORNERS[h % WAVE_CORNERS.length];
1514
+ out.push({
1515
+ ox: fx * (width - 1),
1516
+ oy: fy * (height - 1),
1517
+ r: age * WAVE_SPEED,
1518
+ fade: 1 - age / WAVE_LIFETIME
1519
+ });
1520
+ }
1521
+ return out;
1522
+ }
1523
+ function waveAt(ripples, x, y) {
1524
+ let value = 0;
1525
+ for (const w of ripples) {
1526
+ const front = Math.hypot(x - w.ox, (y - w.oy) * 2) - w.r;
1527
+ value += Math.cos(front * WAVE_NUMBER) * Math.exp(-(front * front) / (2 * WAVE_ENVELOPE ** 2)) * w.fade;
1528
+ }
1529
+ return value;
1530
+ }
1531
+ function waveLevel(value) {
1532
+ return Math.max(-WAVE_LEVELS, Math.min(WAVE_LEVELS, Math.round(value * WAVE_GAIN)));
1533
+ }
1534
+ function splashFrame(notice, frame, width, height, unicode, color) {
1535
+ const art = splashArt();
1536
+ const artWidth = Math.max(...art.map((r) => r.length));
1537
+ if (width < artWidth + 2 || height < art.length + 2) return void 0;
1538
+ const mode = unicode ? "unicode" : "ascii";
1539
+ const shades = SPLASH_SHADES[mode];
1540
+ const solid = shades[shades.length - 1];
1541
+ const indent = " ".repeat(Math.floor((width - artWidth) / 2));
1542
+ const ripples = liveRipples(frame, artWidth, art.length);
1543
+ const inkOf = (x, y) => {
1544
+ const level = waveLevel(waveAt(ripples, x, y));
1545
+ return color ? `38;5;${WAVE_RAMP[WAVE_LEVELS + level]}` : shades[Math.abs(level)];
1546
+ };
1547
+ const paintRow = (row, y) => {
1548
+ const cells = [...row].map((ch, x) => ch === "#" ? inkOf(x, y) : void 0);
1549
+ let out = "";
1550
+ for (let i = 0; i < cells.length; ) {
1551
+ const cell = cells[i];
1552
+ let j = i;
1553
+ while (j < cells.length && cells[j] === cell) j++;
1554
+ if (cell === void 0) out += " ".repeat(j - i);
1555
+ else out += color ? `\x1B[${cell}m${solid.repeat(j - i)}${RESET}` : cell.repeat(j - i);
1556
+ i = j;
1557
+ }
1558
+ return out;
1559
+ };
1560
+ const spinner = SPINNER_FRAMES[mode];
1561
+ const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
1562
+ const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
1563
+ const block = [
1564
+ ...art.map((row, y) => row.trim() === "" ? "" : indent + paintRow(row, y)),
1565
+ "",
1566
+ statusIndent + (color ? `\x1B[90m${status}${RESET}` : status)
1567
+ ];
1568
+ return [...Array.from({ length: Math.max(0, Math.floor((height - block.length) / 2)) }, () => ""), ...block];
1569
+ }
1445
1570
  function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
1446
1571
  const g = (s) => STATUS_GLYPH[s][unicode ? 0 : 1];
1447
1572
  const count = (s) => map.nodes.filter((n) => n.status === s).length;
@@ -1466,6 +1591,7 @@ function main() {
1466
1591
  const mouseActive = interactive && cfg.mouse;
1467
1592
  let lastFrame = "";
1468
1593
  let spinnerFrame = 0;
1594
+ let splashTick = 0;
1469
1595
  let map;
1470
1596
  let notice = `waiting for ${cfg.file} ...`;
1471
1597
  let lastCols = process.stdout.columns ?? 0;
@@ -1567,7 +1693,7 @@ function main() {
1567
1693
  lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
1568
1694
  if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
1569
1695
  } else {
1570
- body = [fitWidth(notice, Math.max(1, cols - 1))];
1696
+ body = (interactive ? splashFrame(notice, splashTick, cols, viewH, cfg.unicode, cfg.color) : void 0) ?? [fitWidth(notice, Math.max(1, cols - 1))];
1571
1697
  }
1572
1698
  if (notice !== "" && map !== void 0) {
1573
1699
  body[body.length - 1] = fitWidth(` ${notice}`, Math.max(1, cols - 1));
@@ -1854,6 +1980,13 @@ function main() {
1854
1980
  }
1855
1981
  tick();
1856
1982
  setInterval(tick, cfg.intervalMs);
1983
+ if (interactive) {
1984
+ setInterval(() => {
1985
+ if (map !== void 0) return;
1986
+ splashTick++;
1987
+ paint();
1988
+ }, 80);
1989
+ }
1857
1990
  }
1858
1991
  function launchedAsEntry(argv1, moduleUrl) {
1859
1992
  if (argv1 === void 0) return false;
@@ -1868,17 +2001,26 @@ if (launchedAsEntry(process.argv[1], import.meta.url)) {
1868
2001
  }
1869
2002
  export {
1870
2003
  PANEL_ROWS_MIN,
2004
+ WAVE_LEVELS,
2005
+ WAVE_NUMBER,
1871
2006
  anchorOffsets,
1872
2007
  clampPanelRows,
1873
2008
  diveOrigin,
1874
2009
  fitWidth,
1875
2010
  launchedAsEntry,
2011
+ liveRipples,
1876
2012
  mapPanel,
1877
2013
  nearestHit,
1878
2014
  nodePanel,
1879
2015
  pageTabRow,
1880
2016
  panelRowsFromDividerY,
1881
2017
  parseArgs,
2018
+ splashArt,
2019
+ splashFrame,
1882
2020
  topLevelFiles,
2021
+ waveAt,
2022
+ waveHash,
2023
+ waveLevel,
2024
+ wordArt,
1883
2025
  wrapWidth
1884
2026
  };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "mellos-mapping",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
+ "mcpName": "io.github.GuangminJu/mellos-mapping",
4
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.",
5
6
  "type": "module",
6
7
  "license": "MIT",
@@ -32,7 +33,8 @@
32
33
  "files": [
33
34
  "dist",
34
35
  "README.zh-CN.md",
35
- "scripts/codex-register.mjs"
36
+ "scripts/codex-register.mjs",
37
+ "scripts/open-pane.mjs"
36
38
  ],
37
39
  "engines": {
38
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
+ }