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.
Files changed (83) hide show
  1. package/README.md +14 -9
  2. package/bin/cli.js +1013 -516
  3. package/bin/cli.ts +62 -5
  4. package/package.json +9 -6
  5. package/packages/daemon/dist/agent-explain.d.ts +8 -1
  6. package/packages/daemon/dist/agent-explain.js +19 -3
  7. package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
  8. package/packages/daemon/dist/lib/tui-binary.js +130 -0
  9. package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
  10. package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
  11. package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
  12. package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
  13. package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
  14. package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
  15. package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
  16. package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
  17. package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
  18. package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
  19. package/packages/daemon/src/agent-explain.ts +34 -6
  20. package/packages/daemon/src/lib/app-config.ts +93 -2
  21. package/packages/daemon/src/lib/tui-binary.ts +165 -0
  22. package/packages/daemon/src/tui/chrome/notify.ts +221 -34
  23. package/packages/daemon/src/tui/chrome/updater.ts +89 -7
  24. package/packages/daemon/src/tui/compiled.ts +11 -3
  25. package/packages/daemon/src/tui/detect/classify.ts +15 -0
  26. package/packages/daemon/src/tui/detect/manifest-loader.ts +2 -1
  27. package/packages/daemon/src/tui/detect/manifest.ts +15 -0
  28. package/packages/daemon/src/tui/detect/manifests.ts +161 -33
  29. package/packages/daemon/src/tui/detect/process-tree.ts +20 -0
  30. package/packages/daemon/src/tui/main.ts +13 -1
  31. package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
  32. package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
  33. package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
  34. package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
  35. package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
  36. package/packages/daemon/src/tui/mirror/blit.ts +186 -0
  37. package/packages/daemon/src/tui/mirror/control-client.ts +80 -9
  38. package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
  39. package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
  40. package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
  41. package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
  42. package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
  43. package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
  44. package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
  45. package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
  46. package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
  47. package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
  48. package/packages/daemon/src/tui/mirror/palette.ts +274 -0
  49. package/packages/daemon/src/tui/mirror/pane-mirror.ts +499 -19
  50. package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
  51. package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
  52. package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
  53. package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
  54. package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
  55. package/packages/daemon/src/tui/mirror/selection.ts +262 -0
  56. package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
  57. package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
  58. package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
  59. package/packages/daemon/src/tui/mirror/spans.ts +46 -0
  60. package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
  61. package/packages/daemon/src/tui/team/entry.ts +34 -7
  62. package/packages/daemon/src/tui/team/report.ts +11 -1
  63. package/packages/daemon/src/tui/team/sessions.ts +102 -13
  64. package/scripts/build-tui.mjs +11 -4
  65. package/scripts/perf-mirror.mjs +313 -0
  66. package/skill/SKILL.md +22 -0
  67. package/templates/AGENTS.md +14 -7
  68. package/templates/agent-team-monorepo.yml +8 -0
  69. package/templates/agent-team-nextjs.yml +8 -0
  70. package/templates/agent-team.yml +10 -0
  71. package/templates/convex.yml +2 -0
  72. package/templates/default.yml +11 -5
  73. package/templates/go.yml +4 -0
  74. package/templates/missions.yml +6 -0
  75. package/templates/nextjs.yml +4 -0
  76. package/templates/python.yml +4 -0
  77. package/templates/skills/backend.md +5 -12
  78. package/templates/skills/frontend.md +5 -12
  79. package/templates/skills/general-worker.md +5 -12
  80. package/templates/skills/researcher.md +7 -12
  81. package/templates/skills/reviewer.md +7 -16
  82. package/templates/vite.yml +4 -0
  83. 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 { shouldOpenCockpit } from "../packages/daemon/src/tui/team/entry.ts";
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 = { ...process.env, TMUX_IDE_CWD: process.cwd(), ...extraEnv };
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
- if (shouldOpenCockpit(hasIdeYml, values.team === true)) {
336
- // No project here (or --team): the cockpit is the front door.
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
- launchTeamCockpit();
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.6.1",
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.1.88",
69
- "@opentui/solid": "^0.1.88",
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": "^1.9.11",
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.1.88"
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
- const mid = r.resolution.manifestId ?? dim("none");
172
- out.push(` ${label("manifest")} ${mid} ${dim(`via ${r.resolution.source}` + (r.resolution.matchedCommand ? ` "${r.resolution.matchedCommand}"` : ""))}`);
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): any;
9
+ export declare function Breadcrumbs(props: BreadcrumbsProps): import("solid-js").JSX.Element;
10
10
  export {};
@@ -2,5 +2,5 @@ import type { WidgetTheme } from "../lib/theme.ts";
2
2
  interface FooterProps {
3
3
  theme: WidgetTheme;
4
4
  }
5
- export declare function Footer(props: FooterProps): any;
5
+ export declare function Footer(props: FooterProps): import("solid-js").JSX.Element;
6
6
  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): any;
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): any;
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): any;
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): any;
12
+ export declare function ConfigTree(props: ConfigTreeProps): import("solid-js").JSX.Element;
13
13
  export {};
@@ -10,5 +10,5 @@ interface DetectPanelProps {
10
10
  onContinue: () => void;
11
11
  theme: WidgetTheme;
12
12
  }
13
- export declare function DetectPanel(props: DetectPanelProps): any;
13
+ export declare function DetectPanel(props: DetectPanelProps): import("solid-js").JSX.Element;
14
14
  export {};
@@ -9,4 +9,4 @@ export interface FieldEditorProps {
9
9
  onCancel: () => void;
10
10
  theme: WidgetTheme;
11
11
  }
12
- export declare function FieldEditor(props: FieldEditorProps): any;
12
+ export declare function FieldEditor(props: FieldEditorProps): import("solid-js").JSX.Element;
@@ -13,5 +13,5 @@ interface FooterProps {
13
13
  theme: WidgetTheme;
14
14
  actions?: FooterActions;
15
15
  }
16
- export declare function Footer(props: FooterProps): any;
16
+ export declare function Footer(props: FooterProps): import("solid-js").JSX.Element;
17
17
  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): any;
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 { readProcessTable, resolveAgentCommand } from "./tui/detect/process-tree.ts";
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
- const mid = r.resolution.manifestId ?? dim("none");
227
- out.push(
228
- ` ${label("manifest")} ${mid} ${dim(`via ${r.resolution.source}` + (r.resolution.matchedCommand ? ` "${r.resolution.matchedCommand}"` : ""))}`,
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("");