mellos-mapping 0.20.2 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +125 -88
  2. package/README.zh-CN.md +122 -84
  3. package/dist/hook-session-start.mjs +25 -19
  4. package/dist/mmap.mjs +302 -170
  5. package/dist/preview.mjs +1418 -0
  6. package/dist/server.mjs +1198 -400
  7. package/dist/store-paths.mjs +40 -21
  8. package/dist/terminal-worker.mjs +3188 -0
  9. package/dist/watch.mjs +523 -280
  10. package/dist/web/TERMINAL-LICENSES.txt +70 -0
  11. package/dist/web/app.css +967 -0
  12. package/dist/web/app.js +1356 -0
  13. package/dist/web/index.html +9 -0
  14. package/dist/web/terminal.css +9 -0
  15. package/dist/web/terminal.html +7 -0
  16. package/dist/web/terminal.js +9293 -0
  17. package/dist/web/xterm.css +285 -0
  18. package/dist/web.mjs +5535 -0
  19. package/docs/codex.md +183 -0
  20. package/lib/domain/text.d.ts +9 -0
  21. package/lib/domain/text.js +43 -0
  22. package/lib/domain/types.js +10 -1
  23. package/lib/preview/index.d.ts +3 -0
  24. package/lib/preview/index.js +3 -0
  25. package/lib/preview/markdown.d.ts +8 -0
  26. package/lib/preview/markdown.js +54 -0
  27. package/lib/preview/presentation.d.ts +6 -0
  28. package/lib/preview/presentation.js +14 -0
  29. package/lib/preview/publisher.d.ts +23 -0
  30. package/lib/preview/publisher.js +143 -0
  31. package/lib/preview/svg.d.ts +3 -0
  32. package/lib/preview/svg.js +74 -0
  33. package/lib/preview/text.d.ts +4 -0
  34. package/lib/preview/text.js +13 -0
  35. package/lib/render/canvas.d.ts +1 -1
  36. package/lib/render/canvas.js +4 -2
  37. package/lib/render/draw.js +9 -6
  38. package/lib/render/render.d.ts +6 -0
  39. package/lib/render/render.js +50 -18
  40. package/lib/render/width.js +3 -1
  41. package/lib/store/atomic.d.ts +18 -0
  42. package/lib/store/atomic.js +86 -0
  43. package/lib/store/channels.d.ts +40 -0
  44. package/lib/store/channels.js +135 -0
  45. package/lib/store/format.js +3 -1
  46. package/lib/store/json-text.d.ts +9 -0
  47. package/lib/store/json-text.js +16 -0
  48. package/lib/store/maps.d.ts +12 -0
  49. package/lib/store/maps.js +42 -0
  50. package/lib/store/migration.d.ts +12 -0
  51. package/lib/store/migration.js +46 -0
  52. package/lib/store/pages.d.ts +46 -0
  53. package/lib/store/pages.js +89 -0
  54. package/lib/store/policy.d.ts +74 -0
  55. package/lib/store/policy.js +144 -0
  56. package/lib/store/store.d.ts +9 -256
  57. package/lib/store/store.js +10 -694
  58. package/lib/store/viewers.d.ts +81 -0
  59. package/lib/store/viewers.js +186 -0
  60. package/package.json +25 -6
  61. package/scripts/codex-cli.mjs +42 -0
  62. package/scripts/codex-register.mjs +27 -94
  63. package/scripts/mmap.mjs +26 -16
  64. package/scripts/open-pane.mjs +37 -18
  65. package/scripts/pane-core.mjs +57 -220
  66. package/scripts/terminal-session.mjs +137 -0
  67. package/scripts/tmux-session.mjs +90 -0
  68. package/scripts/watcher-command.mjs +16 -0
package/dist/mmap.mjs CHANGED
@@ -1,11 +1,245 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
2
7
 
3
8
  // scripts/mmap.mjs
4
9
  import { existsSync as existsSync2 } from "node:fs";
5
10
  import { dirname as dirname2, join as join2, resolve } from "node:path";
6
11
 
