mellos-mapping 0.27.0 → 0.28.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 +9 -2
- package/README.zh-CN.md +8 -2
- package/commands/mmap.md +6 -1
- package/dist/hook-session-start.mjs +0 -0
- package/dist/mmap.mjs +145 -9
- package/dist/preview.mjs +0 -0
- package/dist/server.mjs +7 -6
- package/dist/terminal-worker.mjs +14 -5
- package/dist/watch.mjs +14 -5
- package/dist/web.mjs +0 -0
- package/package.json +2 -1
- package/scripts/codex-register.mjs +0 -0
- package/scripts/install-mmap-command.mjs +0 -0
- package/scripts/mmap.mjs +2 -0
- package/scripts/open-pane.mjs +5 -1
- package/scripts/pane-core.mjs +11 -5
- package/scripts/pebrel-session.mjs +166 -0
- package/scripts/terminal-session.mjs +2 -0
- package/scripts/watcher-command.mjs +6 -3
- package/skills/mellos-mapping/SKILL.md +3 -0
package/README.md
CHANGED
|
@@ -337,8 +337,8 @@ get the eight `mmap_*` tools and the pane, and bring their own prompting.
|
|
|
337
337
|
opens or retargets the pane with `mmap_open` when nobody is — you never
|
|
338
338
|
have to remember to. Open or close it yourself with `mmap` in any
|
|
339
339
|
terminal, or `/mellos-mapping:mmap` in the conversation (Windows
|
|
340
|
-
Terminal split on Windows, tmux split inside tmux,
|
|
341
|
-
to run in any second terminal). On Windows the pane opens in the terminal
|
|
340
|
+
Terminal split on Windows, tmux split inside tmux, a Pebrel split inside
|
|
341
|
+
Pebrel, or a printed command to run in any second terminal). On Windows the pane opens in the terminal
|
|
342
342
|
window hosting YOUR session, even with several windows open; pass
|
|
343
343
|
`--window` to put the map in its own dedicated window instead. Prefer
|
|
344
344
|
`--ascii` if your font lacks box-drawing glyphs.
|
|
@@ -359,6 +359,13 @@ the environment of the MCP server (restart it after changing them):
|
|
|
359
359
|
| `MELLOS_MAPPING_TMUX_TARGET` | Explicit tmux session or pane, such as `work:2.1` or `%7` |
|
|
360
360
|
| `MELLOS_MAPPING_TMUX_SOCKET` | Absolute socket path, for example `/tmp/my-tmux/socket` |
|
|
361
361
|
|
|
362
|
+
Inside [Pebrel](https://github.com/Kuddev/pebrel), `mmap_open` and `mmap` split the pane that hosts
|
|
363
|
+
the conversation through Pebrel's runtime CLI (`PEBREL_CLI`). The launcher
|
|
364
|
+
confirms which window that pane is in from its process tree, returns input focus
|
|
365
|
+
to the conversation and types the watcher into the new pane's shell (PowerShell,
|
|
366
|
+
cmd or a POSIX shell), which exits with it so the pane closes with the map.
|
|
367
|
+
`--window` opens a new Pebrel window. Inside tmux, tmux owns the split.
|
|
368
|
+
|
|
362
369
|
Ambiguous sessions, detached sessions and missing tmux produce a concrete error
|
|
363
370
|
and a fully quoted watcher command, including the project and requested page,
|
|
364
371
|
to paste into any visible terminal. Map writes remain available; after an open
|
package/README.zh-CN.md
CHANGED
|
@@ -310,8 +310,8 @@ npx -y -p mellos-mapping mellos-mapping-watch
|
|
|
310
310
|
(见[谁在看](#谁在看));没人看的时候,Claude 用 `mmap_open` 自己
|
|
311
311
|
把面板开出来或切到该看的那一页——你不需要记着去开它。想自己开关,
|
|
312
312
|
在任意终端敲 `mmap`,或者在对话里用 `/mellos-mapping:mmap`
|
|
313
|
-
(Windows 上是 Windows Terminal 分屏,tmux 里是 tmux
|
|
314
|
-
|
|
313
|
+
(Windows 上是 Windows Terminal 分屏,tmux 里是 tmux 分屏,Pebrel 里
|
|
314
|
+
是 Pebrel 分屏,其他环境会打印一条命令让你在第二个终端里运行)。Windows 上即使开着多个
|
|
315
315
|
终端窗口,分屏也会落在**你的会话所在的窗口**;想让地图独占一个窗口
|
|
316
316
|
就加 `--window`。字体缺少制表符字形时用 `--ascii`。
|
|
317
317
|
3. 看着节点从底部一路亮起。图让你不安的时候就打断它——这正是它存在的
|
|
@@ -330,6 +330,12 @@ tmux 服务中唯一已连接的会话。默认在右侧分屏并保留输入焦
|
|
|
330
330
|
| `MELLOS_MAPPING_TMUX_TARGET` | 指定 tmux 会话或面板,例如 `work:2.1` 或 `%7` |
|
|
331
331
|
| `MELLOS_MAPPING_TMUX_SOCKET` | socket 的绝对路径,例如 `/tmp/my-tmux/socket` |
|
|
332
332
|
|
|
333
|
+
在 [Pebrel](https://github.com/Kuddev/pebrel) 里,`mmap_open` 与 `mmap` 通过 Pebrel 运行时 CLI
|
|
334
|
+
(`PEBREL_CLI`)分屏承载本会话的面板。启动器用进程树确认该面板所在的窗口,
|
|
335
|
+
把输入焦点还给对话,再把 watcher 命令输入新面板的 shell(PowerShell、cmd
|
|
336
|
+
或 POSIX shell);shell 随 watcher 一起退出,地图关闭时面板也随之关闭。
|
|
337
|
+
`--window` 会新开一个 Pebrel 窗口。在 tmux 里则由 tmux 负责分屏。
|
|
338
|
+
|
|
333
339
|
目标不明确、会话未连接或没有安装 tmux 时,启动器会返回具体原因,以及
|
|
334
340
|
包含项目、页面和正确引号的一条完整 watcher 命令,可以粘贴到可见终端。
|
|
335
341
|
地图仍可正常写入;打开失败后,助手只在环境变化或你要求重试时再次打开。
|
package/commands/mmap.md
CHANGED
|
@@ -104,7 +104,12 @@ Follow the platform-appropriate route:
|
|
|
104
104
|
also set `MELLOS_MAPPING_TMUX_SOCKET` to its absolute path, then restart the
|
|
105
105
|
MCP server. Do not guess a session or open a detached window.
|
|
106
106
|
|
|
107
|
-
3. **
|
|
107
|
+
3. **Pebrel** (`PEBREL_CLI` set and not inside tmux): use the same `mmap_open`
|
|
108
|
+
tool or launcher command as route 1. It splits the Pebrel pane hosting this
|
|
109
|
+
conversation, returns input focus to it and types the watcher into the new
|
|
110
|
+
pane's shell; `--window` opens a new Pebrel window.
|
|
111
|
+
|
|
112
|
+
4. **None of these**: print the watcher command and tell the user to run it in any
|
|
108
113
|
second terminal themselves (add `--ascii` if their font lacks box-drawing
|
|
109
114
|
characters). The launcher's own failure message quotes it in full, with
|
|
110
115
|
absolute paths — relay that one verbatim. Written by hand it is
|
|
File without changes
|
package/dist/mmap.mjs
CHANGED
|
@@ -15,8 +15,11 @@ function watcherArgs(cfg, watchPath, mapFile) {
|
|
|
15
15
|
if (cfg.pageSlug !== void 0) args.push("--page", cfg.pageSlug);
|
|
16
16
|
return args;
|
|
17
17
|
}
|
|
18
|
+
function posixQuote(arg) {
|
|
19
|
+
return "'" + arg.replaceAll("'", `'"'"'`) + "'";
|
|
20
|
+
}
|
|
18
21
|
function manualWatcherCommand(cfg, watchPath, mapFile, nodePath = process.execPath) {
|
|
19
|
-
return [nodePath, ...watcherArgs(cfg, watchPath, mapFile)].map(
|
|
22
|
+
return [nodePath, ...watcherArgs(cfg, watchPath, mapFile)].map(posixQuote).join(" ");
|
|
20
23
|
}
|
|
21
24
|
function paneFailureMessage(error, cfg, watchPath, mapFile, platform = process.platform) {
|
|
22
25
|
return platform === "win32" ? error : `${error}
|
|
@@ -29,11 +32,13 @@ var terminal_session_exports = {};
|
|
|
29
32
|
__export(terminal_session_exports, {
|
|
30
33
|
focusSession: () => focusSession,
|
|
31
34
|
inspectSession: () => inspectSession,
|
|
35
|
+
kind: () => kind,
|
|
32
36
|
openWt: () => openWt,
|
|
33
37
|
parseSessionProbe: () => parseSessionProbe,
|
|
34
38
|
sessionProbeScript: () => sessionProbeScript
|
|
35
39
|
});
|
|
36
40
|
import { spawnSync } from "node:child_process";
|
|
41
|
+
var kind = "wt";
|
|
37
42
|
var NATIVE = String.raw`
|
|
38
43
|
$ErrorActionPreference = 'Stop'
|
|
39
44
|
$ProgressPreference = 'SilentlyContinue'
|
|
@@ -158,13 +163,144 @@ function openWt(args, what) {
|
|
|
158
163
|
return result.status === 0 ? { ok: true, value: void 0 } : { ok: false, error: `wt failed to ${what} (${result.error?.message ?? `exit ${result.status}`}).` };
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
// scripts/
|
|
166
|
+
// scripts/pebrel-session.mjs
|
|
162
167
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
163
168
|
import { createHash } from "node:crypto";
|
|
169
|
+
var CTL_TIMEOUT_MS = 1e4;
|
|
170
|
+
var SPLIT_RATIO = "0.42";
|
|
171
|
+
var TERMINAL_TAB_KINDS = ["shell", "profile"];
|
|
172
|
+
function hostedByPebrel(env) {
|
|
173
|
+
return Boolean(env.PEBREL_CLI && env.PEBREL_PANE_ID) && !env.TMUX;
|
|
174
|
+
}
|
|
175
|
+
function shellCommandLine(executable, argv) {
|
|
176
|
+
const shell = executable.toLowerCase().replace(/\.exe$/, "");
|
|
177
|
+
switch (shell) {
|
|
178
|
+
case "pwsh":
|
|
179
|
+
case "powershell":
|
|
180
|
+
return { ok: true, value: `& ${argv.map(powershellQuote).join(" ")}; exit` };
|
|
181
|
+
case "cmd":
|
|
182
|
+
return argv.some((arg) => /["%]/.test(arg)) ? { ok: false, error: 'cmd cannot quote a path containing " or %; no pane was kept.' } : { ok: true, value: `${argv.map((arg) => `"${arg}"`).join(" ")} & exit` };
|
|
183
|
+
case "sh":
|
|
184
|
+
case "bash":
|
|
185
|
+
case "zsh":
|
|
186
|
+
case "dash":
|
|
187
|
+
case "ksh":
|
|
188
|
+
return { ok: true, value: `exec ${argv.map(posixQuote).join(" ")}` };
|
|
189
|
+
default:
|
|
190
|
+
return { ok: false, error: `The new Pebrel pane runs ${executable}; the map starts only from PowerShell, cmd or a POSIX shell. No pane was kept.` };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function powershellQuote(arg) {
|
|
194
|
+
return `'${arg.replace(/['‘’‚‛]/g, "$&$&")}'`;
|
|
195
|
+
}
|
|
196
|
+
function createPebrelAdapter({ env = process.env, spawn = spawnSync2, nodePath = process.execPath, pid = process.pid } = {}) {
|
|
197
|
+
function ctl(command, ...args) {
|
|
198
|
+
const result = spawn(env.PEBREL_CLI, ["ctl", command, ...args.map(String)], {
|
|
199
|
+
encoding: "utf8",
|
|
200
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
201
|
+
timeout: CTL_TIMEOUT_MS,
|
|
202
|
+
windowsHide: true
|
|
203
|
+
});
|
|
204
|
+
const reply = parseReply(result.stdout);
|
|
205
|
+
return reply?.ok === true ? { ok: true, value: reply.result } : { ok: false, error: `pebrel ctl ${command} failed: ${reply?.error?.message ?? result.error?.message ?? (result.stderr?.trim() || `exit ${result.status}`)}` };
|
|
206
|
+
}
|
|
207
|
+
function hosts(window, pane, processId) {
|
|
208
|
+
const tree = ctl("procs", "--window", window, "--pane", pane);
|
|
209
|
+
return tree.ok ? { ok: true, value: tree.value.processes.some((item) => item.pid === processId) } : tree;
|
|
210
|
+
}
|
|
211
|
+
function inspectSession2(cfg) {
|
|
212
|
+
const snapshot = ctl("snapshot");
|
|
213
|
+
if (!snapshot.ok) return snapshot;
|
|
214
|
+
const pane = Number(env.PEBREL_PANE_ID);
|
|
215
|
+
for (const window of snapshot.value.windows) {
|
|
216
|
+
if (!window.tabs.some((tab) => tab.panes.some((item) => item.id === pane))) continue;
|
|
217
|
+
const ours = hosts(window.id, pane, pid);
|
|
218
|
+
if (!ours.ok) return ours;
|
|
219
|
+
if (!ours.value) continue;
|
|
220
|
+
const identity = [snapshot.value.process_id, cfg.mode, cfg.mode === "window" ? "" : `${window.id}:${pane}`];
|
|
221
|
+
const owner = `pebrel-${createHash("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 24)}`;
|
|
222
|
+
return { ok: true, value: { backend: "pebrel", window: window.id, pane, owner, owners: [owner] } };
|
|
223
|
+
}
|
|
224
|
+
return { ok: false, error: `This process is not running inside Pebrel pane ${env.PEBREL_PANE_ID}; no pane was opened.` };
|
|
225
|
+
}
|
|
226
|
+
function createPane(target) {
|
|
227
|
+
if (target.mode === "window") return ctl("new-window");
|
|
228
|
+
const split = ctl("split", "--window", target.window, "--pane", target.pane, "--direction", "right");
|
|
229
|
+
if (!split.ok) return split;
|
|
230
|
+
const created = split.value.action;
|
|
231
|
+
const focused = ctl("focus", "--window", target.window, "--pane", target.pane);
|
|
232
|
+
const resized = focused.ok ? ctl("resize-pane", "--window", created.window_id, "--pane", created.pane_id, "--ratio", SPLIT_RATIO) : focused;
|
|
233
|
+
if (resized.ok) return split;
|
|
234
|
+
ctl("close-pane", "--window", created.window_id, "--pane", created.pane_id);
|
|
235
|
+
return resized;
|
|
236
|
+
}
|
|
237
|
+
function startWatcher(cfg, watchPath, mapFile, target, created) {
|
|
238
|
+
const tree = ctl("procs", "--window", created.window_id, "--pane", created.pane_id);
|
|
239
|
+
if (!tree.ok) return tree;
|
|
240
|
+
const shell = tree.value.processes.find((item) => item.depth === 0)?.executable ?? "no shell";
|
|
241
|
+
const line = shellCommandLine(shell, [nodePath, ...watcherArgs(cfg, watchPath, mapFile), "--owner", target.owner]);
|
|
242
|
+
return line.ok ? ctl("prompt", "--window", created.window_id, "--pane", created.pane_id, "--text", line.value) : line;
|
|
243
|
+
}
|
|
244
|
+
function openPane(cfg, watchPath, mapFile, target) {
|
|
245
|
+
const created = createPane(target);
|
|
246
|
+
if (!created.ok) return created;
|
|
247
|
+
const pane = created.value.action;
|
|
248
|
+
const started = startWatcher(cfg, watchPath, mapFile, target, pane);
|
|
249
|
+
if (started.ok) return { ok: true, value: { ...target, opened: { window: pane.window_id, pane: pane.pane_id } } };
|
|
250
|
+
ctl("close-pane", "--window", pane.window_id, "--pane", pane.pane_id);
|
|
251
|
+
return started;
|
|
252
|
+
}
|
|
253
|
+
function locate(processId) {
|
|
254
|
+
const snapshot = ctl("snapshot");
|
|
255
|
+
if (!snapshot.ok) return snapshot;
|
|
256
|
+
for (const window of snapshot.value.windows) {
|
|
257
|
+
for (const tab of window.tabs.filter((item) => TERMINAL_TAB_KINDS.includes(item.kind))) {
|
|
258
|
+
for (const pane of tab.panes.filter((item) => item.ssh_destination === null)) {
|
|
259
|
+
const found = hosts(window.id, pane.id, processId);
|
|
260
|
+
if (!found.ok) return found;
|
|
261
|
+
if (found.value) return { ok: true, value: { window, tab, pane } };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { ok: false, error: "The live watcher could not be located in any Pebrel pane; visibility is unverified." };
|
|
266
|
+
}
|
|
267
|
+
function revealPane(target, viewer) {
|
|
268
|
+
const located = locate(viewer.pid);
|
|
269
|
+
if (!located.ok) return located;
|
|
270
|
+
const { window, tab, pane } = located.value;
|
|
271
|
+
if (zoomedOver(tab, pane.id)) {
|
|
272
|
+
const unzoomed = ctl("zoom-pane", "--window", window.id, "--pane", tab.zoomed_pane_id, "--zoomed", "false");
|
|
273
|
+
if (!unzoomed.ok) return unzoomed;
|
|
274
|
+
}
|
|
275
|
+
if (!tab.active) {
|
|
276
|
+
const focused = ctl("focus", "--window", window.id, "--pane", target.mode === "window" ? pane.id : target.pane);
|
|
277
|
+
if (!focused.ok) return focused;
|
|
278
|
+
}
|
|
279
|
+
const after = ctl("snapshot");
|
|
280
|
+
if (!after.ok) return after;
|
|
281
|
+
const shown = after.value.windows.find((item) => item.id === window.id)?.tabs.find((item) => item.panes.some((member) => member.id === pane.id));
|
|
282
|
+
return shown?.active && !zoomedOver(shown, pane.id) ? { ok: true, value: "visible" } : { ok: false, error: "The watcher is running, but its Pebrel pane is not visible in its window." };
|
|
283
|
+
}
|
|
284
|
+
return { kind: "pebrel", inspectSession: inspectSession2, openPane, revealPane };
|
|
285
|
+
}
|
|
286
|
+
function zoomedOver(tab, paneId) {
|
|
287
|
+
return tab.zoomed_pane_id !== void 0 && tab.zoomed_pane_id !== null && tab.zoomed_pane_id !== paneId;
|
|
288
|
+
}
|
|
289
|
+
function parseReply(stdout) {
|
|
290
|
+
try {
|
|
291
|
+
return JSON.parse(stdout);
|
|
292
|
+
} catch {
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// scripts/tmux-session.mjs
|
|
298
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
299
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
164
300
|
var SESSION_FORMAT = "#{session_id} #{session_attached}";
|
|
165
301
|
var TARGET_FORMAT = "#{socket_path} #{session_id} #{session_created} #{session_attached} #{pane_id} #{pane_pid}";
|
|
166
302
|
var PANE_FORMAT = "#{pane_id} #{pane_pid} #{window_id} #{window_active} #{window_zoomed_flag} #{pane_active}";
|
|
167
|
-
function createTmuxAdapter({ env = process.env, spawn =
|
|
303
|
+
function createTmuxAdapter({ env = process.env, spawn = spawnSync3, nodePath = process.execPath } = {}) {
|
|
168
304
|
const inherited = /^(.*),\d+,(\d+)$/.exec(env.TMUX ?? "");
|
|
169
305
|
const socket = env.MELLOS_MAPPING_TMUX_SOCKET || inherited?.[1];
|
|
170
306
|
function run(args, socketPath = socket) {
|
|
@@ -196,7 +332,7 @@ function createTmuxAdapter({ env = process.env, spawn = spawnSync2, nodePath = p
|
|
|
196
332
|
if (!(Number(attached) > 0)) return { ok: false, error: `tmux session ${session} has no attached client. Attach it before retrying.` };
|
|
197
333
|
const identity = [socketPath, session, created, cfg.mode, cfg.mode === "window" ? "" : pane];
|
|
198
334
|
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-${
|
|
335
|
+
const owner = selectedViewer?.owner ?? `tmux-${createHash2("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 24)}`;
|
|
200
336
|
return { ok: true, value: { backend: "tmux", socket: socketPath, session, pane, owner, owners: [owner] } };
|
|
201
337
|
}
|
|
202
338
|
function revealPane(target, viewer) {
|
|
@@ -298,14 +434,14 @@ var SPLIT_SIZE = "0.42";
|
|
|
298
434
|
function selectPaneViewer(viewers, owners) {
|
|
299
435
|
return viewers.find((viewer) => viewer.owner !== void 0 && owners.includes(viewer.owner));
|
|
300
436
|
}
|
|
301
|
-
var platformTerminal = () => process.platform === "win32" ? terminal_session_exports : createTmuxAdapter();
|
|
437
|
+
var platformTerminal = () => hostedByPebrel(process.env) ? createPebrelAdapter() : process.platform === "win32" ? terminal_session_exports : createTmuxAdapter();
|
|
302
438
|
function preparePane(cfg, store, mapFile, io = platformTerminal()) {
|
|
303
439
|
const viewers = store.readLiveViewers(mapFile, Date.now());
|
|
304
|
-
const inspected = io.kind
|
|
440
|
+
const inspected = io.kind !== "wt" ? io.inspectSession(cfg, viewers) : cfg.mode === PANE_MODE.window ? { ok: true, value: { owners: ["window"], owner: "window" } } : io.inspectSession();
|
|
305
441
|
if (!inspected.ok) return inspected;
|
|
306
442
|
const session = inspected.value;
|
|
307
443
|
const viewer = selectPaneViewer(viewers, session.owners);
|
|
308
|
-
if (io.kind
|
|
444
|
+
if (io.kind === "wt" && cfg.mode === PANE_MODE.split && !session.hwnd && (!viewer || cfg.force)) {
|
|
309
445
|
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." };
|
|
310
446
|
}
|
|
311
447
|
return { ok: true, value: {
|
|
@@ -315,7 +451,7 @@ function preparePane(cfg, store, mapFile, io = platformTerminal()) {
|
|
|
315
451
|
} };
|
|
316
452
|
}
|
|
317
453
|
function placePane(cfg, watchPath, mapFile, target, io = platformTerminal()) {
|
|
318
|
-
if (io.kind
|
|
454
|
+
if (io.kind !== "wt") return io.openPane(cfg, watchPath, mapFile, target);
|
|
319
455
|
const payload = [...paneCommand(cfg, watchPath, mapFile), "--owner", target.owner];
|
|
320
456
|
if (target.mode === PANE_MODE.window) {
|
|
321
457
|
const opened2 = io.openWt(["-w", DEDICATED_WINDOW_NAME, "nt", ...payload], "open the requested window");
|
|
@@ -451,7 +587,7 @@ async function main() {
|
|
|
451
587
|
if (!reported.ok) {
|
|
452
588
|
fail(reported.error);
|
|
453
589
|
}
|
|
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)";
|
|
590
|
+
const where = placed.value.backend === "tmux" ? placed.value.mode === PANE_MODE.window ? "in a new tmux window" : "beside this terminal (tmux split)" : placed.value.backend === "pebrel" ? placed.value.mode === PANE_MODE.window ? "in a new Pebrel window" : "beside this terminal (Pebrel split)" : placed.value.mode === PANE_MODE.window ? `in the dedicated "${DEDICATED_WINDOW_NAME}" window (${placed.value.reason})` : "beside this terminal (vertical split)";
|
|
455
591
|
console.log(`Map opened for ${project.root} ${where}.`);
|
|
456
592
|
if (!project.found) {
|
|
457
593
|
console.log(
|
package/dist/preview.mjs
CHANGED
|
File without changes
|
package/dist/server.mjs
CHANGED
|
@@ -24383,9 +24383,9 @@ function viewTool() {
|
|
|
24383
24383
|
function openTool() {
|
|
24384
24384
|
return {
|
|
24385
24385
|
title: "Open the map pane",
|
|
24386
|
-
description: 'For automatic display beside the current ChatGPT desktop conversation in Codex mode, use surface: "web-terminal", then call open_in_codex with the returned browser hostOpen object. No paste or Computer Use is needed. For the native host terminal, use surface: "codex-terminal": prepare absolute watcher commands for the current project, then ask the host to open its right terminal. This does not launch the watcher or type into that terminal. Agent exec PTYs cannot be attached using their numeric session ids. For a document panel, use surface: "markdown": generate MD + SVG files, enable automatic preview updates after successful map writes, then use the HOST file-opening tool to display the returned absolute Markdown path on the right of the current conversation. Generated does not mean visible: this server cannot open or observe the desktop side panel. For interactive maps, choose surface: "web": start or reuse a project-local web viewer and pass the returned URL to the host browser-opening tool. Markdown and terminal remain available. The default surface is "terminal", preserving the terminal workflow. Put the live map on the user\'s screen: a terminal pane beside this conversation that redraws on every write. Call it whenever a result says `pane: CLOSED` \u2014 and do NOT ask permission first, because a user who has set a mapping policy has already said they want to see the map. With a pane already open this RETARGETS it to `page` instead of opening a second one, so it is also how you show the user a particular page when they ask for one. It never closes a pane: taking the map off the screen belongs to the user (the `q` key in the pane, or typing `mmap` in a terminal). The reply says whether a pane actually reported itself in afterwards, not merely that a command was run. Automatic terminal opening supports Windows Terminal
|
|
24386
|
+
description: 'For automatic display beside the current ChatGPT desktop conversation in Codex mode, use surface: "web-terminal", then call open_in_codex with the returned browser hostOpen object. No paste or Computer Use is needed. For the native host terminal, use surface: "codex-terminal": prepare absolute watcher commands for the current project, then ask the host to open its right terminal. This does not launch the watcher or type into that terminal. Agent exec PTYs cannot be attached using their numeric session ids. For a document panel, use surface: "markdown": generate MD + SVG files, enable automatic preview updates after successful map writes, then use the HOST file-opening tool to display the returned absolute Markdown path on the right of the current conversation. Generated does not mean visible: this server cannot open or observe the desktop side panel. For interactive maps, choose surface: "web": start or reuse a project-local web viewer and pass the returned URL to the host browser-opening tool. Markdown and terminal remain available. The default surface is "terminal", preserving the terminal workflow. Put the live map on the user\'s screen: a terminal pane beside this conversation that redraws on every write. Call it whenever a result says `pane: CLOSED` \u2014 and do NOT ask permission first, because a user who has set a mapping policy has already said they want to see the map. With a pane already open this RETARGETS it to `page` instead of opening a second one, so it is also how you show the user a particular page when they ask for one. It never closes a pane: taking the map off the screen belongs to the user (the `q` key in the pane, or typing `mmap` in a terminal). The reply says whether a pane actually reported itself in afterwards, not merely that a command was run. Automatic terminal opening supports Windows Terminal, tmux on Linux/macOS, and Pebrel, which splits the conversation\'s own pane. omp (Oh My Pi) has no side panel of its own: there the routes are this same terminal pane (a Windows Terminal split, or a tmux split on Linux/macOS) and the web viewer, whose URL the session opens with its own browser tool. If opening fails, relay the reason and copyable command; retry only after the environment changes or the user asks.',
|
|
24387
24387
|
inputSchema: closed({
|
|
24388
|
-
surface: external_exports.enum(["terminal", "codex-terminal", "markdown", "web", "web-terminal"]).optional().describe("web-terminal = automatically started mmap terminal in a local browser page; codex-terminal = prepare a command for the desktop host terminal; web = local browser viewer; markdown = MD/SVG; terminal = Windows Terminal or
|
|
24388
|
+
surface: external_exports.enum(["terminal", "codex-terminal", "markdown", "web", "web-terminal"]).optional().describe("web-terminal = automatically started mmap terminal in a local browser page; codex-terminal = prepare a command for the desktop host terminal; web = local browser viewer; markdown = MD/SVG; terminal = Windows Terminal, tmux or Pebrel launcher (default)"),
|
|
24389
24389
|
page: id(
|
|
24390
24390
|
"page to show first \u2014 the page THIS effort lives on, the same slug you pass to the other tools. Omit only for the default page: without it a fresh pane opens on whichever page was written last, which after a gap is rarely the one under discussion."
|
|
24391
24391
|
).optional(),
|
|
@@ -24516,6 +24516,9 @@ function readMaps(stateFile, input) {
|
|
|
24516
24516
|
return { resource, project, page: input.page ?? null, revision, total, items: records, nextCursor: offset + limit < total ? Buffer.from(JSON.stringify({ revision, query: queryHash, offset: offset + limit })).toString("base64url") : null };
|
|
24517
24517
|
}
|
|
24518
24518
|
|
|
24519
|
+
// src/support/version.ts
|
|
24520
|
+
var VERSION = "0.28.0";
|
|
24521
|
+
|
|
24519
24522
|
// src/server/presence.ts
|
|
24520
24523
|
var DEFAULT_PAGE_NAME = "(default)";
|
|
24521
24524
|
var DEFAULT_PAGE_ABSENT = "(default: absent)";
|
|
@@ -24632,7 +24635,7 @@ Relay the launcher reason and any copyable fallback command. Do not retry mmap_o
|
|
|
24632
24635
|
}
|
|
24633
24636
|
if (paneShows(viewers, page2)) {
|
|
24634
24637
|
const visible = /^MMAP_PANE [^\r\n]*\bvisibility=visible(?:\s|$)/m.test(run.output);
|
|
24635
|
-
return `pane: running and reporting ${pageName2(page2)} \u2014 ${visible ? "the launcher verified its
|
|
24638
|
+
return `pane: running and reporting ${pageName2(page2)} \u2014 ${visible ? "the launcher verified the terminal shows its pane: its window or tab is active and no other pane is zoomed over it." : "terminal visibility is not confirmed by the process heartbeat."}
|
|
24636
24639
|
${run.output}`;
|
|
24637
24640
|
}
|
|
24638
24641
|
if (viewers.length > 0) {
|
|
@@ -24650,7 +24653,6 @@ function launcherViewerPid(run) {
|
|
|
24650
24653
|
|
|
24651
24654
|
// src/server/server.ts
|
|
24652
24655
|
var SERVER_NAME = "mellos-mapping";
|
|
24653
|
-
var SERVER_VERSION = "0.27.0";
|
|
24654
24656
|
function text(s, isError = false) {
|
|
24655
24657
|
return { content: [{ type: "text", text: s }], ...isError ? { isError: true } : {} };
|
|
24656
24658
|
}
|
|
@@ -24669,7 +24671,7 @@ function saveFailed(error2) {
|
|
|
24669
24671
|
return text(`save failed, nothing changed (retry): ${describeStoreError(error2)}`, true);
|
|
24670
24672
|
}
|
|
24671
24673
|
function buildServer(stateFile, userConfigFile, launch = launchPane) {
|
|
24672
|
-
const server = new McpServer({ name: SERVER_NAME, version:
|
|
24674
|
+
const server = new McpServer({ name: SERVER_NAME, version: VERSION }, { instructions: "Maps persist across conversations. Before mapping or resuming work, use mmap_read to discover pages, then read the relevant records and revision. A new conversation is not a new effort. Reuse verified nodes; submit only necessary changes with expectedRevision. mmap_view is a picture, not the editable data. Read mmap_setup for the user mapping policy." });
|
|
24673
24675
|
let paneOpenFailure;
|
|
24674
24676
|
const currentPaneLine = (page2) => paneLine(stateFile, page2, paneOpenFailure);
|
|
24675
24677
|
const projectConfigFile = configFilePath(stateFile);
|
|
@@ -24910,7 +24912,6 @@ if (launchedAsEntry(process.argv[1], import.meta.url)) {
|
|
|
24910
24912
|
}
|
|
24911
24913
|
export {
|
|
24912
24914
|
SERVER_NAME,
|
|
24913
|
-
SERVER_VERSION,
|
|
24914
24915
|
buildServer,
|
|
24915
24916
|
launchedAsEntry,
|
|
24916
24917
|
launcherArgs,
|
package/dist/terminal-worker.mjs
CHANGED
|
@@ -1787,6 +1787,9 @@ function paint(scene, opts) {
|
|
|
1787
1787
|
return { canvas, hits };
|
|
1788
1788
|
}
|
|
1789
1789
|
|
|
1790
|
+
// src/support/version.ts
|
|
1791
|
+
var VERSION = "0.28.0";
|
|
1792
|
+
|
|
1790
1793
|
// src/store/atomic.ts
|
|
1791
1794
|
import { mkdirSync as mkdirSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1792
1795
|
import { dirname as dirname3 } from "node:path";
|
|
@@ -2210,13 +2213,14 @@ var CLEAR_SCREEN = "\x1B[H\x1B[2J";
|
|
|
2210
2213
|
var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
|
|
2211
2214
|
var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
|
|
2212
2215
|
var RESET = "\x1B[0m";
|
|
2216
|
+
var COMMAND_DONE = "\x1B]133;D;0\x07";
|
|
2213
2217
|
function terminalRestoreSequence(mouseActive, alternateScreen = false) {
|
|
2214
2218
|
return (mouseActive ? MOUSE_OFF : "") + RESET + (alternateScreen ? LEAVE_SCREEN : "") + SHOW_CURSOR + (alternateScreen ? "" : "\n");
|
|
2215
2219
|
}
|
|
2216
2220
|
function openTerminalSession(options) {
|
|
2217
2221
|
const { interactive, write } = options;
|
|
2218
2222
|
const mouseActive = interactive && options.mouse;
|
|
2219
|
-
write((interactive ? ENTER_SCREEN : "") + HIDE_CURSOR + CLEAR_SCREEN + (mouseActive ? MOUSE_ON : ""));
|
|
2223
|
+
write((interactive ? COMMAND_DONE + ENTER_SCREEN : "") + HIDE_CURSOR + CLEAR_SCREEN + (mouseActive ? MOUSE_ON : ""));
|
|
2220
2224
|
const timer = mouseActive ? setInterval(() => write(HIDE_CURSOR + MOUSE_ON), 1e3) : void 0;
|
|
2221
2225
|
timer?.unref();
|
|
2222
2226
|
let closed = false;
|
|
@@ -2233,7 +2237,7 @@ function openTerminalSession(options) {
|
|
|
2233
2237
|
// src/watch/frame-output.ts
|
|
2234
2238
|
function frameDifference(previous, next) {
|
|
2235
2239
|
const compatible = previous?.columns === next.columns;
|
|
2236
|
-
let output2 = ""
|
|
2240
|
+
let output2 = previous?.title === next.title ? "" : `\x1B]2;${next.title}\x07`;
|
|
2237
2241
|
for (let row = 0; row < Math.max(next.rows.length, previous?.rows.length ?? 0); row++) {
|
|
2238
2242
|
const text = next.rows[row] ?? "";
|
|
2239
2243
|
if (compatible && text === previous?.rows[row]) continue;
|
|
@@ -2535,6 +2539,8 @@ function dividerRow(width, unicode, follow) {
|
|
|
2535
2539
|
let bar = (unicode ? "\u2500" : "-").repeat(width);
|
|
2536
2540
|
const gripAt = Math.max(0, Math.floor((width - grip.length) / 2));
|
|
2537
2541
|
if (width > grip.length + 2) bar = bar.slice(0, gripAt) + grip + bar.slice(gripAt + grip.length);
|
|
2542
|
+
const version = ` v${VERSION} `;
|
|
2543
|
+
if (1 + version.length < gripAt) bar = bar.slice(0, 1) + version + bar.slice(1 + version.length);
|
|
2538
2544
|
if (follow) {
|
|
2539
2545
|
const tag = unicode ? " \u21E2 follow " : " > follow ";
|
|
2540
2546
|
const at = width - tag.length - 1;
|
|
@@ -2560,6 +2566,7 @@ var CONFIRM_WINDOW_MS = 3e3;
|
|
|
2560
2566
|
var FALLBACK_COLUMNS = 100;
|
|
2561
2567
|
var FALLBACK_ROWS = 30;
|
|
2562
2568
|
var DEFAULT_PAGE_TAB_LABEL = "main";
|
|
2569
|
+
var PANE_TITLE = "mellos map";
|
|
2563
2570
|
function usableColumns(cols) {
|
|
2564
2571
|
return Math.max(1, cols - 1);
|
|
2565
2572
|
}
|
|
@@ -2762,7 +2769,7 @@ function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
|
|
|
2762
2769
|
const parts = [`${map.layers.length} layers`, `${map.nodes.length} nodes`, `${map.edges.length} edges`];
|
|
2763
2770
|
if (map.lanes.length > 0) parts.push(`${map.lanes.length} lanes`);
|
|
2764
2771
|
const lines = [
|
|
2765
|
-
{ text: fitWidth(map.title ??
|
|
2772
|
+
{ text: fitWidth(map.title ?? PANE_TITLE, width), sgr: "1" },
|
|
2766
2773
|
{ text: fitWidth(parts.join(" \xB7 "), width), sgr: "90" },
|
|
2767
2774
|
// documentation kinds document structure, not progress
|
|
2768
2775
|
{ text: fitWidth(isNeutralKind(map) ? `${map.kind} diagram` : counts, width), sgr: isNeutralKind(map) ? "90" : "" },
|
|
@@ -2821,11 +2828,12 @@ function runWatcher(cfg, io = nativeWatcherIO()) {
|
|
|
2821
2828
|
const viewWidth = () => usableColumns(io.output.columns ?? FALLBACK_COLUMNS);
|
|
2822
2829
|
const viewHeight = () => Math.max(1, (io.output.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
|
|
2823
2830
|
const dividerY = () => tabRows() + viewHeight() + 1;
|
|
2831
|
+
const pageTitleOf = (file) => mapOf(entryOf(pane, file))?.title ?? (pageIdOfFile(cfg.file, file) ?? DEFAULT_PAGE_TAB_LABEL);
|
|
2824
2832
|
const pageTabsOf = (files) => files.map((f) => {
|
|
2825
2833
|
const entry = entryOf(pane, f);
|
|
2826
2834
|
const m = mapOf(entry);
|
|
2827
2835
|
return {
|
|
2828
|
-
title:
|
|
2836
|
+
title: pageTitleOf(f),
|
|
2829
2837
|
status: m !== void 0 ? mapStatus(m) : "planned",
|
|
2830
2838
|
active: f === pane.activeFile,
|
|
2831
2839
|
fresh: entry?.fresh ?? false,
|
|
@@ -3028,7 +3036,8 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
3028
3036
|
footer
|
|
3029
3037
|
];
|
|
3030
3038
|
if (frameOutput !== void 0) {
|
|
3031
|
-
|
|
3039
|
+
const title = pane.activeFile === void 0 ? PANE_TITLE : `${PANE_TITLE} \xB7 ${pageTitleOf(pane.activeFile)}`;
|
|
3040
|
+
frameOutput.present({ columns: cols, rows, title });
|
|
3032
3041
|
} else {
|
|
3033
3042
|
const frame = HOME + rows.map((row) => row + ERASE_LINE_END).join("\n");
|
|
3034
3043
|
if (frame !== lastFrame) {
|
package/dist/watch.mjs
CHANGED
|
@@ -1787,6 +1787,9 @@ function paint(scene, opts) {
|
|
|
1787
1787
|
return { canvas, hits };
|
|
1788
1788
|
}
|
|
1789
1789
|
|
|
1790
|
+
// src/support/version.ts
|
|
1791
|
+
var VERSION = "0.28.0";
|
|
1792
|
+
|
|
1790
1793
|
// src/store/atomic.ts
|
|
1791
1794
|
import { mkdirSync as mkdirSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1792
1795
|
import { dirname as dirname3 } from "node:path";
|
|
@@ -2210,13 +2213,14 @@ var CLEAR_SCREEN = "\x1B[H\x1B[2J";
|
|
|
2210
2213
|
var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
|
|
2211
2214
|
var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
|
|
2212
2215
|
var RESET = "\x1B[0m";
|
|
2216
|
+
var COMMAND_DONE = "\x1B]133;D;0\x07";
|
|
2213
2217
|
function terminalRestoreSequence(mouseActive, alternateScreen = false) {
|
|
2214
2218
|
return (mouseActive ? MOUSE_OFF : "") + RESET + (alternateScreen ? LEAVE_SCREEN : "") + SHOW_CURSOR + (alternateScreen ? "" : "\n");
|
|
2215
2219
|
}
|
|
2216
2220
|
function openTerminalSession(options) {
|
|
2217
2221
|
const { interactive, write } = options;
|
|
2218
2222
|
const mouseActive = interactive && options.mouse;
|
|
2219
|
-
write((interactive ? ENTER_SCREEN : "") + HIDE_CURSOR + CLEAR_SCREEN + (mouseActive ? MOUSE_ON : ""));
|
|
2223
|
+
write((interactive ? COMMAND_DONE + ENTER_SCREEN : "") + HIDE_CURSOR + CLEAR_SCREEN + (mouseActive ? MOUSE_ON : ""));
|
|
2220
2224
|
const timer = mouseActive ? setInterval(() => write(HIDE_CURSOR + MOUSE_ON), 1e3) : void 0;
|
|
2221
2225
|
timer?.unref();
|
|
2222
2226
|
let closed = false;
|
|
@@ -2233,7 +2237,7 @@ function openTerminalSession(options) {
|
|
|
2233
2237
|
// src/watch/frame-output.ts
|
|
2234
2238
|
function frameDifference(previous, next) {
|
|
2235
2239
|
const compatible = previous?.columns === next.columns;
|
|
2236
|
-
let output = ""
|
|
2240
|
+
let output = previous?.title === next.title ? "" : `\x1B]2;${next.title}\x07`;
|
|
2237
2241
|
for (let row = 0; row < Math.max(next.rows.length, previous?.rows.length ?? 0); row++) {
|
|
2238
2242
|
const text = next.rows[row] ?? "";
|
|
2239
2243
|
if (compatible && text === previous?.rows[row]) continue;
|
|
@@ -2536,6 +2540,8 @@ function dividerRow(width, unicode, follow) {
|
|
|
2536
2540
|
let bar = (unicode ? "\u2500" : "-").repeat(width);
|
|
2537
2541
|
const gripAt = Math.max(0, Math.floor((width - grip.length) / 2));
|
|
2538
2542
|
if (width > grip.length + 2) bar = bar.slice(0, gripAt) + grip + bar.slice(gripAt + grip.length);
|
|
2543
|
+
const version = ` v${VERSION} `;
|
|
2544
|
+
if (1 + version.length < gripAt) bar = bar.slice(0, 1) + version + bar.slice(1 + version.length);
|
|
2539
2545
|
if (follow) {
|
|
2540
2546
|
const tag = unicode ? " \u21E2 follow " : " > follow ";
|
|
2541
2547
|
const at = width - tag.length - 1;
|
|
@@ -2561,6 +2567,7 @@ var CONFIRM_WINDOW_MS = 3e3;
|
|
|
2561
2567
|
var FALLBACK_COLUMNS = 100;
|
|
2562
2568
|
var FALLBACK_ROWS = 30;
|
|
2563
2569
|
var DEFAULT_PAGE_TAB_LABEL = "main";
|
|
2570
|
+
var PANE_TITLE = "mellos map";
|
|
2564
2571
|
function usableColumns(cols) {
|
|
2565
2572
|
return Math.max(1, cols - 1);
|
|
2566
2573
|
}
|
|
@@ -2763,7 +2770,7 @@ function mapPanel(map, unicode, width, rows = PANEL_CONTENT_ROWS) {
|
|
|
2763
2770
|
const parts = [`${map.layers.length} layers`, `${map.nodes.length} nodes`, `${map.edges.length} edges`];
|
|
2764
2771
|
if (map.lanes.length > 0) parts.push(`${map.lanes.length} lanes`);
|
|
2765
2772
|
const lines = [
|
|
2766
|
-
{ text: fitWidth(map.title ??
|
|
2773
|
+
{ text: fitWidth(map.title ?? PANE_TITLE, width), sgr: "1" },
|
|
2767
2774
|
{ text: fitWidth(parts.join(" \xB7 "), width), sgr: "90" },
|
|
2768
2775
|
// documentation kinds document structure, not progress
|
|
2769
2776
|
{ text: fitWidth(isNeutralKind(map) ? `${map.kind} diagram` : counts, width), sgr: isNeutralKind(map) ? "90" : "" },
|
|
@@ -2822,11 +2829,12 @@ function runWatcher(cfg, io2 = nativeWatcherIO()) {
|
|
|
2822
2829
|
const viewWidth = () => usableColumns(io2.output.columns ?? FALLBACK_COLUMNS);
|
|
2823
2830
|
const viewHeight = () => Math.max(1, (io2.output.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
|
|
2824
2831
|
const dividerY = () => tabRows() + viewHeight() + 1;
|
|
2832
|
+
const pageTitleOf = (file) => mapOf(entryOf(pane, file))?.title ?? (pageIdOfFile(cfg.file, file) ?? DEFAULT_PAGE_TAB_LABEL);
|
|
2825
2833
|
const pageTabsOf = (files) => files.map((f) => {
|
|
2826
2834
|
const entry = entryOf(pane, f);
|
|
2827
2835
|
const m = mapOf(entry);
|
|
2828
2836
|
return {
|
|
2829
|
-
title:
|
|
2837
|
+
title: pageTitleOf(f),
|
|
2830
2838
|
status: m !== void 0 ? mapStatus(m) : "planned",
|
|
2831
2839
|
active: f === pane.activeFile,
|
|
2832
2840
|
fresh: entry?.fresh ?? false,
|
|
@@ -3029,7 +3037,8 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
3029
3037
|
footer
|
|
3030
3038
|
];
|
|
3031
3039
|
if (frameOutput !== void 0) {
|
|
3032
|
-
|
|
3040
|
+
const title = pane.activeFile === void 0 ? PANE_TITLE : `${PANE_TITLE} \xB7 ${pageTitleOf(pane.activeFile)}`;
|
|
3041
|
+
frameOutput.present({ columns: cols, rows, title });
|
|
3033
3042
|
} else {
|
|
3034
3043
|
const frame = HOME + rows.map((row) => row + ERASE_LINE_END).join("\n");
|
|
3035
3044
|
if (frame !== lastFrame) {
|
package/dist/web.mjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mellos-mapping",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"scripts/mmap.mjs",
|
|
97
97
|
"scripts/open-pane.mjs",
|
|
98
98
|
"scripts/pane-core.mjs",
|
|
99
|
+
"scripts/pebrel-session.mjs",
|
|
99
100
|
"scripts/terminal-session.mjs",
|
|
100
101
|
"scripts/tmux-session.mjs",
|
|
101
102
|
"scripts/watcher-command.mjs"
|
|
File without changes
|
|
File without changes
|
package/scripts/mmap.mjs
CHANGED
|
@@ -208,6 +208,8 @@ async function main() {
|
|
|
208
208
|
const where =
|
|
209
209
|
placed.value.backend === 'tmux'
|
|
210
210
|
? (placed.value.mode === PANE_MODE.window ? 'in a new tmux window' : 'beside this terminal (tmux split)')
|
|
211
|
+
: placed.value.backend === 'pebrel'
|
|
212
|
+
? (placed.value.mode === PANE_MODE.window ? 'in a new Pebrel window' : 'beside this terminal (Pebrel split)')
|
|
211
213
|
: placed.value.mode === PANE_MODE.window
|
|
212
214
|
? `in the dedicated "${DEDICATED_WINDOW_NAME}" window (${placed.value.reason})`
|
|
213
215
|
: 'beside this terminal (vertical split)';
|
package/scripts/open-pane.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Open the live map pane in the owning Windows Terminal or
|
|
3
|
+
* Open the live map pane in the owning Windows Terminal, tmux or Pebrel session — the AGENT's
|
|
4
4
|
* launcher, addressed by `/mmap` and by the skill.
|
|
5
5
|
*
|
|
6
6
|
* node scripts/open-pane.mjs <project-dir> [--page <slug>] [--window] [--force]
|
|
@@ -151,6 +151,10 @@ async function main() {
|
|
|
151
151
|
console.log(`MMAP_PANE mode=${placed.value.mode} pid=${reported.value.pid} backend=tmux session=${placed.value.session} visibility=${visible.value}`);
|
|
152
152
|
console.log(placed.value.mode === PANE_MODE.window
|
|
153
153
|
? 'Map opened in a new tmux window.' : 'Map opened beside this conversation (tmux split).');
|
|
154
|
+
} else if (placed.value.backend === 'pebrel') {
|
|
155
|
+
console.log(`MMAP_PANE mode=${placed.value.mode} pid=${reported.value.pid} backend=pebrel window=${placed.value.opened.window} pane=${placed.value.opened.pane} visibility=${visible.value}`);
|
|
156
|
+
console.log(placed.value.mode === PANE_MODE.window
|
|
157
|
+
? 'Map opened in a new Pebrel window.' : 'Map opened beside this conversation (Pebrel split).');
|
|
154
158
|
} else if (placed.value.mode === PANE_MODE.window) {
|
|
155
159
|
console.log(`MMAP_PANE mode=window pid=${reported.value.pid} name=${DEDICATED_WINDOW_NAME} reason=${placed.value.reason}`);
|
|
156
160
|
console.log(`Map opened in the dedicated "${DEDICATED_WINDOW_NAME}" window.`);
|
package/scripts/pane-core.mjs
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* Pure helpers are exported for the spec; nothing here runs on import.
|
|
31
31
|
*/
|
|
32
32
|
import * as terminal from './terminal-session.mjs';
|
|
33
|
+
import { createPebrelAdapter, hostedByPebrel } from './pebrel-session.mjs';
|
|
33
34
|
import { createTmuxAdapter } from './tmux-session.mjs';
|
|
34
35
|
import { watcherArgs } from './watcher-command.mjs';
|
|
35
36
|
import { existsSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -199,18 +200,23 @@ export function selectPaneViewer(viewers, owners) {
|
|
|
199
200
|
return viewers.find((viewer) => viewer.owner !== undefined && owners.includes(viewer.owner));
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Windows Terminal is composed here from its probe and `wt`; every other
|
|
205
|
+
* backend is an adapter that places and reveals its own panes.
|
|
206
|
+
*/
|
|
207
|
+
const platformTerminal = () => hostedByPebrel(process.env) ? createPebrelAdapter()
|
|
208
|
+
: process.platform === 'win32' ? terminal : createTmuxAdapter();
|
|
203
209
|
|
|
204
210
|
/** Resolve identity before deciding whether to reuse, open or toggle. */
|
|
205
211
|
export function preparePane(cfg, store, mapFile, io = platformTerminal()) {
|
|
206
212
|
const viewers = store.readLiveViewers(mapFile, Date.now());
|
|
207
|
-
const inspected = io.kind
|
|
213
|
+
const inspected = io.kind !== 'wt' ? io.inspectSession(cfg, viewers) : cfg.mode === PANE_MODE.window
|
|
208
214
|
? { ok: true, value: { owners: ['window'], owner: 'window' } }
|
|
209
215
|
: io.inspectSession();
|
|
210
216
|
if (!inspected.ok) return inspected;
|
|
211
217
|
const session = inspected.value;
|
|
212
218
|
const viewer = selectPaneViewer(viewers, session.owners);
|
|
213
|
-
if (io.kind
|
|
219
|
+
if (io.kind === 'wt' && cfg.mode === PANE_MODE.split && !session.hwnd && (!viewer || cfg.force)) {
|
|
214
220
|
return { ok: false, error: 'Could not identify this conversation’s active Windows Terminal pane. Activate its PowerShell tab and retry; no separate window was opened. Use --window only if you want a separate window.' };
|
|
215
221
|
}
|
|
216
222
|
return { ok: true, value: {
|
|
@@ -222,12 +228,12 @@ export function preparePane(cfg, store, mapFile, io = platformTerminal()) {
|
|
|
222
228
|
|
|
223
229
|
/** A process heartbeat alone cannot establish that its pane is on screen. */
|
|
224
230
|
export function revealPane(target, viewer, io = platformTerminal()) {
|
|
225
|
-
return io.kind
|
|
231
|
+
return io.kind !== 'wt' ? io.revealPane(target, viewer) : { ok: true, value: 'unverified' };
|
|
226
232
|
}
|
|
227
233
|
|
|
228
234
|
/** Compose a verified target with the watcher payload. Never changes the requested mode. */
|
|
229
235
|
export function placePane(cfg, watchPath, mapFile, target, io = platformTerminal()) {
|
|
230
|
-
if (io.kind
|
|
236
|
+
if (io.kind !== 'wt') return io.openPane(cfg, watchPath, mapFile, target);
|
|
231
237
|
const payload = [...paneCommand(cfg, watchPath, mapFile), '--owner', target.owner];
|
|
232
238
|
if (target.mode === PANE_MODE.window) {
|
|
233
239
|
const opened = io.openWt(['-w', DEDICATED_WINDOW_NAME, 'nt', ...payload], 'open the requested window');
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/** Pebrel adapter: split the conversation's own pane through Pebrel's runtime CLI. */
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { posixQuote, watcherArgs } from './watcher-command.mjs';
|
|
5
|
+
|
|
6
|
+
const CTL_TIMEOUT_MS = 10_000;
|
|
7
|
+
const SPLIT_RATIO = '0.42';
|
|
8
|
+
const TERMINAL_TAB_KINDS = ['shell', 'profile'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Pebrel exports PEBREL_CLI and PEBREL_PANE_ID to every local pane. A tmux
|
|
12
|
+
* server inherits them but is not a descendant of that pane, so a conversation
|
|
13
|
+
* inside tmux belongs to tmux.
|
|
14
|
+
*/
|
|
15
|
+
export function hostedByPebrel(env) {
|
|
16
|
+
return Boolean(env.PEBREL_CLI && env.PEBREL_PANE_ID) && !env.TMUX;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pebrel splits always start the default shell, so the watcher is typed as one
|
|
21
|
+
* line in that shell's own syntax. The line ends the shell with the watcher,
|
|
22
|
+
* which closes the pane the way a wt or tmux pane closes with its command.
|
|
23
|
+
*/
|
|
24
|
+
export function shellCommandLine(executable, argv) {
|
|
25
|
+
const shell = executable.toLowerCase().replace(/\.exe$/, '');
|
|
26
|
+
switch (shell) {
|
|
27
|
+
case 'pwsh':
|
|
28
|
+
case 'powershell':
|
|
29
|
+
return { ok: true, value: `& ${argv.map(powershellQuote).join(' ')}; exit` };
|
|
30
|
+
case 'cmd':
|
|
31
|
+
return argv.some((arg) => /["%]/.test(arg))
|
|
32
|
+
? { ok: false, error: 'cmd cannot quote a path containing " or %; no pane was kept.' }
|
|
33
|
+
: { ok: true, value: `${argv.map((arg) => `"${arg}"`).join(' ')} & exit` };
|
|
34
|
+
case 'sh':
|
|
35
|
+
case 'bash':
|
|
36
|
+
case 'zsh':
|
|
37
|
+
case 'dash':
|
|
38
|
+
case 'ksh':
|
|
39
|
+
return { ok: true, value: `exec ${argv.map(posixQuote).join(' ')}` };
|
|
40
|
+
default:
|
|
41
|
+
return { ok: false, error: `The new Pebrel pane runs ${executable}; the map starts only from PowerShell, cmd or a POSIX shell. No pane was kept.` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** PowerShell ends a single-quoted string at any of the four single quotes. */
|
|
46
|
+
function powershellQuote(arg) {
|
|
47
|
+
return `'${arg.replace(/['‘’‚‛]/g, '$&$&')}'`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createPebrelAdapter({ env = process.env, spawn = spawnSync, nodePath = process.execPath, pid = process.pid } = {}) {
|
|
51
|
+
function ctl(command, ...args) {
|
|
52
|
+
const result = spawn(env.PEBREL_CLI, ['ctl', command, ...args.map(String)], {
|
|
53
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: CTL_TIMEOUT_MS, windowsHide: true,
|
|
54
|
+
});
|
|
55
|
+
const reply = parseReply(result.stdout);
|
|
56
|
+
return reply?.ok === true
|
|
57
|
+
? { ok: true, value: reply.result }
|
|
58
|
+
: { ok: false, error: `pebrel ctl ${command} failed: ${reply?.error?.message ?? result.error?.message ?? (result.stderr?.trim() || `exit ${result.status}`)}` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hosts(window, pane, processId) {
|
|
62
|
+
const tree = ctl('procs', '--window', window, '--pane', pane);
|
|
63
|
+
return tree.ok ? { ok: true, value: tree.value.processes.some((item) => item.pid === processId) } : tree;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function inspectSession(cfg) {
|
|
67
|
+
const snapshot = ctl('snapshot');
|
|
68
|
+
if (!snapshot.ok) return snapshot;
|
|
69
|
+
const pane = Number(env.PEBREL_PANE_ID);
|
|
70
|
+
// Pane ids are unique only within a window; the process tree says which window is ours.
|
|
71
|
+
for (const window of snapshot.value.windows) {
|
|
72
|
+
if (!window.tabs.some((tab) => tab.panes.some((item) => item.id === pane))) continue;
|
|
73
|
+
const ours = hosts(window.id, pane, pid);
|
|
74
|
+
if (!ours.ok) return ours;
|
|
75
|
+
if (!ours.value) continue;
|
|
76
|
+
const identity = [snapshot.value.process_id, cfg.mode, cfg.mode === 'window' ? '' : `${window.id}:${pane}`];
|
|
77
|
+
const owner = `pebrel-${createHash('sha256').update(JSON.stringify(identity)).digest('hex').slice(0, 24)}`;
|
|
78
|
+
return { ok: true, value: { backend: 'pebrel', window: window.id, pane, owner, owners: [owner] } };
|
|
79
|
+
}
|
|
80
|
+
return { ok: false, error: `This process is not running inside Pebrel pane ${env.PEBREL_PANE_ID}; no pane was opened.` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createPane(target) {
|
|
84
|
+
if (target.mode === 'window') return ctl('new-window');
|
|
85
|
+
const split = ctl('split', '--window', target.window, '--pane', target.pane, '--direction', 'right');
|
|
86
|
+
if (!split.ok) return split;
|
|
87
|
+
const created = split.value.action;
|
|
88
|
+
const focused = ctl('focus', '--window', target.window, '--pane', target.pane);
|
|
89
|
+
const resized = focused.ok
|
|
90
|
+
? ctl('resize-pane', '--window', created.window_id, '--pane', created.pane_id, '--ratio', SPLIT_RATIO)
|
|
91
|
+
: focused;
|
|
92
|
+
if (resized.ok) return split;
|
|
93
|
+
ctl('close-pane', '--window', created.window_id, '--pane', created.pane_id);
|
|
94
|
+
return resized;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function startWatcher(cfg, watchPath, mapFile, target, created) {
|
|
98
|
+
const tree = ctl('procs', '--window', created.window_id, '--pane', created.pane_id);
|
|
99
|
+
if (!tree.ok) return tree;
|
|
100
|
+
const shell = tree.value.processes.find((item) => item.depth === 0)?.executable ?? 'no shell';
|
|
101
|
+
const line = shellCommandLine(shell, [nodePath, ...watcherArgs(cfg, watchPath, mapFile), '--owner', target.owner]);
|
|
102
|
+
return line.ok ? ctl('prompt', '--window', created.window_id, '--pane', created.pane_id, '--text', line.value) : line;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function openPane(cfg, watchPath, mapFile, target) {
|
|
106
|
+
const created = createPane(target);
|
|
107
|
+
if (!created.ok) return created;
|
|
108
|
+
const pane = created.value.action;
|
|
109
|
+
const started = startWatcher(cfg, watchPath, mapFile, target, pane);
|
|
110
|
+
if (started.ok) return { ok: true, value: { ...target, opened: { window: pane.window_id, pane: pane.pane_id } } };
|
|
111
|
+
ctl('close-pane', '--window', pane.window_id, '--pane', pane.pane_id);
|
|
112
|
+
return started;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function locate(processId) {
|
|
116
|
+
const snapshot = ctl('snapshot');
|
|
117
|
+
if (!snapshot.ok) return snapshot;
|
|
118
|
+
for (const window of snapshot.value.windows) {
|
|
119
|
+
for (const tab of window.tabs.filter((item) => TERMINAL_TAB_KINDS.includes(item.kind))) {
|
|
120
|
+
for (const pane of tab.panes.filter((item) => item.ssh_destination === null)) {
|
|
121
|
+
const found = hosts(window.id, pane.id, processId);
|
|
122
|
+
if (!found.ok) return found;
|
|
123
|
+
if (found.value) return { ok: true, value: { window, tab, pane } };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { ok: false, error: 'The live watcher could not be located in any Pebrel pane; visibility is unverified.' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function revealPane(target, viewer) {
|
|
131
|
+
const located = locate(viewer.pid);
|
|
132
|
+
if (!located.ok) return located;
|
|
133
|
+
const { window, tab, pane } = located.value;
|
|
134
|
+
if (zoomedOver(tab, pane.id)) {
|
|
135
|
+
const unzoomed = ctl('zoom-pane', '--window', window.id, '--pane', tab.zoomed_pane_id, '--zoomed', 'false');
|
|
136
|
+
if (!unzoomed.ok) return unzoomed;
|
|
137
|
+
}
|
|
138
|
+
if (!tab.active) {
|
|
139
|
+
// A split shares the conversation's tab, so focusing the conversation reveals it without taking input.
|
|
140
|
+
const focused = ctl('focus', '--window', window.id, '--pane', target.mode === 'window' ? pane.id : target.pane);
|
|
141
|
+
if (!focused.ok) return focused;
|
|
142
|
+
}
|
|
143
|
+
const after = ctl('snapshot');
|
|
144
|
+
if (!after.ok) return after;
|
|
145
|
+
const shown = after.value.windows.find((item) => item.id === window.id)
|
|
146
|
+
?.tabs.find((item) => item.panes.some((member) => member.id === pane.id));
|
|
147
|
+
return shown?.active && !zoomedOver(shown, pane.id)
|
|
148
|
+
? { ok: true, value: 'visible' }
|
|
149
|
+
: { ok: false, error: 'The watcher is running, but its Pebrel pane is not visible in its window.' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { kind: 'pebrel', inspectSession, openPane, revealPane };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Pebrel 2.2 omits zoomed_pane_id from a tab that has no zoom. */
|
|
156
|
+
function zoomedOver(tab, paneId) {
|
|
157
|
+
return tab.zoomed_pane_id !== undefined && tab.zoomed_pane_id !== null && tab.zoomed_pane_id !== paneId;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseReply(stdout) {
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(stdout);
|
|
163
|
+
} catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Windows Terminal adapter. No project paths, reuse policy or persistent state. */
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
|
|
4
|
+
export const kind = 'wt';
|
|
5
|
+
|
|
4
6
|
const NATIVE = String.raw`
|
|
5
7
|
$ErrorActionPreference = 'Stop'
|
|
6
8
|
$ProgressPreference = 'SilentlyContinue'
|
|
@@ -5,10 +5,13 @@ export function watcherArgs(cfg, watchPath, mapFile) {
|
|
|
5
5
|
return args;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
export function posixQuote(arg) {
|
|
9
|
+
return "'" + arg.replaceAll("'", "'\"'\"'") + "'";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The human fallback; adapters that can pass argv never quote. */
|
|
9
13
|
export function manualWatcherCommand(cfg, watchPath, mapFile, nodePath = process.execPath) {
|
|
10
|
-
return [nodePath, ...watcherArgs(cfg, watchPath, mapFile)]
|
|
11
|
-
.map(arg => "'" + arg.replaceAll("'", "'\"'\"'") + "'").join(' ');
|
|
14
|
+
return [nodePath, ...watcherArgs(cfg, watchPath, mapFile)].map(posixQuote).join(' ');
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export function paneFailureMessage(error, cfg, watchPath, mapFile, platform = process.platform) {
|
|
@@ -204,6 +204,9 @@ confirmed open desktop panel. See `docs/codex.md` for transport and lifecycle de
|
|
|
204
204
|
attached sessions require `MELLOS_MAPPING_TMUX_TARGET`; custom sockets use
|
|
205
205
|
`MELLOS_MAPPING_TMUX_SOCKET` in the MCP server environment. `window: true`
|
|
206
206
|
requests a new tmux window. Do not guess between attached sessions.
|
|
207
|
+
Inside Pebrel (`PEBREL_CLI` and `PEBREL_PANE_ID` set, not inside tmux) it
|
|
208
|
+
splits the conversation's own Pebrel pane; `window: true` opens a new
|
|
209
|
+
Pebrel window.
|
|
207
210
|
Under omp (Oh My Pi) nothing about this changes: the same tool runs the same
|
|
208
211
|
launcher, splitting the Windows Terminal window hosting the omp session
|
|
209
212
|
(tmux split on Linux/macOS) and reusing the pane already bound to it. omp has
|