tmux-ide 2.6.1 → 2.7.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 +14 -9
- package/bin/cli.js +1013 -516
- package/bin/cli.ts +62 -5
- package/package.json +9 -6
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
- package/packages/daemon/dist/lib/tui-binary.js +130 -0
- package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
- package/packages/daemon/src/agent-explain.ts +34 -6
- package/packages/daemon/src/lib/app-config.ts +93 -2
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/tui/chrome/notify.ts +221 -34
- package/packages/daemon/src/tui/chrome/updater.ts +89 -7
- package/packages/daemon/src/tui/compiled.ts +11 -3
- package/packages/daemon/src/tui/detect/classify.ts +15 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +2 -1
- package/packages/daemon/src/tui/detect/manifest.ts +15 -0
- package/packages/daemon/src/tui/detect/manifests.ts +161 -33
- package/packages/daemon/src/tui/detect/process-tree.ts +20 -0
- package/packages/daemon/src/tui/main.ts +13 -1
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
- package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +80 -9
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
- package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
- package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
- package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
- package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
- package/packages/daemon/src/tui/mirror/palette.ts +274 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +499 -19
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
- package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
- package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
- package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
- package/packages/daemon/src/tui/mirror/selection.ts +262 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
- package/packages/daemon/src/tui/mirror/spans.ts +46 -0
- package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
- package/packages/daemon/src/tui/team/entry.ts +34 -7
- package/packages/daemon/src/tui/team/report.ts +11 -1
- package/packages/daemon/src/tui/team/sessions.ts +102 -13
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/skill/SKILL.md +22 -0
- package/templates/AGENTS.md +14 -7
- package/templates/agent-team-monorepo.yml +8 -0
- package/templates/agent-team-nextjs.yml +8 -0
- package/templates/agent-team.yml +10 -0
- package/templates/convex.yml +2 -0
- package/templates/default.yml +11 -5
- package/templates/go.yml +4 -0
- package/templates/missions.yml +6 -0
- package/templates/nextjs.yml +4 -0
- package/templates/python.yml +4 -0
- package/templates/skills/backend.md +5 -12
- package/templates/skills/frontend.md +5 -12
- package/templates/skills/general-worker.md +5 -12
- package/templates/skills/researcher.md +7 -12
- package/templates/skills/reviewer.md +7 -16
- package/templates/vite.yml +4 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +0 -166
package/bin/cli.ts
CHANGED
|
@@ -9,8 +9,17 @@ import { existsSync } from "node:fs";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
// The node-runnable CLI path that spawned TUI surfaces shell back to (the app's
|
|
13
|
+
// async fleet poll + `detect --write`). When we're the published `bin/cli.js`
|
|
14
|
+
// this file IS it; under dev (`bun bin/cli.ts`) it's the built sibling
|
|
15
|
+
// `bin/cli.js`. Forwarded to surfaces as TMUX_IDE_CLI so the COMPILED TUI binary
|
|
16
|
+
// — whose own `import.meta.url` is a virtual bunfs path with no real cli.js next
|
|
17
|
+
// to it — can still find the CLI to run its subprocesses.
|
|
18
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
19
|
+
const nodeCliPath = selfPath.endsWith(".js") ? selfPath : resolve(__dirname, "cli.js");
|
|
12
20
|
import { launch } from "../packages/daemon/src/launch.ts";
|
|
13
|
-
import {
|
|
21
|
+
import { resolveEntry } from "../packages/daemon/src/tui/team/entry.ts";
|
|
22
|
+
import { loadAppConfig } from "../packages/daemon/src/lib/app-config.ts";
|
|
14
23
|
import {
|
|
15
24
|
resolveTuiLaunch,
|
|
16
25
|
findCompiledTui,
|
|
@@ -110,6 +119,7 @@ const knownCommands = new Set([
|
|
|
110
119
|
"send",
|
|
111
120
|
"settings",
|
|
112
121
|
"team",
|
|
122
|
+
"app",
|
|
113
123
|
"switcher",
|
|
114
124
|
"wait",
|
|
115
125
|
"events",
|
|
@@ -177,6 +187,7 @@ ${bold("Usage:")}
|
|
|
177
187
|
${dim("(--resume-agents revives claude conversations via claude --resume)")}
|
|
178
188
|
${cyan("tmux-ide attach")} ${dim("Reattach to a running session")}
|
|
179
189
|
${cyan("tmux-ide team")} [--json] ${dim("TUI over all tmux sessions (--json prints fleet state)")}
|
|
190
|
+
${cyan("tmux-ide app")} [session] ${dim("Unified app: fleet home + live session mirror (bare = home)")}
|
|
180
191
|
${cyan("tmux-ide switcher")} ${dim("Compact session picker (opens in the M-p popup on adopted sessions)")}
|
|
181
192
|
${cyan("tmux-ide wait agent-status")} <session> --status <s> [--timeout <ms>]
|
|
182
193
|
${dim("Block until a session reaches a status (exit 0 match / 1 timeout)")}
|
|
@@ -269,7 +280,12 @@ function execBunWidget(
|
|
|
269
280
|
);
|
|
270
281
|
}
|
|
271
282
|
|
|
272
|
-
const env = {
|
|
283
|
+
const env = {
|
|
284
|
+
...process.env,
|
|
285
|
+
TMUX_IDE_CWD: process.cwd(),
|
|
286
|
+
TMUX_IDE_CLI: nodeCliPath,
|
|
287
|
+
...extraEnv,
|
|
288
|
+
};
|
|
273
289
|
if (launch.mode === "bun") {
|
|
274
290
|
// Spawn from the repo root so bun finds `bunfig.toml` (the @opentui/solid
|
|
275
291
|
// JSX preload). Without this, running from any other cwd — e.g. bare
|
|
@@ -302,6 +318,7 @@ async function printFleetJson(): Promise<void> {
|
|
|
302
318
|
}
|
|
303
319
|
|
|
304
320
|
const teamScriptPath = resolve(__dirname, "../packages/daemon/src/tui/team/index.tsx");
|
|
321
|
+
const appScriptPath = resolve(__dirname, "../packages/daemon/src/tui/mirror/app.tsx");
|
|
305
322
|
|
|
306
323
|
// `tmux-ide team` runs the standalone full-screen cockpit (the OpenTUI app owns
|
|
307
324
|
// the whole terminal). The floating switcher popup (M-p on adopted sessions)
|
|
@@ -310,6 +327,13 @@ function launchTeamCockpit(): void {
|
|
|
310
327
|
execBunWidget("team", teamScriptPath, [], "team");
|
|
311
328
|
}
|
|
312
329
|
|
|
330
|
+
// The unified app as the front door (M22.6): bare `tmux-ide` opens `tmux-ide
|
|
331
|
+
// app`'s HOME panel when `app.frontDoor` is on and there's nothing else to
|
|
332
|
+
// launch. Same entry as the explicit `app` command with no session positional.
|
|
333
|
+
function launchApp(): void {
|
|
334
|
+
execBunWidget("app", appScriptPath, [], "app");
|
|
335
|
+
}
|
|
336
|
+
|
|
313
337
|
try {
|
|
314
338
|
switch (command) {
|
|
315
339
|
case "start": {
|
|
@@ -332,13 +356,23 @@ try {
|
|
|
332
356
|
}
|
|
333
357
|
const targetDir = resolve(startTargetDir || ".");
|
|
334
358
|
const hasIdeYml = existsSync(join(targetDir, "ide.yml"));
|
|
335
|
-
|
|
336
|
-
|
|
359
|
+
// M22.6 — the front-door decision. `--team` always means the classic
|
|
360
|
+
// cockpit; a present ide.yml still auto-launches the project; otherwise
|
|
361
|
+
// `app.frontDoor` flips the default no-project entry to the unified app.
|
|
362
|
+
const entry = resolveEntry({
|
|
363
|
+
hasIdeYml,
|
|
364
|
+
teamFlag: values.team === true,
|
|
365
|
+
frontDoor: loadAppConfig().app.frontDoor,
|
|
366
|
+
});
|
|
367
|
+
if (entry !== "project") {
|
|
368
|
+
// No project to launch here. `--json` is a scripting surface — it always
|
|
369
|
+
// prints the fleet, whichever interactive front door is configured.
|
|
337
370
|
if (json) {
|
|
338
371
|
await printFleetJson();
|
|
339
372
|
break;
|
|
340
373
|
}
|
|
341
|
-
|
|
374
|
+
if (entry === "app") launchApp();
|
|
375
|
+
else launchTeamCockpit();
|
|
342
376
|
break;
|
|
343
377
|
}
|
|
344
378
|
await launch(startTargetDir, { json });
|
|
@@ -491,6 +525,16 @@ try {
|
|
|
491
525
|
break;
|
|
492
526
|
}
|
|
493
527
|
|
|
528
|
+
case "app": {
|
|
529
|
+
// The unified app (M18.1): sidebar fleet + a live tmux-session mirror.
|
|
530
|
+
// Bare `tmux-ide app` opens the HOME panel (fleet cards); an optional
|
|
531
|
+
// session positional boots straight into that session's mirror.
|
|
532
|
+
const session = positionals[1];
|
|
533
|
+
const appArgs = session ? [`--target=${session}`] : [];
|
|
534
|
+
execBunWidget("app", appScriptPath, appArgs, "app");
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
|
|
494
538
|
case "switcher": {
|
|
495
539
|
// Runs the team app in PICKER mode inside a tmux `display-popup` (bound to
|
|
496
540
|
// `M-p` on adopt). Picking a session `switch-client`s the invoking client
|
|
@@ -1338,6 +1382,19 @@ try {
|
|
|
1338
1382
|
}
|
|
1339
1383
|
|
|
1340
1384
|
case "update": {
|
|
1385
|
+
// `--tui-binary`: download the per-platform TUI binary (the fallback that
|
|
1386
|
+
// lets an npm install with no bun run the full cockpit). Explicit opt-in —
|
|
1387
|
+
// never auto-fetched on install (it's ~70MB). See lib/tui-binary.ts.
|
|
1388
|
+
if (values["tui-binary"] === true) {
|
|
1389
|
+
const { downloadTuiBinary } = await import("../packages/daemon/src/lib/tui-binary.ts");
|
|
1390
|
+
const { path } = await downloadTuiBinary({ log: (m) => console.error(m) });
|
|
1391
|
+
if (json) {
|
|
1392
|
+
console.log(JSON.stringify({ ok: true, path }, null, 2));
|
|
1393
|
+
} else {
|
|
1394
|
+
console.log(`TUI binary ready: ${path}`);
|
|
1395
|
+
}
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1341
1398
|
// Detect how tmux-ide was installed and act on the pending update: a dev
|
|
1342
1399
|
// checkout prints the `git pull` hint; a global install prints (with
|
|
1343
1400
|
// --dry-run) or runs its package manager's update command. `__dirname` is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-ide",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Turn any project into a tmux-powered terminal IDE with a simple ide.yml",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -65,15 +65,15 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@hono/node-server": "^1.19.11",
|
|
67
67
|
"@hono/zod-validator": "^0.7.6",
|
|
68
|
-
"@opentui/core": "^0.
|
|
69
|
-
"@opentui/solid": "^0.
|
|
68
|
+
"@opentui/core": "^0.4.3",
|
|
69
|
+
"@opentui/solid": "^0.4.3",
|
|
70
70
|
"@parcel/watcher": "^2.5.6",
|
|
71
71
|
"@types/ws": "^8.18.1",
|
|
72
72
|
"hono": "^4.12.8",
|
|
73
73
|
"ignore": "^7.0.5",
|
|
74
74
|
"js-yaml": "^4.1.1",
|
|
75
75
|
"node-pty": "1.2.0-beta.12",
|
|
76
|
-
"solid-js": "
|
|
76
|
+
"solid-js": "1.9.12",
|
|
77
77
|
"ws": "^8.20.0",
|
|
78
78
|
"zod": "^4.3.6"
|
|
79
79
|
},
|
|
@@ -82,7 +82,10 @@
|
|
|
82
82
|
"@parcel/watcher",
|
|
83
83
|
"esbuild",
|
|
84
84
|
"node-pty"
|
|
85
|
-
]
|
|
85
|
+
],
|
|
86
|
+
"overrides": {
|
|
87
|
+
"zod": "^4.3.6"
|
|
88
|
+
}
|
|
86
89
|
},
|
|
87
90
|
"devDependencies": {
|
|
88
91
|
"@eslint/js": "^10.0.1",
|
|
@@ -100,6 +103,6 @@
|
|
|
100
103
|
"vitest": "^4.1.0"
|
|
101
104
|
},
|
|
102
105
|
"optionalDependencies": {
|
|
103
|
-
"@opentui/core-darwin-arm64": "^0.
|
|
106
|
+
"@opentui/core-darwin-arm64": "^0.4.3"
|
|
104
107
|
}
|
|
105
108
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DetectedState } from "./tui/detect/manifest.ts";
|
|
1
|
+
import { type DetectedState, type ManifestConfidence } from "./tui/detect/manifest.ts";
|
|
2
2
|
import { type AgentStatus, type InstantState } from "./tui/detect/classify.ts";
|
|
3
3
|
export interface ExplainReport {
|
|
4
4
|
pane: {
|
|
@@ -23,6 +23,13 @@ export interface ExplainReport {
|
|
|
23
23
|
manifestId: string | null;
|
|
24
24
|
matchedCommand: string;
|
|
25
25
|
source: "hint" | "fast" | "tree" | "none";
|
|
26
|
+
/** The matched manifest's evidence confidence (null when nothing resolved). */
|
|
27
|
+
confidence: ManifestConfidence | null;
|
|
28
|
+
/**
|
|
29
|
+
* Executable-ish tokens seen in the pane's process subtree. Populated for
|
|
30
|
+
* the "no manifest matched" case so the report can say what it DID find.
|
|
31
|
+
*/
|
|
32
|
+
subtree: string[];
|
|
26
33
|
};
|
|
27
34
|
states: Array<{
|
|
28
35
|
state: DetectedState;
|
|
@@ -17,7 +17,7 @@ import { execFileSync } from "node:child_process";
|
|
|
17
17
|
import { explain } from "./tui/detect/manifest.js";
|
|
18
18
|
import { classifyInstant, parseAuthority, } from "./tui/detect/classify.js";
|
|
19
19
|
import { getManifests } from "./tui/detect/manifest-loader.js";
|
|
20
|
-
import { readProcessTable, resolveAgentCommand } from "./tui/detect/process-tree.js";
|
|
20
|
+
import { describeSubtree, readProcessTable, resolveAgentCommand, } from "./tui/detect/process-tree.js";
|
|
21
21
|
import { readPaneSnapshot } from "./tui/detect/snapshot.js";
|
|
22
22
|
import { IdeError } from "./lib/errors.js";
|
|
23
23
|
/** Working/blocked authority reports older than this are stale (mirrors classify.ts). */
|
|
@@ -91,6 +91,9 @@ export function buildReport(target) {
|
|
|
91
91
|
hint: info.hintRaw,
|
|
92
92
|
});
|
|
93
93
|
const manifest = resolved.manifest;
|
|
94
|
+
// When nothing resolved, record what the process-tree DID see so `explain`
|
|
95
|
+
// can point the user at an `@agent_hint` target instead of a dead end.
|
|
96
|
+
const subtree = manifest ? [] : describeSubtree(table, info.pid);
|
|
94
97
|
// Snapshot + per-state explanation.
|
|
95
98
|
const snapshot = { ...readPaneSnapshot(info.id), title: info.title };
|
|
96
99
|
const explained = manifest
|
|
@@ -119,6 +122,8 @@ export function buildReport(target) {
|
|
|
119
122
|
manifestId: manifest?.id ?? null,
|
|
120
123
|
matchedCommand: resolved.matchedCommand,
|
|
121
124
|
source: resolved.source,
|
|
125
|
+
confidence: manifest ? (manifest.confidence ?? "conservative") : null,
|
|
126
|
+
subtree,
|
|
122
127
|
},
|
|
123
128
|
states: explained.checked,
|
|
124
129
|
winner: explained.state,
|
|
@@ -168,8 +173,19 @@ export function renderReport(r, opts = {}) {
|
|
|
168
173
|
out.push(` ${label("hint")} ${dim("(unset)")}`);
|
|
169
174
|
}
|
|
170
175
|
// Resolution
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
if (r.resolution.manifestId) {
|
|
177
|
+
const conf = r.resolution.confidence === "tuned"
|
|
178
|
+
? c("\x1b[32m", "tuned")
|
|
179
|
+
: dim(r.resolution.confidence ?? "conservative");
|
|
180
|
+
out.push(` ${label("manifest")} ${r.resolution.manifestId} ${dim(`via ${r.resolution.source}` + (r.resolution.matchedCommand ? ` "${r.resolution.matchedCommand}"` : ""))} [${conf}]`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// No manifest matched — say so, and report what the process-tree DID see so
|
|
184
|
+
// the user can set an `@agent_hint` or add a manifest.
|
|
185
|
+
const saw = r.resolution.subtree.length > 0 ? r.resolution.subtree.join(", ") : r.pane.cmd || "(nothing)";
|
|
186
|
+
out.push(` ${label("manifest")} ${dim("none matched")} — ${dim(`process-tree saw: ${saw}`)}`);
|
|
187
|
+
out.push(` ${dim("set `tmux set-option -p @agent_hint <agent>` to force one")}`);
|
|
188
|
+
}
|
|
173
189
|
// Per-state rule results
|
|
174
190
|
out.push("");
|
|
175
191
|
out.push(bold(" state rules"));
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** The `<os>-<arch>` tags we publish a prebuilt TUI binary for. */
|
|
2
|
+
export type TuiPlatformTag = "darwin-arm64" | "darwin-x64" | "linux-x64" | "linux-arm64";
|
|
3
|
+
/** The GitHub repo the release assets live under. */
|
|
4
|
+
export declare const RELEASE_REPO = "wavyrai/tmux-ide";
|
|
5
|
+
/** A downloaded binary smaller than this is treated as corrupt/truncated. */
|
|
6
|
+
export declare const MIN_TUI_BINARY_BYTES: number;
|
|
7
|
+
/**
|
|
8
|
+
* PURE — map a Node `process.platform`/`process.arch` pair to the release tag,
|
|
9
|
+
* or null when we don't publish a binary for it (e.g. windows, freebsd) so the
|
|
10
|
+
* caller can fall back to the "install bun" message.
|
|
11
|
+
*/
|
|
12
|
+
export declare function tuiPlatformTag(platform?: NodeJS.Platform, arch?: string): TuiPlatformTag | null;
|
|
13
|
+
/** PURE — the `bun build --compile --target` flag for a tag (`bun-<tag>`). */
|
|
14
|
+
export declare function bunTargetForTag(tag: TuiPlatformTag): string;
|
|
15
|
+
/** PURE — the release asset filename for a tag (gzip-compressed binary). */
|
|
16
|
+
export declare function releaseAssetName(tag: TuiPlatformTag): string;
|
|
17
|
+
/** PURE — strip a leading `v` from a version string; `2.6.1` and `v2.6.1` both → `2.6.1`. */
|
|
18
|
+
export declare function normalizeVersion(version: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* PURE — the GitHub download URL for a platform's asset at a given version:
|
|
21
|
+
* `https://github.com/<repo>/releases/download/v<version>/tmux-ide-tui-<tag>.gz`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function releaseAssetUrl(version: string, tag: TuiPlatformTag): string;
|
|
24
|
+
/**
|
|
25
|
+
* PURE — where a downloaded binary lives: `<home>/bin/tmux-ide-tui-<tag>-<version>`.
|
|
26
|
+
* The version is stamped INTO the name so a `tmux-ide update` to a new version
|
|
27
|
+
* misses the old download and re-fetches (rather than launching a stale binary).
|
|
28
|
+
*/
|
|
29
|
+
export declare function downloadedTuiPath(home: string, tag: TuiPlatformTag, version: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* io — the tmux-ide state home (`TMUX_IDE_HOME` override, else `~/.tmux-ide`),
|
|
32
|
+
* the same resolution the update-check cache and welcome marker use.
|
|
33
|
+
*/
|
|
34
|
+
export declare function tuiStateHome(): string;
|
|
35
|
+
/**
|
|
36
|
+
* io — locate a previously downloaded per-platform binary for THIS version and
|
|
37
|
+
* platform, or null. Feeds the resolution order in `tui/compiled.ts` after the
|
|
38
|
+
* shipped/local compiled binary and before the honest "unavailable" error.
|
|
39
|
+
*/
|
|
40
|
+
export declare function findDownloadedTui(version?: string): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* io — download, verify, and install the per-platform TUI binary for the running
|
|
43
|
+
* version. Fetches the gzip asset, inflates it, rejects anything under
|
|
44
|
+
* {@link MIN_TUI_BINARY_BYTES} (a truncated download or an HTML error page),
|
|
45
|
+
* writes it `0o755`, and atomically renames it into place (temp-in-same-dir →
|
|
46
|
+
* rename, so a crashed download never leaves a half-written executable).
|
|
47
|
+
*
|
|
48
|
+
* Throws with an actionable message on an unsupported platform or a failed
|
|
49
|
+
* fetch. Returns the installed path and byte size.
|
|
50
|
+
*/
|
|
51
|
+
export declare function downloadTuiBinary(opts?: {
|
|
52
|
+
version?: string;
|
|
53
|
+
log?: (msg: string) => void;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
path: string;
|
|
56
|
+
bytes: number;
|
|
57
|
+
}>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-platform TUI binary: the runtime-download fallback that lets a clean
|
|
3
|
+
* `npm i -g tmux-ide` run the full OpenTUI/Solid cockpit WITHOUT `bun`.
|
|
4
|
+
*
|
|
5
|
+
* The dev checkout runs the `.tsx` surfaces via bun; an npm install with no bun
|
|
6
|
+
* needs a self-contained binary. We do NOT bundle that ~70MB blob in the npm
|
|
7
|
+
* tarball (a surprise on every install) — instead the release workflow
|
|
8
|
+
* (`.github/workflows/release-binaries.yml`) cross-compiles one per platform and
|
|
9
|
+
* uploads them as GitHub release assets, and this module downloads the right one
|
|
10
|
+
* on demand (explicit `tmux-ide update --tui-binary`, or a consented first run).
|
|
11
|
+
*
|
|
12
|
+
* The mapping/URL/path helpers are PURE (unit-tested); {@link downloadTuiBinary}
|
|
13
|
+
* and {@link findDownloadedTui} are the thin io that fetch and probe.
|
|
14
|
+
*/
|
|
15
|
+
import { chmodSync, existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { gunzipSync } from "node:zlib";
|
|
19
|
+
import { getCurrentVersion } from "./update-check.js";
|
|
20
|
+
/** The GitHub repo the release assets live under. */
|
|
21
|
+
export const RELEASE_REPO = "wavyrai/tmux-ide";
|
|
22
|
+
/** A downloaded binary smaller than this is treated as corrupt/truncated. */
|
|
23
|
+
export const MIN_TUI_BINARY_BYTES = 10 * 1024 * 1024;
|
|
24
|
+
const SUPPORTED = {
|
|
25
|
+
"darwin-arm64": "darwin-arm64",
|
|
26
|
+
"darwin-x64": "darwin-x64",
|
|
27
|
+
"linux-x64": "linux-x64",
|
|
28
|
+
"linux-arm64": "linux-arm64",
|
|
29
|
+
};
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Pure
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/**
|
|
34
|
+
* PURE — map a Node `process.platform`/`process.arch` pair to the release tag,
|
|
35
|
+
* or null when we don't publish a binary for it (e.g. windows, freebsd) so the
|
|
36
|
+
* caller can fall back to the "install bun" message.
|
|
37
|
+
*/
|
|
38
|
+
export function tuiPlatformTag(platform = process.platform, arch = process.arch) {
|
|
39
|
+
return SUPPORTED[`${platform}-${arch}`] ?? null;
|
|
40
|
+
}
|
|
41
|
+
/** PURE — the `bun build --compile --target` flag for a tag (`bun-<tag>`). */
|
|
42
|
+
export function bunTargetForTag(tag) {
|
|
43
|
+
return `bun-${tag}`;
|
|
44
|
+
}
|
|
45
|
+
/** PURE — the release asset filename for a tag (gzip-compressed binary). */
|
|
46
|
+
export function releaseAssetName(tag) {
|
|
47
|
+
return `tmux-ide-tui-${tag}.gz`;
|
|
48
|
+
}
|
|
49
|
+
/** PURE — strip a leading `v` from a version string; `2.6.1` and `v2.6.1` both → `2.6.1`. */
|
|
50
|
+
export function normalizeVersion(version) {
|
|
51
|
+
return version.startsWith("v") ? version.slice(1) : version;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* PURE — the GitHub download URL for a platform's asset at a given version:
|
|
55
|
+
* `https://github.com/<repo>/releases/download/v<version>/tmux-ide-tui-<tag>.gz`.
|
|
56
|
+
*/
|
|
57
|
+
export function releaseAssetUrl(version, tag) {
|
|
58
|
+
return `https://github.com/${RELEASE_REPO}/releases/download/v${normalizeVersion(version)}/${releaseAssetName(tag)}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* PURE — where a downloaded binary lives: `<home>/bin/tmux-ide-tui-<tag>-<version>`.
|
|
62
|
+
* The version is stamped INTO the name so a `tmux-ide update` to a new version
|
|
63
|
+
* misses the old download and re-fetches (rather than launching a stale binary).
|
|
64
|
+
*/
|
|
65
|
+
export function downloadedTuiPath(home, tag, version) {
|
|
66
|
+
return join(home, "bin", `tmux-ide-tui-${tag}-${normalizeVersion(version)}`);
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// io
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* io — the tmux-ide state home (`TMUX_IDE_HOME` override, else `~/.tmux-ide`),
|
|
73
|
+
* the same resolution the update-check cache and welcome marker use.
|
|
74
|
+
*/
|
|
75
|
+
export function tuiStateHome() {
|
|
76
|
+
return process.env.TMUX_IDE_HOME ?? join(homedir(), ".tmux-ide");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* io — locate a previously downloaded per-platform binary for THIS version and
|
|
80
|
+
* platform, or null. Feeds the resolution order in `tui/compiled.ts` after the
|
|
81
|
+
* shipped/local compiled binary and before the honest "unavailable" error.
|
|
82
|
+
*/
|
|
83
|
+
export function findDownloadedTui(version = getCurrentVersion()) {
|
|
84
|
+
const tag = tuiPlatformTag();
|
|
85
|
+
if (!tag)
|
|
86
|
+
return null;
|
|
87
|
+
const path = downloadedTuiPath(tuiStateHome(), tag, version);
|
|
88
|
+
return existsSync(path) ? path : null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* io — download, verify, and install the per-platform TUI binary for the running
|
|
92
|
+
* version. Fetches the gzip asset, inflates it, rejects anything under
|
|
93
|
+
* {@link MIN_TUI_BINARY_BYTES} (a truncated download or an HTML error page),
|
|
94
|
+
* writes it `0o755`, and atomically renames it into place (temp-in-same-dir →
|
|
95
|
+
* rename, so a crashed download never leaves a half-written executable).
|
|
96
|
+
*
|
|
97
|
+
* Throws with an actionable message on an unsupported platform or a failed
|
|
98
|
+
* fetch. Returns the installed path and byte size.
|
|
99
|
+
*/
|
|
100
|
+
export async function downloadTuiBinary(opts = {}) {
|
|
101
|
+
const log = opts.log ?? (() => { });
|
|
102
|
+
const version = normalizeVersion(opts.version ?? getCurrentVersion());
|
|
103
|
+
const tag = tuiPlatformTag();
|
|
104
|
+
if (!tag) {
|
|
105
|
+
throw new Error(`no prebuilt TUI binary is published for ${process.platform}-${process.arch} — ` +
|
|
106
|
+
`install bun (https://bun.sh) to run the TUI surfaces from source instead`);
|
|
107
|
+
}
|
|
108
|
+
const url = releaseAssetUrl(version, tag);
|
|
109
|
+
const dest = downloadedTuiPath(tuiStateHome(), tag, version);
|
|
110
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
111
|
+
log(`downloading ${url}`);
|
|
112
|
+
const res = await fetch(url);
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
throw new Error(`could not download the TUI binary (${url} → HTTP ${res.status} ${res.statusText}). ` +
|
|
115
|
+
`Check that release v${version} exists and published its assets.`);
|
|
116
|
+
}
|
|
117
|
+
const gz = Buffer.from(await res.arrayBuffer());
|
|
118
|
+
const bin = gunzipSync(gz);
|
|
119
|
+
if (bin.byteLength < MIN_TUI_BINARY_BYTES) {
|
|
120
|
+
throw new Error(`the downloaded TUI binary is only ${bin.byteLength} bytes (expected >10MB) — ` +
|
|
121
|
+
`treating it as corrupt and leaving the previous binary (if any) in place`);
|
|
122
|
+
}
|
|
123
|
+
const tmp = `${dest}.${process.pid}.tmp`;
|
|
124
|
+
writeFileSync(tmp, bin, { mode: 0o755 });
|
|
125
|
+
chmodSync(tmp, 0o755);
|
|
126
|
+
renameSync(tmp, dest);
|
|
127
|
+
const mb = (bin.byteLength / 1024 / 1024).toFixed(1);
|
|
128
|
+
log(`installed ${dest} (${mb} MB)`);
|
|
129
|
+
return { path: dest, bytes: bin.byteLength };
|
|
130
|
+
}
|
|
@@ -6,5 +6,5 @@ interface BreadcrumbsProps {
|
|
|
6
6
|
theme: WidgetTheme;
|
|
7
7
|
onNavigate: (absolutePath: string) => void;
|
|
8
8
|
}
|
|
9
|
-
export declare function Breadcrumbs(props: BreadcrumbsProps):
|
|
9
|
+
export declare function Breadcrumbs(props: BreadcrumbsProps): import("solid-js").JSX.Element;
|
|
10
10
|
export {};
|
|
@@ -9,5 +9,5 @@ interface FileTreeProps {
|
|
|
9
9
|
onActivate: (node: TreeNode) => void;
|
|
10
10
|
onInputModeChange: (mode: "keyboard" | "mouse") => void;
|
|
11
11
|
}
|
|
12
|
-
export declare function FileTree(props: FileTreeProps):
|
|
12
|
+
export declare function FileTree(props: FileTreeProps): import("solid-js").JSX.Element;
|
|
13
13
|
export {};
|
|
@@ -16,4 +16,4 @@ export interface HelpOverlayProps {
|
|
|
16
16
|
* the surface's own keys. Replaces the surface body while `?` is held open;
|
|
17
17
|
* `esc`/`q`/`?` close it.
|
|
18
18
|
*/
|
|
19
|
-
export declare function HelpOverlay(props: HelpOverlayProps):
|
|
19
|
+
export declare function HelpOverlay(props: HelpOverlayProps): import("solid-js").JSX.Element;
|
|
@@ -5,5 +5,5 @@ interface AgentNamingProps {
|
|
|
5
5
|
onContinue: (names: string[]) => void;
|
|
6
6
|
theme: WidgetTheme;
|
|
7
7
|
}
|
|
8
|
-
export declare function AgentNaming(props: AgentNamingProps):
|
|
8
|
+
export declare function AgentNaming(props: AgentNamingProps): import("solid-js").JSX.Element;
|
|
9
9
|
export {};
|
|
@@ -9,5 +9,5 @@ interface ConfigTreeProps {
|
|
|
9
9
|
onConfigChange: (config: IdeConfig) => void;
|
|
10
10
|
theme: WidgetTheme;
|
|
11
11
|
}
|
|
12
|
-
export declare function ConfigTree(props: ConfigTreeProps):
|
|
12
|
+
export declare function ConfigTree(props: ConfigTreeProps): import("solid-js").JSX.Element;
|
|
13
13
|
export {};
|
|
@@ -5,5 +5,5 @@ interface LayoutPickerProps {
|
|
|
5
5
|
onSelect: (preset: LayoutPreset) => void;
|
|
6
6
|
theme: WidgetTheme;
|
|
7
7
|
}
|
|
8
|
-
export declare function LayoutPicker(props: LayoutPickerProps):
|
|
8
|
+
export declare function LayoutPicker(props: LayoutPickerProps): import("solid-js").JSX.Element;
|
|
9
9
|
export {};
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* the target pane.
|
|
15
15
|
*/
|
|
16
16
|
import { execFileSync } from "node:child_process";
|
|
17
|
-
import { explain, type DetectedState } from "./tui/detect/manifest.ts";
|
|
17
|
+
import { explain, type DetectedState, type ManifestConfidence } from "./tui/detect/manifest.ts";
|
|
18
18
|
import {
|
|
19
19
|
classifyInstant,
|
|
20
20
|
parseAuthority,
|
|
@@ -22,7 +22,11 @@ import {
|
|
|
22
22
|
type InstantState,
|
|
23
23
|
} from "./tui/detect/classify.ts";
|
|
24
24
|
import { getManifests } from "./tui/detect/manifest-loader.ts";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
describeSubtree,
|
|
27
|
+
readProcessTable,
|
|
28
|
+
resolveAgentCommand,
|
|
29
|
+
} from "./tui/detect/process-tree.ts";
|
|
26
30
|
import { readPaneSnapshot } from "./tui/detect/snapshot.ts";
|
|
27
31
|
import { IdeError } from "./lib/errors.ts";
|
|
28
32
|
|
|
@@ -53,6 +57,13 @@ export interface ExplainReport {
|
|
|
53
57
|
manifestId: string | null;
|
|
54
58
|
matchedCommand: string;
|
|
55
59
|
source: "hint" | "fast" | "tree" | "none";
|
|
60
|
+
/** The matched manifest's evidence confidence (null when nothing resolved). */
|
|
61
|
+
confidence: ManifestConfidence | null;
|
|
62
|
+
/**
|
|
63
|
+
* Executable-ish tokens seen in the pane's process subtree. Populated for
|
|
64
|
+
* the "no manifest matched" case so the report can say what it DID find.
|
|
65
|
+
*/
|
|
66
|
+
subtree: string[];
|
|
56
67
|
};
|
|
57
68
|
states: Array<{ state: DetectedState; matched: boolean }>;
|
|
58
69
|
winner: DetectedState | null;
|
|
@@ -137,6 +148,9 @@ export function buildReport(target: string): ExplainReport {
|
|
|
137
148
|
hint: info.hintRaw,
|
|
138
149
|
});
|
|
139
150
|
const manifest = resolved.manifest;
|
|
151
|
+
// When nothing resolved, record what the process-tree DID see so `explain`
|
|
152
|
+
// can point the user at an `@agent_hint` target instead of a dead end.
|
|
153
|
+
const subtree = manifest ? [] : describeSubtree(table, info.pid);
|
|
140
154
|
|
|
141
155
|
// Snapshot + per-state explanation.
|
|
142
156
|
const snapshot = { ...readPaneSnapshot(info.id), title: info.title };
|
|
@@ -168,6 +182,8 @@ export function buildReport(target: string): ExplainReport {
|
|
|
168
182
|
manifestId: manifest?.id ?? null,
|
|
169
183
|
matchedCommand: resolved.matchedCommand,
|
|
170
184
|
source: resolved.source,
|
|
185
|
+
confidence: manifest ? (manifest.confidence ?? "conservative") : null,
|
|
186
|
+
subtree,
|
|
171
187
|
},
|
|
172
188
|
states: explained.checked,
|
|
173
189
|
winner: explained.state,
|
|
@@ -223,10 +239,22 @@ export function renderReport(r: ExplainReport, opts: { color?: boolean } = {}):
|
|
|
223
239
|
}
|
|
224
240
|
|
|
225
241
|
// Resolution
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
242
|
+
if (r.resolution.manifestId) {
|
|
243
|
+
const conf =
|
|
244
|
+
r.resolution.confidence === "tuned"
|
|
245
|
+
? c("\x1b[32m", "tuned")
|
|
246
|
+
: dim(r.resolution.confidence ?? "conservative");
|
|
247
|
+
out.push(
|
|
248
|
+
` ${label("manifest")} ${r.resolution.manifestId} ${dim(`via ${r.resolution.source}` + (r.resolution.matchedCommand ? ` "${r.resolution.matchedCommand}"` : ""))} [${conf}]`,
|
|
249
|
+
);
|
|
250
|
+
} else {
|
|
251
|
+
// No manifest matched — say so, and report what the process-tree DID see so
|
|
252
|
+
// the user can set an `@agent_hint` or add a manifest.
|
|
253
|
+
const saw =
|
|
254
|
+
r.resolution.subtree.length > 0 ? r.resolution.subtree.join(", ") : r.pane.cmd || "(nothing)";
|
|
255
|
+
out.push(` ${label("manifest")} ${dim("none matched")} — ${dim(`process-tree saw: ${saw}`)}`);
|
|
256
|
+
out.push(` ${dim("set `tmux set-option -p @agent_hint <agent>` to force one")}`);
|
|
257
|
+
}
|
|
230
258
|
|
|
231
259
|
// Per-state rule results
|
|
232
260
|
out.push("");
|