7
- // scripts/pane-core.mjs
12
+ // scripts/watcher-command.mjs
13
+ function watcherArgs(cfg, watchPath, mapFile) {
14
+ const args = [watchPath, "--file", mapFile, ...cfg.watcherFlags];
15
+ if (cfg.pageSlug !== void 0) args.push("--page", cfg.pageSlug);
16
+ return args;
17
+ }
18
+ function manualWatcherCommand(cfg, watchPath, mapFile, nodePath = process.execPath) {
19
+ return [nodePath, ...watcherArgs(cfg, watchPath, mapFile)].map((arg) => "'" + arg.replaceAll("'", `'"'"'`) + "'").join(" ");
20
+ }
21
+ function paneFailureMessage(error, cfg, watchPath, mapFile, platform = process.platform) {
22
+ return platform === "win32" ? error : `${error}
23
+ Automatic opening failed. Do not retry until the terminal environment changes. Run this command in a visible terminal:
24
+ ${manualWatcherCommand(cfg, watchPath, mapFile)}`;
25
+ }
26
+
27
+ // scripts/terminal-session.mjs
28
+ var terminal_session_exports = {};
29
+ __export(terminal_session_exports, {
30
+ focusSession: () => focusSession,
31
+ inspectSession: () => inspectSession,
32
+ openWt: () => openWt,
33
+ parseSessionProbe: () => parseSessionProbe,
34
+ sessionProbeScript: () => sessionProbeScript
35
+ });
8
36
  import { spawnSync } from "node:child_process";
