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.
- package/README.md +125 -88
- package/README.zh-CN.md +122 -84
- package/dist/hook-session-start.mjs +25 -19
- package/dist/mmap.mjs +302 -170
- package/dist/preview.mjs +1418 -0
- package/dist/server.mjs +1198 -400
- package/dist/store-paths.mjs +40 -21
- package/dist/terminal-worker.mjs +3188 -0
- package/dist/watch.mjs +523 -280
- package/dist/web/TERMINAL-LICENSES.txt +70 -0
- package/dist/web/app.css +967 -0
- package/dist/web/app.js +1356 -0
- package/dist/web/index.html +9 -0
- package/dist/web/terminal.css +9 -0
- package/dist/web/terminal.html +7 -0
- package/dist/web/terminal.js +9293 -0
- package/dist/web/xterm.css +285 -0
- package/dist/web.mjs +5535 -0
- package/docs/codex.md +183 -0
- package/lib/domain/text.d.ts +9 -0
- package/lib/domain/text.js +43 -0
- package/lib/domain/types.js +10 -1
- package/lib/preview/index.d.ts +3 -0
- package/lib/preview/index.js +3 -0
- package/lib/preview/markdown.d.ts +8 -0
- package/lib/preview/markdown.js +54 -0
- package/lib/preview/presentation.d.ts +6 -0
- package/lib/preview/presentation.js +14 -0
- package/lib/preview/publisher.d.ts +23 -0
- package/lib/preview/publisher.js +143 -0
- package/lib/preview/svg.d.ts +3 -0
- package/lib/preview/svg.js +74 -0
- package/lib/preview/text.d.ts +4 -0
- package/lib/preview/text.js +13 -0
- package/lib/render/canvas.d.ts +1 -1
- package/lib/render/canvas.js +4 -2
- package/lib/render/draw.js +9 -6
- package/lib/render/render.d.ts +6 -0
- package/lib/render/render.js +50 -18
- package/lib/render/width.js +3 -1
- package/lib/store/atomic.d.ts +18 -0
- package/lib/store/atomic.js +86 -0
- package/lib/store/channels.d.ts +40 -0
- package/lib/store/channels.js +135 -0
- package/lib/store/format.js +3 -1
- package/lib/store/json-text.d.ts +9 -0
- package/lib/store/json-text.js +16 -0
- package/lib/store/maps.d.ts +12 -0
- package/lib/store/maps.js +42 -0
- package/lib/store/migration.d.ts +12 -0
- package/lib/store/migration.js +46 -0
- package/lib/store/pages.d.ts +46 -0
- package/lib/store/pages.js +89 -0
- package/lib/store/policy.d.ts +74 -0
- package/lib/store/policy.js +144 -0
- package/lib/store/store.d.ts +9 -256
- package/lib/store/store.js +10 -694
- package/lib/store/viewers.d.ts +81 -0
- package/lib/store/viewers.js +186 -0
- package/package.json +25 -6
- package/scripts/codex-cli.mjs +42 -0
- package/scripts/codex-register.mjs +27 -94
- package/scripts/mmap.mjs +26 -16
- package/scripts/open-pane.mjs +37 -18
- package/scripts/pane-core.mjs +57 -220
- package/scripts/terminal-session.mjs +137 -0
- package/scripts/tmux-session.mjs +90 -0
- 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/
|
|
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
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
-
|
|
320
|
-
|
|
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(
|