tmux-ide 2.6.0 → 2.6.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/bin/cli.js +13 -5
- package/bin/cli.ts +1 -1
- package/bunfig.toml +4 -0
- package/package.json +11 -3
- package/packages/contracts/package.json +22 -0
- package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
- package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
- package/packages/contracts/src/actions-contract.ts +310 -0
- package/packages/contracts/src/actions-errors.ts +41 -0
- package/packages/contracts/src/domain.ts +36 -0
- package/packages/contracts/src/ide-config.ts +170 -0
- package/packages/contracts/src/index.ts +24 -0
- package/packages/contracts/src/lib-internal/auth.ts +13 -0
- package/packages/contracts/src/lib-internal/hq.ts +38 -0
- package/packages/contracts/src/terminals.ts +116 -0
- package/packages/contracts/src/tmux.ts +60 -0
- package/packages/contracts/src/workspace.ts +67 -0
- package/packages/daemon/src/agent-explain.ts +270 -0
- package/packages/daemon/src/attach.ts +20 -0
- package/packages/daemon/src/bin.ts +4 -0
- package/packages/daemon/src/canonical.ts +7 -0
- package/packages/daemon/src/cli.ts +499 -0
- package/packages/daemon/src/command-center/actions/contract.ts +2 -0
- package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
- package/packages/daemon/src/command-center/actions/errors.ts +105 -0
- package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
- package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
- package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
- package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
- package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
- package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
- package/packages/daemon/src/command-center/actions/registry.ts +149 -0
- package/packages/daemon/src/command-center/discovery.ts +96 -0
- package/packages/daemon/src/command-center/index.ts +31 -0
- package/packages/daemon/src/command-center/schemas.ts +85 -0
- package/packages/daemon/src/command-center/server.ts +1260 -0
- package/packages/daemon/src/command-center/ws-events.ts +316 -0
- package/packages/daemon/src/config.ts +549 -0
- package/packages/daemon/src/detect.ts +248 -0
- package/packages/daemon/src/doctor.ts +242 -0
- package/packages/daemon/src/embed.ts +5 -0
- package/packages/daemon/src/index.ts +12 -0
- package/packages/daemon/src/init.ts +211 -0
- package/packages/daemon/src/inspect.ts +178 -0
- package/packages/daemon/src/js-yaml.d.ts +10 -0
- package/packages/daemon/src/launch.ts +349 -0
- package/packages/daemon/src/lib/active-projects.ts +49 -0
- package/packages/daemon/src/lib/agent-discovery.ts +121 -0
- package/packages/daemon/src/lib/app-config.ts +336 -0
- package/packages/daemon/src/lib/app-settings.ts +53 -0
- package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
- package/packages/daemon/src/lib/auth/middleware.ts +56 -0
- package/packages/daemon/src/lib/auth/types.ts +2 -0
- package/packages/daemon/src/lib/auth-token.ts +5 -0
- package/packages/daemon/src/lib/authorship.ts +280 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
- package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
- package/packages/daemon/src/lib/daemon-embed.ts +782 -0
- package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
- package/packages/daemon/src/lib/daemon.ts +79 -0
- package/packages/daemon/src/lib/dot-path.ts +17 -0
- package/packages/daemon/src/lib/errors.ts +67 -0
- package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
- package/packages/daemon/src/lib/launch-plan.ts +90 -0
- package/packages/daemon/src/lib/log.ts +134 -0
- package/packages/daemon/src/lib/output.ts +76 -0
- package/packages/daemon/src/lib/project-init-runner.ts +150 -0
- package/packages/daemon/src/lib/project-inspect.ts +80 -0
- package/packages/daemon/src/lib/project-onboard.ts +149 -0
- package/packages/daemon/src/lib/project-probe.ts +92 -0
- package/packages/daemon/src/lib/project-registry.ts +296 -0
- package/packages/daemon/src/lib/session-monitor.ts +122 -0
- package/packages/daemon/src/lib/session-options.ts +100 -0
- package/packages/daemon/src/lib/shell.ts +8 -0
- package/packages/daemon/src/lib/sizes.ts +36 -0
- package/packages/daemon/src/lib/skill-sync.ts +155 -0
- package/packages/daemon/src/lib/slugify.ts +10 -0
- package/packages/daemon/src/lib/terminals-store.ts +125 -0
- package/packages/daemon/src/lib/update-check.ts +298 -0
- package/packages/daemon/src/lib/update.ts +158 -0
- package/packages/daemon/src/lib/workspace-registry.ts +229 -0
- package/packages/daemon/src/lib/worktree.ts +289 -0
- package/packages/daemon/src/lib/yaml-io.ts +27 -0
- package/packages/daemon/src/ls.ts +40 -0
- package/packages/daemon/src/restart.ts +24 -0
- package/packages/daemon/src/restore.ts +514 -0
- package/packages/daemon/src/schemas/domain.ts +2 -0
- package/packages/daemon/src/schemas/filesystem.ts +34 -0
- package/packages/daemon/src/schemas/ide-config.ts +2 -0
- package/packages/daemon/src/schemas/index.ts +59 -0
- package/packages/daemon/src/schemas/inspect.ts +67 -0
- package/packages/daemon/src/schemas/registry.ts +55 -0
- package/packages/daemon/src/schemas/ws-events.ts +135 -0
- package/packages/daemon/src/send.ts +171 -0
- package/packages/daemon/src/server/README.md +15 -0
- package/packages/daemon/src/server/index.ts +74 -0
- package/packages/daemon/src/server/pty-bridge.ts +532 -0
- package/packages/daemon/src/server/standalone.ts +18 -0
- package/packages/daemon/src/server/ws-route.ts +483 -0
- package/packages/daemon/src/status.ts +59 -0
- package/packages/daemon/src/stop.ts +28 -0
- package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
- package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
- package/packages/daemon/src/terminal/README.md +92 -0
- package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
- package/packages/daemon/src/tui/chrome/chip.ts +30 -0
- package/packages/daemon/src/tui/chrome/events.ts +119 -0
- package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
- package/packages/daemon/src/tui/chrome/menu.ts +289 -0
- package/packages/daemon/src/tui/chrome/notify.ts +195 -0
- package/packages/daemon/src/tui/chrome/panels.ts +111 -0
- package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
- package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
- package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
- package/packages/daemon/src/tui/chrome/updater.ts +428 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +113 -0
- package/packages/daemon/src/tui/detect/classify.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +192 -0
- package/packages/daemon/src/tui/detect/manifest.ts +184 -0
- package/packages/daemon/src/tui/detect/manifests.ts +226 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +197 -0
- package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
- package/packages/daemon/src/tui/integrations/claude.ts +176 -0
- package/packages/daemon/src/tui/integrations/offer.ts +145 -0
- package/packages/daemon/src/tui/main.ts +70 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +143 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +81 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +166 -0
- package/packages/daemon/src/tui/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +11 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
- package/packages/daemon/src/tui/team/home.ts +170 -0
- package/packages/daemon/src/tui/team/index.tsx +1521 -0
- package/packages/daemon/src/tui/team/input.ts +34 -0
- package/packages/daemon/src/tui/team/keymap.ts +127 -0
- package/packages/daemon/src/tui/team/mouse.ts +29 -0
- package/packages/daemon/src/tui/team/nav.ts +31 -0
- package/packages/daemon/src/tui/team/preview.ts +34 -0
- package/packages/daemon/src/tui/team/projects.ts +191 -0
- package/packages/daemon/src/tui/team/report.ts +73 -0
- package/packages/daemon/src/tui/team/sessions.ts +344 -0
- package/packages/daemon/src/tui/team/tree.ts +62 -0
- package/packages/daemon/src/types.ts +13 -0
- package/packages/daemon/src/ui/index.ts +32 -0
- package/packages/daemon/src/ui/terminal/index.ts +9 -0
- package/packages/daemon/src/ui/types.ts +91 -0
- package/packages/daemon/src/ui/web/base.css +80 -0
- package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
- package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
- package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
- package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
- package/packages/daemon/src/ui/web/hooks.ts +106 -0
- package/packages/daemon/src/ui/web/index.ts +27 -0
- package/packages/daemon/src/ui/web/render.ts +77 -0
- package/packages/daemon/src/ui/web/utils/color.ts +27 -0
- package/packages/daemon/src/validate.ts +217 -0
- package/packages/daemon/src/widgets/changes/README.md +3 -0
- package/packages/daemon/src/widgets/changes/index.tsx +691 -0
- package/packages/daemon/src/widgets/config/README.md +3 -0
- package/packages/daemon/src/widgets/config/index.tsx +481 -0
- package/packages/daemon/src/widgets/explorer/README.md +3 -0
- package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
- package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
- package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
- package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
- package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
- package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
- package/packages/daemon/src/widgets/lib/files.ts +88 -0
- package/packages/daemon/src/widgets/lib/git.ts +88 -0
- package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
- package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
- package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
- package/packages/daemon/src/widgets/lib/theme.ts +194 -0
- package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
- package/packages/daemon/src/widgets/preview/README.md +3 -0
- package/packages/daemon/src/widgets/preview/index.tsx +416 -0
- package/packages/daemon/src/widgets/resolve.ts +121 -0
- package/packages/daemon/src/widgets/setup/README.md +3 -0
- package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
- package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
- package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
- package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
- package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
- package/packages/daemon/src/widgets/setup/index.tsx +341 -0
- package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
- package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
- package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
- package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
- package/packages/tmux-bridge/package.json +22 -0
- package/packages/tmux-bridge/src/errors.ts +28 -0
- package/packages/tmux-bridge/src/index.ts +31 -0
- package/packages/tmux-bridge/src/monitor.ts +77 -0
- package/packages/tmux-bridge/src/panes.ts +136 -0
- package/packages/tmux-bridge/src/runner.test.ts +501 -0
- package/packages/tmux-bridge/src/runner.ts +91 -0
- package/packages/tmux-bridge/src/sessions.ts +126 -0
- package/packages/tmux-bridge/src/targeting.test.ts +107 -0
- package/packages/tmux-bridge/src/targeting.ts +90 -0
- package/scripts/postinstall.js +26 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orphaned legacy crash-recovery watchdog for the old per-session daemon.
|
|
3
|
+
*
|
|
4
|
+
* The canonical daemon is now owned by Electron or `tmux-ide --headless`.
|
|
5
|
+
* No supported code path should spawn this file.
|
|
6
|
+
*
|
|
7
|
+
* Minimal crash-recovery watchdog for the tmux-ide daemon.
|
|
8
|
+
* Respawns daemon.ts on unexpected exits with exponential backoff.
|
|
9
|
+
* Gives up after 5 crashes within 60 seconds.
|
|
10
|
+
*
|
|
11
|
+
* Entry: bun src/lib/daemon-watchdog.ts <session> [port]
|
|
12
|
+
*
|
|
13
|
+
* ZERO imports from business logic — this file must never crash
|
|
14
|
+
* due to broken application code.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
18
|
+
import { resolve, dirname } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { appendFileSync, mkdirSync, existsSync } from "node:fs";
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const sessionArg = process.argv[2];
|
|
24
|
+
const port = process.argv[3] ?? "0";
|
|
25
|
+
|
|
26
|
+
if (!sessionArg) {
|
|
27
|
+
console.error("Usage: daemon-watchdog.ts <session> [port]");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const session: string = sessionArg;
|
|
32
|
+
|
|
33
|
+
const daemonScript = resolve(__dirname, "daemon.ts");
|
|
34
|
+
|
|
35
|
+
let backoffMs = 1000;
|
|
36
|
+
let crashCount = 0;
|
|
37
|
+
let lastCrashTime = 0;
|
|
38
|
+
let child: ChildProcess | null = null;
|
|
39
|
+
|
|
40
|
+
function logError(msg: string): void {
|
|
41
|
+
try {
|
|
42
|
+
const tasksDir = resolve(process.cwd(), ".tasks");
|
|
43
|
+
if (!existsSync(tasksDir)) mkdirSync(tasksDir, { recursive: true });
|
|
44
|
+
const entry = JSON.stringify({
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
type: "error",
|
|
47
|
+
source: "daemon-watchdog",
|
|
48
|
+
message: msg,
|
|
49
|
+
});
|
|
50
|
+
appendFileSync(resolve(tasksDir, "events.log"), entry + "\n");
|
|
51
|
+
} catch {
|
|
52
|
+
// If even logging fails, silently continue
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function spawnDaemon(): void {
|
|
57
|
+
// The daemon MUST run under node/tsx, never bun: node-pty's `onData`
|
|
58
|
+
// callback never fires under bun (PTY spawns + exits silently). See T085
|
|
59
|
+
// for the diagnostic story and T087 for the PtyAdapter abstraction this
|
|
60
|
+
// pins down. Prod swaps this for `node dist/lib/daemon.js`.
|
|
61
|
+
child = spawn("tsx", [daemonScript, session, port], {
|
|
62
|
+
cwd: process.cwd(),
|
|
63
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Pipe child stdout/stderr to our own so logs are visible in the detached process
|
|
67
|
+
child.stdout?.pipe(process.stdout);
|
|
68
|
+
child.stderr?.pipe(process.stderr);
|
|
69
|
+
|
|
70
|
+
child.on("exit", (code) => {
|
|
71
|
+
child = null;
|
|
72
|
+
|
|
73
|
+
// Clean shutdown (code 0) — don't respawn
|
|
74
|
+
if (code === 0) {
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
|
|
80
|
+
// Reset crash counter if the last crash was more than 60s ago
|
|
81
|
+
if (now - lastCrashTime > 60_000) {
|
|
82
|
+
crashCount = 0;
|
|
83
|
+
backoffMs = 1000;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
crashCount++;
|
|
87
|
+
lastCrashTime = now;
|
|
88
|
+
|
|
89
|
+
if (crashCount > 5) {
|
|
90
|
+
logError(`Daemon crashed ${crashCount} times in 60s — giving up`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
logError(`Daemon exited with code ${code}, respawning in ${backoffMs}ms`);
|
|
95
|
+
setTimeout(spawnDaemon, backoffMs);
|
|
96
|
+
backoffMs = Math.min(backoffMs * 2, 30_000);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Forward termination signals to the child daemon
|
|
101
|
+
process.on("SIGTERM", () => {
|
|
102
|
+
if (child) child.kill("SIGTERM");
|
|
103
|
+
else process.exit(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
process.on("SIGINT", () => {
|
|
107
|
+
if (child) child.kill("SIGTERM");
|
|
108
|
+
else process.exit(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
spawnDaemon();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orphaned legacy daemon entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* The per-session daemon spawn path is removed. This file is retained only
|
|
5
|
+
* for stale external invocations and starts the canonical headless daemon in
|
|
6
|
+
* this process. Prefer `tmux-ide --headless`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { startEmbeddedDaemon, type EmbeddedDaemonHandle } from "./daemon-embed.ts";
|
|
12
|
+
|
|
13
|
+
async function main(): Promise<void> {
|
|
14
|
+
// Accept the legacy `node daemon.ts <session> <port>` invocation: the
|
|
15
|
+
// first positional may be a session name OR (when omitted) a port. We
|
|
16
|
+
// sniff by digit-shape and forward both. Without this, an explicit
|
|
17
|
+
// session name was silently dropped on the floor and the daemon booted
|
|
18
|
+
// as `__embedded__` — leaving the workspace registry empty for live
|
|
19
|
+
// tmux sessions the operator clearly meant to expose.
|
|
20
|
+
const arg2 = process.argv[2];
|
|
21
|
+
const arg3 = process.argv[3];
|
|
22
|
+
const arg2IsPort = arg2 != null && /^\d+$/.test(arg2);
|
|
23
|
+
const sessionName = arg2IsPort ? undefined : arg2;
|
|
24
|
+
const rawPort = arg2IsPort ? arg2 : arg3;
|
|
25
|
+
const port = rawPort === undefined || rawPort === "0" ? undefined : Number.parseInt(rawPort, 10);
|
|
26
|
+
let handle: EmbeddedDaemonHandle;
|
|
27
|
+
try {
|
|
28
|
+
handle = await startEmbeddedDaemon({
|
|
29
|
+
port,
|
|
30
|
+
bindHostname: "127.0.0.1",
|
|
31
|
+
...(sessionName ? { sessionName } : {}),
|
|
32
|
+
});
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error("[daemon] failed to start:", err);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let stopping = false;
|
|
39
|
+
const exitAfterStop = (code: number): void => {
|
|
40
|
+
if (stopping) return;
|
|
41
|
+
stopping = true;
|
|
42
|
+
// Defer one tick so any awaiter of handle.stop() observes the resolved
|
|
43
|
+
// promise before we tear the process down.
|
|
44
|
+
process.nextTick(() => process.exit(code));
|
|
45
|
+
};
|
|
46
|
+
const shutdown = async (): Promise<void> => {
|
|
47
|
+
if (stopping) return;
|
|
48
|
+
try {
|
|
49
|
+
await handle.stop();
|
|
50
|
+
exitAfterStop(0);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error("[daemon] failed to stop:", err);
|
|
53
|
+
exitAfterStop(1);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Wrap handle.stop so non-signal stop paths terminate the process. Without
|
|
58
|
+
// this, the canonical-daemon takeover action (POST /api/v2/action/daemon.shutdown
|
|
59
|
+
// from a competing launcher), the tick() that calls stopSelf when the
|
|
60
|
+
// tmux session disappears, and the remote-access restart backend all
|
|
61
|
+
// close the HTTP listener but leave the Node event loop spinning on
|
|
62
|
+
// task-store WAL timers and other handles. The visible symptom is a
|
|
63
|
+
// ghost daemon: process alive, port 6060 dead, takeover falls back to
|
|
64
|
+
// its 10s SIGTERM/SIGKILL deadline before the next launch can bind.
|
|
65
|
+
const origStop = handle.stop.bind(handle);
|
|
66
|
+
handle.stop = async (opts) => {
|
|
67
|
+
try {
|
|
68
|
+
await origStop(opts);
|
|
69
|
+
} finally {
|
|
70
|
+
exitAfterStop(0);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
process.on("SIGTERM", () => void shutdown());
|
|
75
|
+
process.on("SIGINT", () => void shutdown());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const isMainModule = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
79
|
+
if (isMainModule) void main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
export function getByPath(obj: Record<string, any>, path: string): unknown {
|
|
3
|
+
return path.split(".").reduce((o: any, k) => o?.[k], obj);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function setByPath(obj: Record<string, any>, path: string, value: unknown): void {
|
|
7
|
+
const keys = path.split(".");
|
|
8
|
+
const last = keys.pop()!;
|
|
9
|
+
let i = 0;
|
|
10
|
+
const target = keys.reduce((o: any, k) => {
|
|
11
|
+
const nextKey = keys[i + 1] ?? last;
|
|
12
|
+
if (o[k] === undefined) o[k] = /^\d+$/.test(nextKey) ? [] : {};
|
|
13
|
+
i++;
|
|
14
|
+
return o[k];
|
|
15
|
+
}, obj);
|
|
16
|
+
target[last] = value;
|
|
17
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export class IdeError extends Error {
|
|
2
|
+
code: string | undefined;
|
|
3
|
+
exitCode: number;
|
|
4
|
+
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
{ code, exitCode = 1, cause }: { code?: string; exitCode?: number; cause?: Error } = {},
|
|
8
|
+
) {
|
|
9
|
+
super(message, { cause });
|
|
10
|
+
this.name = "IdeError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.exitCode = exitCode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
toJSON(): { error: string; code: string | undefined; cause?: string } {
|
|
16
|
+
const obj: { error: string; code: string | undefined; cause?: string } = {
|
|
17
|
+
error: this.message,
|
|
18
|
+
code: this.code,
|
|
19
|
+
};
|
|
20
|
+
if (this.cause) obj.cause = (this.cause as Error).message;
|
|
21
|
+
return obj;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ConfigError extends IdeError {
|
|
26
|
+
constructor(message: string, code: string, { cause }: { cause?: Error } = {}) {
|
|
27
|
+
super(message, { code, exitCode: 1, cause });
|
|
28
|
+
this.name = "ConfigError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// TmuxError now lives in @tmux-ide/tmux-bridge. Re-exported here so the
|
|
33
|
+
// existing `import { TmuxError } from "./errors.ts"` paths keep working
|
|
34
|
+
// across the daemon. The bridge's class extends Error directly (not IdeError);
|
|
35
|
+
// the CLI's printCommandError handles both via duck-typing on `code`/`exitCode`.
|
|
36
|
+
export { TmuxError } from "@tmux-ide/tmux-bridge";
|
|
37
|
+
|
|
38
|
+
export class SessionError extends IdeError {
|
|
39
|
+
constructor(message: string, code: string, { cause }: { cause?: Error } = {}) {
|
|
40
|
+
super(message, { code, exitCode: 1, cause });
|
|
41
|
+
this.name = "SessionError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type DaemonStartupReason =
|
|
46
|
+
| "port_in_use"
|
|
47
|
+
| "port_invalid"
|
|
48
|
+
| "bind_failed"
|
|
49
|
+
| "tmux_session_missing"
|
|
50
|
+
| "canonical_already_running";
|
|
51
|
+
|
|
52
|
+
export class DaemonStartupError extends IdeError {
|
|
53
|
+
readonly reason: DaemonStartupReason;
|
|
54
|
+
|
|
55
|
+
constructor(message: string, reason: DaemonStartupReason, { cause }: { cause?: Error } = {}) {
|
|
56
|
+
super(message, { code: `DAEMON_${reason.toUpperCase()}`, exitCode: 1, cause });
|
|
57
|
+
this.name = "DaemonStartupError";
|
|
58
|
+
this.reason = reason;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class DaemonShutdownError extends IdeError {
|
|
63
|
+
constructor(message: string, { cause }: { cause?: Error } = {}) {
|
|
64
|
+
super(message, { code: "DAEMON_SHUTDOWN_FAILED", exitCode: 1, cause });
|
|
65
|
+
this.name = "DaemonShutdownError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem browsing for the dashboard's directory picker.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities:
|
|
5
|
+
* 1. Sandbox enforcement — the daemon refuses to list anything outside the
|
|
6
|
+
* user's home or the platform-conventional roots (/Users, /home,
|
|
7
|
+
* /Volumes). Never trust the client to limit paths.
|
|
8
|
+
* 2. Entry classification — directories first, then files; alphabetical
|
|
9
|
+
* within group; symlinks resolved once to determine target kind.
|
|
10
|
+
*
|
|
11
|
+
* The io functions are pluggable for tests so we don't need a full mock
|
|
12
|
+
* filesystem.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { realpathSync, readdirSync, statSync, type Dirent } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
18
|
+
|
|
19
|
+
import type { FilesystemBrowseResult, FilesystemEntry } from "../schemas/filesystem.ts";
|
|
20
|
+
|
|
21
|
+
/** Root directories the sandbox allows in addition to the user's home. */
|
|
22
|
+
const ALLOWED_PLATFORM_ROOTS = ["/Users", "/home", "/Volumes"] as const;
|
|
23
|
+
|
|
24
|
+
/** Per-entry stat budget. Slow network mounts get skipped silently. */
|
|
25
|
+
const STAT_BUDGET_MS = 100;
|
|
26
|
+
|
|
27
|
+
export class SandboxViolationError extends Error {
|
|
28
|
+
readonly code = "outside-sandbox";
|
|
29
|
+
constructor(path: string) {
|
|
30
|
+
super(`Path "${path}" is outside the allowed sandbox`);
|
|
31
|
+
this.name = "SandboxViolationError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class PathNotFoundError extends Error {
|
|
36
|
+
readonly code = "not-found";
|
|
37
|
+
constructor(path: string) {
|
|
38
|
+
super(`Path "${path}" does not exist`);
|
|
39
|
+
this.name = "PathNotFoundError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class InvalidPathError extends Error {
|
|
44
|
+
readonly code = "invalid-path";
|
|
45
|
+
constructor(message: string) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "InvalidPathError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BrowseIo {
|
|
52
|
+
/** `fs.realpathSync`. Throws if the path doesn't exist. */
|
|
53
|
+
realpath(path: string): string;
|
|
54
|
+
/** `fs.statSync`. Used to follow symlinks once when classifying. */
|
|
55
|
+
stat(path: string): { isDirectory(): boolean };
|
|
56
|
+
/** `fs.readdirSync(path, { withFileTypes: true })`. */
|
|
57
|
+
readdir(path: string): Dirent[];
|
|
58
|
+
/** `os.homedir()`. */
|
|
59
|
+
home(): string;
|
|
60
|
+
/** Wall-clock; injectable for tests and for the per-entry stat budget. */
|
|
61
|
+
now(): number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the user's home directory. Honors `TMUX_IDE_HOME_OVERRIDE` so
|
|
66
|
+
* tests can pin a temp directory without depending on `os.homedir()` (some
|
|
67
|
+
* runtimes — bun in particular — cache the value at startup and do not
|
|
68
|
+
* pick up later `process.env.HOME` mutations).
|
|
69
|
+
*/
|
|
70
|
+
function resolveHome(): string {
|
|
71
|
+
const override = process.env.TMUX_IDE_HOME_OVERRIDE;
|
|
72
|
+
if (override && override.trim().length > 0) return override;
|
|
73
|
+
return homedir();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const realIo: BrowseIo = {
|
|
77
|
+
realpath: realpathSync,
|
|
78
|
+
stat: statSync,
|
|
79
|
+
readdir: (path) => readdirSync(path, { withFileTypes: true }),
|
|
80
|
+
home: resolveHome,
|
|
81
|
+
now: () => Date.now(),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export interface BrowseInput {
|
|
85
|
+
/** Absolute path to list. Empty/missing → home. */
|
|
86
|
+
path?: string | null | undefined;
|
|
87
|
+
/** When true, return entries whose names start with `.`. */
|
|
88
|
+
showHidden?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate an input path before passing to realpath. We reject obvious
|
|
93
|
+
* traversal attempts and non-absolute paths so error messages stay clean.
|
|
94
|
+
* `~` and `~/...` expand to the user's home directory so the dialog's
|
|
95
|
+
* "Home" quick-jump (and any caller that hands us a tilde-prefixed path)
|
|
96
|
+
* doesn't trip the absolute-path check.
|
|
97
|
+
*/
|
|
98
|
+
function preflight(rawPath: string, io: BrowseIo): string {
|
|
99
|
+
const trimmed = rawPath.trim();
|
|
100
|
+
if (!trimmed) {
|
|
101
|
+
throw new InvalidPathError("Path must not be empty");
|
|
102
|
+
}
|
|
103
|
+
if (trimmed.includes("\0")) {
|
|
104
|
+
throw new InvalidPathError("Path contains a null byte");
|
|
105
|
+
}
|
|
106
|
+
let candidate = trimmed;
|
|
107
|
+
if (candidate === "~") {
|
|
108
|
+
candidate = io.home();
|
|
109
|
+
} else if (candidate.startsWith("~/")) {
|
|
110
|
+
candidate = join(io.home(), candidate.slice(2));
|
|
111
|
+
}
|
|
112
|
+
if (!isAbsolute(candidate)) {
|
|
113
|
+
throw new InvalidPathError("Path must be absolute");
|
|
114
|
+
}
|
|
115
|
+
// Collapse `..` and `.` early so the realpath call has a clean input.
|
|
116
|
+
return resolve(candidate);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Returns true iff `canonical` is the allowed root or strictly under it.
|
|
121
|
+
* Both arguments must be canonical (already realpath'd) and absolute.
|
|
122
|
+
*/
|
|
123
|
+
function isUnderRoot(canonical: string, root: string): boolean {
|
|
124
|
+
if (canonical === root) return true;
|
|
125
|
+
const prefix = root.endsWith(sep) ? root : root + sep;
|
|
126
|
+
return canonical.startsWith(prefix);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Sandbox check: the canonical path must live inside the user's home OR
|
|
131
|
+
* one of the platform-conventional public roots. Anything else throws.
|
|
132
|
+
*
|
|
133
|
+
* Exposed for tests; do NOT skip this from the route handler.
|
|
134
|
+
*/
|
|
135
|
+
export function assertInsideSandbox(canonical: string, home: string): void {
|
|
136
|
+
if (isUnderRoot(canonical, home)) return;
|
|
137
|
+
for (const root of ALLOWED_PLATFORM_ROOTS) {
|
|
138
|
+
if (isUnderRoot(canonical, root)) return;
|
|
139
|
+
}
|
|
140
|
+
throw new SandboxViolationError(canonical);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Compute the parent directory path for browse navigation. Returns null
|
|
145
|
+
* when the parent would escape the sandbox, so the UI can disable the
|
|
146
|
+
* back button.
|
|
147
|
+
*/
|
|
148
|
+
export function computeParentPath(canonical: string, home: string): string | null {
|
|
149
|
+
// `path.resolve` collapses trailing separators; never returns an empty
|
|
150
|
+
// string, which means "/" maps to "/".
|
|
151
|
+
if (canonical === "/" || canonical === sep) return null;
|
|
152
|
+
|
|
153
|
+
const parent = resolve(canonical, "..");
|
|
154
|
+
if (parent === canonical) return null;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
assertInsideSandbox(parent, home);
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return parent;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sort dirs first, files second; case-insensitive alphabetical within group.
|
|
166
|
+
* Stable: returns a new array, doesn't mutate input.
|
|
167
|
+
*/
|
|
168
|
+
export function sortEntries(entries: ReadonlyArray<FilesystemEntry>): FilesystemEntry[] {
|
|
169
|
+
return [...entries].sort((a, b) => {
|
|
170
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
171
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface ClassifyResult {
|
|
176
|
+
entry: FilesystemEntry | null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Convert a single dirent to a `FilesystemEntry`. Symlinks are followed
|
|
181
|
+
* exactly once via `stat` so we can mark `isDir` correctly. Per-entry stat
|
|
182
|
+
* is bounded by `STAT_BUDGET_MS`; entries that exceed it are dropped.
|
|
183
|
+
*
|
|
184
|
+
* Exposed for tests.
|
|
185
|
+
*/
|
|
186
|
+
export function classifyDirent(dirent: Dirent, fullPath: string, io: BrowseIo): ClassifyResult {
|
|
187
|
+
const isSymlink = dirent.isSymbolicLink();
|
|
188
|
+
if (!isSymlink) {
|
|
189
|
+
return {
|
|
190
|
+
entry: {
|
|
191
|
+
name: dirent.name,
|
|
192
|
+
fullPath,
|
|
193
|
+
isDir: dirent.isDirectory(),
|
|
194
|
+
isSymlink: false,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Follow symlinks once. Slow network mounts get a short stat budget and
|
|
200
|
+
// skip silently if exceeded.
|
|
201
|
+
const start = io.now();
|
|
202
|
+
let isDir: boolean;
|
|
203
|
+
try {
|
|
204
|
+
const stat = io.stat(fullPath);
|
|
205
|
+
if (io.now() - start > STAT_BUDGET_MS) {
|
|
206
|
+
return { entry: null };
|
|
207
|
+
}
|
|
208
|
+
isDir = stat.isDirectory();
|
|
209
|
+
} catch {
|
|
210
|
+
// Broken symlink — keep it as a non-directory entry so the UI can
|
|
211
|
+
// still show it, but mark accordingly.
|
|
212
|
+
return {
|
|
213
|
+
entry: {
|
|
214
|
+
name: dirent.name,
|
|
215
|
+
fullPath,
|
|
216
|
+
isDir: false,
|
|
217
|
+
isSymlink: true,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
entry: {
|
|
224
|
+
name: dirent.name,
|
|
225
|
+
fullPath,
|
|
226
|
+
isDir,
|
|
227
|
+
isSymlink: true,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Browse a directory. Returns canonical path + entries + parent.
|
|
234
|
+
*
|
|
235
|
+
* @throws {InvalidPathError} Bad input (empty, relative, null bytes).
|
|
236
|
+
* @throws {PathNotFoundError} Path doesn't exist (realpath failed).
|
|
237
|
+
* @throws {SandboxViolationError} Path resolves outside the allowed sandbox.
|
|
238
|
+
*/
|
|
239
|
+
export function browseDirectory(input: BrowseInput, io: BrowseIo = realIo): FilesystemBrowseResult {
|
|
240
|
+
const home = io.home();
|
|
241
|
+
const requestedPath = input.path?.trim() ? input.path.trim() : home;
|
|
242
|
+
|
|
243
|
+
let preflightPath: string;
|
|
244
|
+
try {
|
|
245
|
+
preflightPath = preflight(requestedPath, io);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (err instanceof InvalidPathError) throw err;
|
|
248
|
+
throw new InvalidPathError("Invalid path");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let canonical: string;
|
|
252
|
+
try {
|
|
253
|
+
canonical = io.realpath(preflightPath);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
256
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
257
|
+
throw new PathNotFoundError(preflightPath);
|
|
258
|
+
}
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
assertInsideSandbox(canonical, home);
|
|
263
|
+
|
|
264
|
+
let dirents: Dirent[];
|
|
265
|
+
try {
|
|
266
|
+
dirents = io.readdir(canonical);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
269
|
+
if (code === "ENOENT") {
|
|
270
|
+
throw new PathNotFoundError(canonical);
|
|
271
|
+
}
|
|
272
|
+
if (code === "ENOTDIR") {
|
|
273
|
+
throw new InvalidPathError(`"${canonical}" is not a directory`);
|
|
274
|
+
}
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const showHidden = Boolean(input.showHidden);
|
|
279
|
+
const entries: FilesystemEntry[] = [];
|
|
280
|
+
for (const dirent of dirents) {
|
|
281
|
+
if (!showHidden && dirent.name.startsWith(".")) continue;
|
|
282
|
+
const fullPath = join(canonical, dirent.name);
|
|
283
|
+
const { entry } = classifyDirent(dirent, fullPath, io);
|
|
284
|
+
if (entry !== null) entries.push(entry);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
path: canonical,
|
|
289
|
+
parentPath: computeParentPath(canonical, home),
|
|
290
|
+
entries: sortEntries(entries),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { Pane, Row, PaneAction } from "../types.ts";
|
|
3
|
+
import { shellEscape } from "./shell.ts";
|
|
4
|
+
|
|
5
|
+
export function buildPaneCommand(pane: Pane): string | null {
|
|
6
|
+
if (!pane.command) return null;
|
|
7
|
+
return pane.command;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function collectPaneStartupPlan(
|
|
11
|
+
rows: Row[],
|
|
12
|
+
paneMap: string[][],
|
|
13
|
+
firstPanesOfRows: Set<string>,
|
|
14
|
+
dir: string,
|
|
15
|
+
): { focusPane: string; paneActions: PaneAction[] } {
|
|
16
|
+
let focusPane = paneMap[0]![0]!;
|
|
17
|
+
const paneActions: PaneAction[] = [];
|
|
18
|
+
|
|
19
|
+
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
|
20
|
+
const row = rows[rowIdx]!;
|
|
21
|
+
const panes = row.panes ?? [];
|
|
22
|
+
|
|
23
|
+
for (let paneIdx = 0; paneIdx < panes.length; paneIdx++) {
|
|
24
|
+
const pane = panes[paneIdx]!;
|
|
25
|
+
const tmuxPane = paneMap[rowIdx]![paneIdx]!;
|
|
26
|
+
// Derive @ide_role from pane config
|
|
27
|
+
let paneRole: string;
|
|
28
|
+
if (pane.role === "lead") {
|
|
29
|
+
paneRole = "lead";
|
|
30
|
+
} else if (pane.role === "teammate" || pane.role === "planner") {
|
|
31
|
+
paneRole = "teammate";
|
|
32
|
+
} else if (pane.type) {
|
|
33
|
+
paneRole = "widget";
|
|
34
|
+
} else {
|
|
35
|
+
paneRole = "shell";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Derive @ide_type from pane config
|
|
39
|
+
let paneType: string;
|
|
40
|
+
if (pane.type) {
|
|
41
|
+
paneType = pane.type;
|
|
42
|
+
} else if (pane.command && /claude|codex/i.test(pane.command)) {
|
|
43
|
+
paneType = "agent";
|
|
44
|
+
} else {
|
|
45
|
+
paneType = "shell";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const action: PaneAction = {
|
|
49
|
+
targetPane: tmuxPane,
|
|
50
|
+
title: pane.title ?? null,
|
|
51
|
+
chdir: null,
|
|
52
|
+
exports: [],
|
|
53
|
+
command: null,
|
|
54
|
+
widgetType: pane.type ?? null,
|
|
55
|
+
widgetTarget: pane.target ?? null,
|
|
56
|
+
paneRole,
|
|
57
|
+
paneType,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (pane.dir && firstPanesOfRows.has(tmuxPane)) {
|
|
61
|
+
action.chdir = resolve(dir, pane.dir);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (pane.env && typeof pane.env === "object") {
|
|
65
|
+
action.exports = Object.entries(pane.env).map(
|
|
66
|
+
([key, value]) => `export ${shellEscape(key)}=${shellEscape(String(value))}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let command = buildPaneCommand(pane);
|
|
71
|
+
// Inject --name flag into Claude/Codex commands so the agent
|
|
72
|
+
// sets its own pane title to match the configured name.
|
|
73
|
+
// This is agent-agnostic: any CLI that respects --name will work.
|
|
74
|
+
if (command && pane.title && /claude|codex/i.test(command) && !command.includes("--name")) {
|
|
75
|
+
command = `${command} --name ${shellEscape(pane.title)}`;
|
|
76
|
+
}
|
|
77
|
+
if (command) {
|
|
78
|
+
action.command = command;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (pane.focus) {
|
|
82
|
+
focusPane = tmuxPane;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
paneActions.push(action);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { focusPane, paneActions };
|
|
90
|
+
}
|