37
+ var NATIVE = String.raw`
38
+ $ErrorActionPreference = 'Stop'
39
+ $ProgressPreference = 'SilentlyContinue'
40
+ Add-Type @"
41
+ using System;
42
+ using System.Text;
43
+ using System.Runtime.InteropServices;
44
+ public class MmapSession {
45
+ public delegate bool Callback(IntPtr hwnd, IntPtr lp);
46
+ [DllImport("user32.dll")] public static extern bool EnumWindows(Callback cb, IntPtr lp);
47
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hwnd);
48
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassName(IntPtr hwnd, StringBuilder text, int size);
49
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr hwnd, StringBuilder text, int size);
50
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
51
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hwnd);
52
+ [DllImport("kernel32.dll")] public static extern bool FreeConsole();
53
+ [DllImport("kernel32.dll")] public static extern bool AttachConsole(uint pid);
54
+ [DllImport("kernel32.dll")] public static extern uint GetConsoleProcessList(uint[] list, uint size);
55
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern bool SetConsoleTitle(string title);
56
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern uint GetConsoleTitle(StringBuilder text, uint size);
57
+ }
58
+ "@
59
+ `;
60
+ function sessionProbeScript(nonce) {
61
+ return NATIVE + String.raw`
62
+ $processes = @{}
63
+ Get-CimInstance Win32_Process | ForEach-Object { $processes[[uint32]$_.ProcessId] = $_ }
64
+ $cursor = [uint32]$PID
65
+ $seen = @{}
66
+ $hwnd = [IntPtr]::Zero
67
+ for ($depth = 0; $depth -lt 16 -and $processes.ContainsKey($cursor); $depth++) {
68
+ [void][MmapSession]::FreeConsole()
69
+ if ([MmapSession]::AttachConsole($cursor)) {
70
+ $members = New-Object uint32[] 4096
71
+ $count = [MmapSession]::GetConsoleProcessList($members, $members.Length)
72
+ if ($count -gt 0 -and $count -le $members.Length) {
73
+ $root = $members[0..($count - 1)] | ForEach-Object { $processes[$_] } |
74
+ Where-Object { $_ -and $_.CreationDate } | Sort-Object CreationDate | Select-Object -First 1
75
+ if ($root) {
76
+ $owner = "split-$($root.ProcessId)-$($root.CreationDate.ToUniversalTime().Ticks)"
77
+ if (-not $seen.ContainsKey($owner)) {
78
+ $seen[$owner] = $true
79
+ Write-Output "CONSOLE=$owner"
80
+ $text = New-Object System.Text.StringBuilder 1024
81
+ [void][MmapSession]::GetConsoleTitle($text, 1024)
82
+ $original = $text.ToString()
83
+ try {
84
+ for ($attempt = 0; $attempt -lt 6 -and $hwnd -eq [IntPtr]::Zero; $attempt++) {
85
+ [void][MmapSession]::SetConsoleTitle('__NONCE__')
86
+ Start-Sleep -Milliseconds 60
87
+ $matches = New-Object System.Collections.ArrayList
88
+ $callback = {
89
+ param($window, $unused)
90
+ if ([MmapSession]::IsWindowVisible($window)) {
91
+ $class = New-Object System.Text.StringBuilder 256
92
+ [void][MmapSession]::GetClassName($window, $class, 256)
93
+ if ($class.ToString() -eq 'CASCADIA_HOSTING_WINDOW_CLASS') {
94
+ $title = New-Object System.Text.StringBuilder 1024
95
+ [void][MmapSession]::GetWindowText($window, $title, 1024)
96
+ if ($title.ToString().Contains('__NONCE__')) { [void]$matches.Add($window) }
97
+ }
98
+ }
99
+ return $true
100
+ }
101
+ [void][MmapSession]::EnumWindows($callback, [IntPtr]::Zero)
102
+ if ($matches.Count -eq 1) { $hwnd = $matches[0] }
103
+ }
104
+ } finally {
105
+ [void][MmapSession]::SetConsoleTitle($original)
106
+ [void][MmapSession]::FreeConsole()
107
+ }
108
+ if ($hwnd -ne [IntPtr]::Zero) {
109
+ Write-Output "OWNER=$owner"
110
+ break
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ $cursor = [uint32]$processes[$cursor].ParentProcessId
117
+ }
118
+ [void][MmapSession]::FreeConsole()
119
+ Write-Output "IDENT=$($hwnd.ToInt64())"
120
+ `.replaceAll("__NONCE__", nonce);
121
+ }
122
+ function powershell(script, timeout = 3e4) {
123
+ const result = spawnSync("powershell.exe", [
124
+ "-NoProfile",
125
+ "-NonInteractive",
126
+ "-ExecutionPolicy",
127
+ "Bypass",
128
+ "-EncodedCommand",
129
+ Buffer.from(script, "utf16le").toString("base64")
130
+ ], { encoding: "utf8", windowsHide: true, timeout });
131
+ return result.status === 0 ? { ok: true, value: result.stdout } : { ok: false, error: `Terminal session probe failed (${result.error?.message ?? `exit ${result.status}`}).` };
132
+ }
133
+ function parseSessionProbe(output) {
134
+ const owners = [...output.matchAll(/^CONSOLE=(split-\d+-\d+)\r?$/gm)].map((m) => m[1]);
135
+ const hwnd = /^IDENT=([1-9]\d*)\r?$/m.exec(output)?.[1];
136
+ const owner = /^OWNER=(split-\d+-\d+)\r?$/m.exec(output)?.[1];
137
+ return { owners, ...hwnd && owner && owners.includes(owner) ? { hwnd, owner } : {} };
138
+ }
139
+ function inspectSession() {
140
+ const result = powershell(sessionProbeScript(`MMAP-SESSION-${process.pid}-${Date.now()}`));
141
+ return result.ok ? { ok: true, value: parseSessionProbe(result.value) } : result;
142
+ }
143
+ function focusSession(hwnd) {
144
+ if (!/^[1-9]\d*$/.test(hwnd)) return { ok: false, error: "Invalid terminal window handle." };
145
+ const result = powershell(NATIVE + `
146
+ $window = [IntPtr]${hwnd}
147
+ if ([MmapSession]::GetForegroundWindow() -ne $window) {
148
+ [void][MmapSession]::SetForegroundWindow($window)
149
+ Start-Sleep -Milliseconds 150
150
+ }
151
+ Write-Output "FOCUS=$([MmapSession]::GetForegroundWindow() -eq $window)"
152
+ `, 5e3);
153
+ if (!result.ok) return result;
154
+ return /^FOCUS=True\r?$/m.test(result.value) ? { ok: true, value: void 0 } : { ok: false, error: "The session window was identified, but Windows refused to focus it. Activate the PowerShell tab for this conversation and retry; no separate window was opened." };
155
+ }
156
+ function openWt(args, what) {
157
+ const result = spawnSync("wt", args, { stdio: "ignore", timeout: 15e3, windowsHide: false });
158
+ return result.status === 0 ? { ok: true, value: void 0 } : { ok: false, error: `wt failed to ${what} (${result.error?.message ?? `exit ${result.status}`}).` };
159
+ }
160
+
161
+ // scripts/tmux-session.mjs
162
+ import { spawnSync as spawnSync2 } from "node:child_process";
163
+ import { createHash } from "node:crypto";
164
+ var SESSION_FORMAT = "#{session_id} #{session_attached}";
165
+ var TARGET_FORMAT = "#{socket_path} #{session_id} #{session_created} #{session_attached} #{pane_id} #{pane_pid}";
166
+ var PANE_FORMAT = "#{pane_id} #{pane_pid} #{window_id} #{window_active} #{window_zoomed_flag} #{pane_active}";
167
+ function createTmuxAdapter({ env = process.env, spawn = spawnSync2, nodePath = process.execPath } = {}) {
168
+ const inherited = /^(.*),\d+,(\d+)$/.exec(env.TMUX ?? "");
169
+ const socket = env.MELLOS_MAPPING_TMUX_SOCKET || inherited?.[1];
170
+ function run(args, socketPath = socket) {
171
+ const result = spawn("tmux", [...socketPath ? ["-S", socketPath] : [], ...args], {
172
+ env,
173
+ encoding: "utf8",
174
+ stdio: ["ignore", "pipe", "pipe"],
175
+ timeout: 5e3,
176
+ windowsHide: true
177
+ });
178
+ return result.status === 0 ? { ok: true, value: result.stdout.trim() } : { ok: false, error: `tmux ${args[0]} failed: ${result.error?.message || result.stderr?.trim() || `exit ${result.status}`}` };
179
+ }
180
+ function inspectSession2(cfg, viewers = []) {
181
+ const sameServer = !env.MELLOS_MAPPING_TMUX_SOCKET || socket === inherited?.[1];
182
+ let selected = env.MELLOS_MAPPING_TMUX_TARGET || (sameServer && inherited ? /^%\d+$/.test(env.TMUX_PANE ?? "") ? env.TMUX_PANE : `$${inherited[2]}:` : void 0);
183
+ if (!selected) {
184
+ const listed = run(["list-sessions", "-F", SESSION_FORMAT]);
185
+ if (!listed.ok) return listed;
186
+ const attached2 = listed.value.split("\n").map((line) => line.split(" ")).filter(([id, count]) => /^\$\d+$/.test(id) && Number(count) > 0).map(([id]) => id);
187
+ if (attached2.length !== 1) return { ok: false, error: attached2.length === 0 ? "No attached tmux session was found. Attach a session before retrying." : `Multiple attached tmux sessions (${attached2.join(", ")}). Set MELLOS_MAPPING_TMUX_TARGET to the intended session or pane in the MCP server environment before retrying.` };
188
+ selected = `${attached2[0]}:`;
189
+ }
190
+ const inspected = run(["display-message", "-p", "-t", selected, TARGET_FORMAT]);
191
+ if (!inspected.ok) return inspected;
192
+ const [socketPath, session, created, attached, pane, panePid] = inspected.value.split(" ");
193
+ if (!socketPath || !/^\$\d+$/.test(session) || !/^\d+$/.test(created) || !/^%\d+$/.test(pane)) {
194
+ return { ok: false, error: "tmux returned an invalid session target; no pane was opened." };
195
+ }
196
+ if (!(Number(attached) > 0)) return { ok: false, error: `tmux session ${session} has no attached client. Attach it before retrying.` };
197
+ const identity = [socketPath, session, created, cfg.mode, cfg.mode === "window" ? "" : pane];
198
+ const selectedViewer = cfg.mode === "split" ? viewers.find((viewer) => viewer.pid === Number(panePid) && /^tmux-[a-f0-9]{24}$/.test(viewer.owner ?? "")) : void 0;
199
+ const owner = selectedViewer?.owner ?? `tmux-${createHash("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 24)}`;
200
+ return { ok: true, value: { backend: "tmux", socket: socketPath, session, pane, owner, owners: [owner] } };
201
+ }
202
+ function revealPane(target, viewer) {
203
+ const listed = run(["list-panes", "-s", "-t", target.session, "-F", PANE_FORMAT], target.socket);
204
+ if (!listed.ok) return listed;
205
+ const pane = listed.value.split("\n").map((line) => line.split(" ")).find((fields) => Number(fields[1]) === viewer.pid);
206
+ if (!pane || !/^%\d+$/.test(pane[0]) || !/^@\d+$/.test(pane[2])) {
207
+ return { ok: false, error: "The live watcher could not be located in this tmux session; visibility is unverified." };
208
+ }
209
+ const selected = run(["select-window", "-t", `${target.session}:${pane[2]}`], target.socket);
210
+ if (!selected.ok) return selected;
211
+ if (pane[4] === "1" && pane[5] !== "1") {
212
+ const unzoomed = run(["resize-pane", "-Z", "-t", pane[0]], target.socket);
213
+ if (!unzoomed.ok) return unzoomed;
214
+ }
215
+ const visible = run([
216
+ "display-message",
217
+ "-p",
218
+ "-t",
219
+ `${target.session}:${pane[2]}.${pane[0]}`,
220
+ "#{session_attached} #{window_active} #{window_zoomed_flag} #{pane_active}"
221
+ ], target.socket);
222
+ if (!visible.ok) return visible;
223
+ const [attached, active, zoomed, focused] = visible.value.split(" ");
224
+ return Number(attached) > 0 && active === "1" && (zoomed !== "1" || focused === "1") ? { ok: true, value: "visible" } : { ok: false, error: "The watcher is running, but its tmux pane is not visible in the attached session." };
225
+ }
226
+ function openPane(cfg, watchPath, mapFile, target) {
227
+ const placement = target.mode === "window" ? ["new-window", "-n", "mellos-mapping", "-t", `${target.session}:`] : ["split-window", "-h", "-d", "-l", "42%", "-t", target.pane];
228
+ const result = run([
229
+ ...placement,
230
+ "-c",
231
+ cfg.projectDir,
232
+ nodePath,
233
+ ...watcherArgs(cfg, watchPath, mapFile),
234
+ "--owner",
235
+ target.owner
236
+ ], target.socket);
237
+ return result.ok ? { ok: true, value: { ...target, reason: "requested" } } : result;
238
+ }
239
+ return { kind: "tmux", inspectSession: inspectSession2, openPane, revealPane };
240
+ }
241
+
242
+ // scripts/pane-core.mjs
9
243
  import { existsSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
244
  import { dirname, join } from "node:path";
11
245
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -57,169 +291,60 @@ function writeQuitRequest(quitFile) {
57
291
  writeRequest(quitFile, {});
58
292
  }
59
293
  function paneCommand(cfg, watchPath, mapFile) {
60
- const cmd = ["--title", "mellos map", "-d", cfg.projectDir, "node", watchPath, "--file", mapFile, ...cfg.watcherFlags];
61
- if (cfg.pageSlug !== void 0) cmd.push("--page", cfg.pageSlug);
62
- return cmd;
63
- }
64
- function powerShellQuote(value) {
65
- return `'${value.replace(/'/g, "''")}'`;
66
- }
67
- function watcherProbeScript(mapFile) {
68
- return `$ErrorActionPreference = 'SilentlyContinue'
69
- $needle = ${powerShellQuote(mapFile)}
70
- $w = @(Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object {
71
- $_.CommandLine -and
72
- $_.CommandLine.IndexOf('watch.mjs', [StringComparison]::OrdinalIgnoreCase) -ge 0 -and
73
- $_.CommandLine.IndexOf($needle, [StringComparison]::OrdinalIgnoreCase) -ge 0
74
- })
75
- Write-Output "WATCHERS=$($w.Count)"`;
294
+ return ["--title", "mellos map", "-d", cfg.projectDir, "node", ...watcherArgs(cfg, watchPath, mapFile)];
76
295
  }
77
- var IDENTIFY_AND_FOCUS = String.raw`
78
- $ErrorActionPreference = 'SilentlyContinue'
79
- Add-Type @"
80
- using System;
81
- using System.Text;
82
- using System.Runtime.InteropServices;
83
- public class MmapWin {
84
- [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lp);
85
- public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lp);
86
- [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
87
- [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassName(IntPtr hWnd, StringBuilder sb, int max);
88
- [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder sb, int max);
89
- [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
90
- [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
91
- [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd);
92
- [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
93
- [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
94
- [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
95
- [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, IntPtr dwExtraInfo);
96
- [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
97
- [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
98
- [DllImport("kernel32.dll")] public static extern bool FreeConsole();
99
- [DllImport("kernel32.dll")] public static extern bool AttachConsole(uint pid);
100
- [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern bool SetConsoleTitle(string title);
101
- [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern uint GetConsoleTitle(StringBuilder sb, uint size);
296
+ var DEDICATED_WINDOW_NAME = "mellos-mapping";
297
+ var SPLIT_SIZE = "0.42";
298
+ function selectPaneViewer(viewers, owners) {
299
+ return viewers.find((viewer) => viewer.owner !== void 0 && owners.includes(viewer.owner));
102
300
  }
103
- "@
104
- function Get-WtWindows {
105
- $wins = New-Object System.Collections.ArrayList
106
- $cb = {
107
- param($h, $lp)
108
- if ([MmapWin]::IsWindowVisible($h)) {
109
- $cls = New-Object System.Text.StringBuilder 256
110
- [void][MmapWin]::GetClassName($h, $cls, 256)
111
- if ($cls.ToString() -eq 'CASCADIA_HOSTING_WINDOW_CLASS') {
112
- $t = New-Object System.Text.StringBuilder 512
113
- [void][MmapWin]::GetWindowText($h, $t, 512)
114
- [void]$wins.Add(@{ hwnd = $h.ToInt64(); title = $t.ToString() })
115
- }
116
- }
117
- return $true
301
+ var platformTerminal = () => process.platform === "win32" ? terminal_session_exports : createTmuxAdapter();
302
+ function preparePane(cfg, store, mapFile, io = platformTerminal()) {
303
+ const viewers = store.readLiveViewers(mapFile, Date.now());
304
+ const inspected = io.kind === "tmux" ? io.inspectSession(cfg, viewers) : cfg.mode === PANE_MODE.window ? { ok: true, value: { owners: ["window"], owner: "window" } } : io.inspectSession();
305
+ if (!inspected.ok) return inspected;
306
+ const session = inspected.value;
307
+ const viewer = selectPaneViewer(viewers, session.owners);
308
+ if (io.kind !== "tmux" && cfg.mode === PANE_MODE.split && !session.hwnd && (!viewer || cfg.force)) {
309
+ return { ok: false, error: "Could not identify this conversation\u2019s active Windows Terminal pane. Activate its PowerShell tab and retry; no separate window was opened. Use --window only if you want a separate window." };
118
310
  }
119
- [void][MmapWin]::EnumWindows($cb, [IntPtr]::Zero)
120
- return ,$wins
311
+ return { ok: true, value: {
312
+ target: { ...session, mode: cfg.mode, owner: session.owner ?? viewer?.owner },
313
+ viewer,
314
+ previousPids: viewers.map((item) => item.pid)
315
+ } };
121
316
  }
122
-
123
- $ancestors = @()
124
- $p = $PID
125
- for ($i = 0; $i -lt 12 -and $p; $i++) {
126
- $proc = Get-CimInstance Win32_Process -Filter "ProcessId=$p"
127
- if (-not $proc) { break }
128
- if ($i -gt 0) { $ancestors += [uint32]$proc.ProcessId }
129
- $p = $proc.ParentProcessId
130
- }
131
-
132
- # The agent CLI (claude/codex/...) is some ancestor holding the console that a
133
- # WT window renders; hidden-console ancestors just never light a window up.
134
- $nonce = "__NONCE__"
135
- $hwnd = [IntPtr]::Zero
136
- foreach ($apid in $ancestors) {
137
- [void][MmapWin]::FreeConsole()
138
- if (-not [MmapWin]::AttachConsole($apid)) { continue }
139
- $sb = New-Object System.Text.StringBuilder 1024
140
- [void][MmapWin]::GetConsoleTitle($sb, 1024)
141
- $orig = $sb.ToString()
142
- for ($i = 0; $i -lt 6 -and $hwnd -eq [IntPtr]::Zero; $i++) {
143
- [void][MmapWin]::SetConsoleTitle($nonce)
144
- Start-Sleep -Milliseconds 60
145
- foreach ($w in (Get-WtWindows)) {
146
- if ($w.title -like "*$nonce*") { $hwnd = [IntPtr]$w.hwnd; break }
147
- }
317
+ function placePane(cfg, watchPath, mapFile, target, io = platformTerminal()) {
318
+ if (io.kind === "tmux") return io.openPane(cfg, watchPath, mapFile, target);
319
+ const payload = [...paneCommand(cfg, watchPath, mapFile), "--owner", target.owner];
320
+ if (target.mode === PANE_MODE.window) {
321
+ const opened2 = io.openWt(["-w", DEDICATED_WINDOW_NAME, "nt", ...payload], "open the requested window");
322
+ return opened2.ok ? { ok: true, value: { ...target, reason: "requested" } } : opened2;
148
323
  }
149
- Start-Sleep -Milliseconds 100
150
- [void][MmapWin]::SetConsoleTitle($orig)
151
- Start-Sleep -Milliseconds 200
152
- [void][MmapWin]::FreeConsole()
153
- if ($hwnd -ne [IntPtr]::Zero) { break }
324
+ const focused = io.focusSession(target.hwnd);
325
+ if (!focused.ok) return focused;
326
+ const opened = io.openWt([
327
+ "-w",
328
+ "0",
329
+ "sp",
330
+ "-V",
331
+ "--size",
332
+ SPLIT_SIZE,
333
+ ...payload,
334
+ ";",
335
+ "move-focus",
336
+ "previous"
337
+ ], "split the session window");
338
+ return opened.ok ? { ok: true, value: target } : opened;
154
339
  }
155
-
156
- if ($hwnd -eq [IntPtr]::Zero) { Write-Output 'IDENT=0'; exit 0 }
157
- Write-Output "IDENT=$($hwnd.ToInt64())"
158
-
159
- if ([MmapWin]::IsIconic($hwnd)) { [void][MmapWin]::ShowWindow($hwnd, 9) }
160
- $focused = $false
161
- [void][MmapWin]::SetForegroundWindow($hwnd)
162
- Start-Sleep -Milliseconds 150
163
- if ([MmapWin]::GetForegroundWindow() -eq $hwnd) { $focused = $true }
164
- if (-not $focused) {
165
- [MmapWin]::keybd_event(0x12, 0, 0, [IntPtr]::Zero)
166
- [void][MmapWin]::SetForegroundWindow($hwnd)
167
- [MmapWin]::keybd_event(0x12, 0, 2, [IntPtr]::Zero)
168
- Start-Sleep -Milliseconds 150
169
- if ([MmapWin]::GetForegroundWindow() -eq $hwnd) { $focused = $true }
170
- }
171
- if (-not $focused) {
172
- $fgpid = 0
173
- $fgThread = [MmapWin]::GetWindowThreadProcessId([MmapWin]::GetForegroundWindow(), [ref]$fgpid)
174
- $myThread = [MmapWin]::GetCurrentThreadId()
175
- [void][MmapWin]::AttachThreadInput($myThread, $fgThread, $true)
176
- [void][MmapWin]::BringWindowToTop($hwnd)
177
- [void][MmapWin]::SetForegroundWindow($hwnd)
178
- [void][MmapWin]::AttachThreadInput($myThread, $fgThread, $false)
179
- Start-Sleep -Milliseconds 150
180
- if ([MmapWin]::GetForegroundWindow() -eq $hwnd) { $focused = $true }
181
- }
182
- Write-Output "FOCUS=$(if ($focused) { 1 } else { 0 })"
183
- `;
184
- var PROBE_TIMEOUT_MS = 3e4;
185
- var WT_TIMEOUT_MS = 15e3;
186
- var SPLIT_SIZE = "0.42";
187
- var DEDICATED_WINDOW_NAME = "mellos-mapping";
188
- function runPowerShell(script) {
189
- const encoded = Buffer.from(script, "utf16le").toString("base64");
190
- const r = spawnSync(
191
- "powershell.exe",
192
- ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded],
193
- { encoding: "utf8", timeout: PROBE_TIMEOUT_MS, windowsHide: true }
194
- );
195
- return r.stdout ?? "";
196
- }
197
- function paneIsOpen(store, mapFile) {
198
- if (store.readLiveViewers(mapFile, Date.now()).length > 0) return true;
199
- if (process.platform !== "win32") return false;
200
- const m = runPowerShell(watcherProbeScript(mapFile)).match(/WATCHERS=(\d+)/);
201
- return m !== null && Number(m[1]) > 0;
202
- }
203
- function openWt(args, what) {
204
- const r = spawnSync("wt", args, { stdio: "ignore", timeout: WT_TIMEOUT_MS, windowsHide: true });
205
- return r.status === 0 ? { ok: true, value: void 0 } : { ok: false, error: `wt failed to ${what} (exit ${r.status ?? "timeout"}) \u2014 is Windows Terminal installed?` };
206
- }
207
- function placePane(cfg, watchPath, mapFile) {
208
- const dedicated = (reason) => {
209
- const opened = openWt(["-w", DEDICATED_WINDOW_NAME, "nt", ...paneCommand(cfg, watchPath, mapFile)], "open the dedicated window");
210
- return opened.ok ? { ok: true, value: { mode: PANE_MODE.window, reason } } : opened;
211
- };
212
- if (cfg.mode === PANE_MODE.window) return dedicated("requested");
213
- const nonce = `MMAP-NONCE-${process.pid}`;
214
- const out = runPowerShell(IDENTIFY_AND_FOCUS.replaceAll("__NONCE__", nonce));
215
- const ident = out.match(/IDENT=(\d+)/)?.[1] ?? "0";
216
- if (ident === "0") return dedicated("session-window-not-identified");
217
- if (!/FOCUS=1/.test(out)) return dedicated("session-window-focus-denied");
218
- const split = openWt(
219
- ["-w", "0", "sp", "-V", "--size", SPLIT_SIZE, ...paneCommand(cfg, watchPath, mapFile)],
220
- "split the session window"
221
- );
222
- return split.ok ? { ok: true, value: { mode: PANE_MODE.split, hwnd: ident } } : split;
340
+ async function awaitNewPane(store, mapFile, context, timeoutMs = 8e3) {
341
+ const deadline = Date.now() + timeoutMs;
342
+ do {
343
+ const viewer = store.readLiveViewers(mapFile, Date.now()).find((item) => item.owner === context.target.owner && !context.previousPids.includes(item.pid));
344
+ if (viewer) return { ok: true, value: viewer };
345
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
346
+ } while (Date.now() < deadline);
347
+ return { ok: false, error: "The requested pane has not reported in. Its placement is unverified; do not assume that another open map is the requested split." };
223
348
  }
224
349
 
225
350
  // scripts/mmap.mjs
@@ -293,33 +418,40 @@ async function main() {
293
418
  console.error(parsed.error);
294
419
  process.exit(1);
295
420
  }
296
- if (process.platform !== "win32") {
297
- console.error("mmap opens the pane through Windows Terminal \u2014 on other platforms run the watcher yourself:");
298
- console.error(` node "${watchPath}" --file "<project>/${store.STATE_FILE_RELATIVE_PATH}"`);
299
- process.exit(1);
300
- }
301
421
  const candidates = storeSearchPath(process.cwd());
302
422
  const marker = storeMarkerOf(store.STATE_FILE_RELATIVE_PATH);
303
423
  const project = nearestProject(candidates, candidates.map((dir) => existsSync2(join2(dir, marker))));
304
424
  const cfg = { ...parsed.value, projectDir: project.root };
305
425
  const mapFile = join2(project.root, store.STATE_FILE_RELATIVE_PATH);
306
- const action = toggleAction(paneIsOpen(store, mapFile), cfg.pageSlug, cfg.force);
426
+ const fail = (error) => {
427
+ console.error(paneFailureMessage(error, cfg, watchPath, mapFile));
428
+ process.exit(1);
429
+ };
430
+ const prepared = preparePane(cfg, store, mapFile);
431
+ if (!prepared.ok) {
432
+ fail(prepared.error);
433
+ }
434
+ const context = prepared.value;
435
+ const action = toggleAction(context.viewer !== void 0, cfg.pageSlug, cfg.force);
307
436
  if (action.kind === "quit") {
308
- writeQuitRequest(store.quitFilePath(mapFile));
437
+ writeQuitRequest(store.quitFilePath(mapFile, context.viewer.pid));
309
438
  console.log(`Closing the map pane for ${project.root}.`);
310
439
  return;
311
440
  }
312
441
  if (action.kind === "focus") {
313
- writeFocusRequest(store.focusFilePath(mapFile), action.page);
442
+ writeFocusRequest(store.focusFilePath(mapFile, context.viewer.pid), action.page);
314
443
  console.log(`The map pane for ${project.root} is already open \u2014 showing page "${action.page}".`);
315
444
  return;
316
445
  }
317
- const placed = placePane(cfg, watchPath, mapFile);
446
+ const placed = placePane(cfg, watchPath, mapFile, context.target);
318
447
  if (!placed.ok) {
319
- console.error(placed.error);
320
- process.exit(1);
448
+ fail(placed.error);
449
+ }
450
+ const reported = await awaitNewPane(store, mapFile, context);
451
+ if (!reported.ok) {
452
+ fail(reported.error);
321
453
  }
322
- const where = placed.value.mode === PANE_MODE.window ? `in the dedicated "${DEDICATED_WINDOW_NAME}" window (${placed.value.reason})` : "beside this terminal (vertical split)";
454
+ const where = placed.value.backend === "tmux" ? placed.value.mode === PANE_MODE.window ? "in a new tmux window" : "beside this terminal (tmux split)" : placed.value.mode === PANE_MODE.window ? `in the dedicated "${DEDICATED_WINDOW_NAME}" window (${placed.value.reason})` : "beside this terminal (vertical split)";
323
455
  console.log(`Map opened for ${project.root} ${where}.`);
324
456
  if (!project.found) {
325
457
  console.log(