tmux-ide 2.6.0 → 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 (271) hide show
  1. package/README.md +14 -9
  2. package/bin/cli.js +1024 -519
  3. package/bin/cli.ts +63 -6
  4. package/bunfig.toml +4 -0
  5. package/package.json +18 -7
  6. package/packages/contracts/package.json +22 -0
  7. package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
  8. package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
  9. package/packages/contracts/src/actions-contract.ts +310 -0
  10. package/packages/contracts/src/actions-errors.ts +41 -0
  11. package/packages/contracts/src/domain.ts +36 -0
  12. package/packages/contracts/src/ide-config.ts +170 -0
  13. package/packages/contracts/src/index.ts +24 -0
  14. package/packages/contracts/src/lib-internal/auth.ts +13 -0
  15. package/packages/contracts/src/lib-internal/hq.ts +38 -0
  16. package/packages/contracts/src/terminals.ts +116 -0
  17. package/packages/contracts/src/tmux.ts +60 -0
  18. package/packages/contracts/src/workspace.ts +67 -0
  19. package/packages/daemon/dist/agent-explain.d.ts +8 -1
  20. package/packages/daemon/dist/agent-explain.js +19 -3
  21. package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
  22. package/packages/daemon/dist/lib/tui-binary.js +130 -0
  23. package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
  24. package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
  25. package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
  26. package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
  27. package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
  28. package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
  29. package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
  30. package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
  31. package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
  32. package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
  33. package/packages/daemon/src/agent-explain.ts +298 -0
  34. package/packages/daemon/src/attach.ts +20 -0
  35. package/packages/daemon/src/bin.ts +4 -0
  36. package/packages/daemon/src/canonical.ts +7 -0
  37. package/packages/daemon/src/cli.ts +499 -0
  38. package/packages/daemon/src/command-center/actions/contract.ts +2 -0
  39. package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
  40. package/packages/daemon/src/command-center/actions/errors.ts +105 -0
  41. package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
  42. package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
  43. package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
  44. package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
  45. package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
  46. package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
  47. package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
  48. package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
  49. package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
  50. package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
  51. package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
  52. package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
  53. package/packages/daemon/src/command-center/actions/registry.ts +149 -0
  54. package/packages/daemon/src/command-center/discovery.ts +96 -0
  55. package/packages/daemon/src/command-center/index.ts +31 -0
  56. package/packages/daemon/src/command-center/schemas.ts +85 -0
  57. package/packages/daemon/src/command-center/server.ts +1260 -0
  58. package/packages/daemon/src/command-center/ws-events.ts +316 -0
  59. package/packages/daemon/src/config.ts +549 -0
  60. package/packages/daemon/src/detect.ts +248 -0
  61. package/packages/daemon/src/doctor.ts +242 -0
  62. package/packages/daemon/src/embed.ts +5 -0
  63. package/packages/daemon/src/index.ts +12 -0
  64. package/packages/daemon/src/init.ts +211 -0
  65. package/packages/daemon/src/inspect.ts +178 -0
  66. package/packages/daemon/src/js-yaml.d.ts +10 -0
  67. package/packages/daemon/src/launch.ts +349 -0
  68. package/packages/daemon/src/lib/active-projects.ts +49 -0
  69. package/packages/daemon/src/lib/agent-discovery.ts +121 -0
  70. package/packages/daemon/src/lib/app-config.ts +427 -0
  71. package/packages/daemon/src/lib/app-settings.ts +53 -0
  72. package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
  73. package/packages/daemon/src/lib/auth/middleware.ts +56 -0
  74. package/packages/daemon/src/lib/auth/types.ts +2 -0
  75. package/packages/daemon/src/lib/auth-token.ts +5 -0
  76. package/packages/daemon/src/lib/authorship.ts +280 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
  78. package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
  79. package/packages/daemon/src/lib/daemon-embed.ts +782 -0
  80. package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
  81. package/packages/daemon/src/lib/daemon.ts +79 -0
  82. package/packages/daemon/src/lib/dot-path.ts +17 -0
  83. package/packages/daemon/src/lib/errors.ts +67 -0
  84. package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
  85. package/packages/daemon/src/lib/launch-plan.ts +90 -0
  86. package/packages/daemon/src/lib/log.ts +134 -0
  87. package/packages/daemon/src/lib/output.ts +76 -0
  88. package/packages/daemon/src/lib/project-init-runner.ts +150 -0
  89. package/packages/daemon/src/lib/project-inspect.ts +80 -0
  90. package/packages/daemon/src/lib/project-onboard.ts +149 -0
  91. package/packages/daemon/src/lib/project-probe.ts +92 -0
  92. package/packages/daemon/src/lib/project-registry.ts +296 -0
  93. package/packages/daemon/src/lib/session-monitor.ts +122 -0
  94. package/packages/daemon/src/lib/session-options.ts +100 -0
  95. package/packages/daemon/src/lib/shell.ts +8 -0
  96. package/packages/daemon/src/lib/sizes.ts +36 -0
  97. package/packages/daemon/src/lib/skill-sync.ts +155 -0
  98. package/packages/daemon/src/lib/slugify.ts +10 -0
  99. package/packages/daemon/src/lib/terminals-store.ts +125 -0
  100. package/packages/daemon/src/lib/tui-binary.ts +165 -0
  101. package/packages/daemon/src/lib/update-check.ts +298 -0
  102. package/packages/daemon/src/lib/update.ts +158 -0
  103. package/packages/daemon/src/lib/workspace-registry.ts +229 -0
  104. package/packages/daemon/src/lib/worktree.ts +289 -0
  105. package/packages/daemon/src/lib/yaml-io.ts +27 -0
  106. package/packages/daemon/src/ls.ts +40 -0
  107. package/packages/daemon/src/restart.ts +24 -0
  108. package/packages/daemon/src/restore.ts +514 -0
  109. package/packages/daemon/src/schemas/domain.ts +2 -0
  110. package/packages/daemon/src/schemas/filesystem.ts +34 -0
  111. package/packages/daemon/src/schemas/ide-config.ts +2 -0
  112. package/packages/daemon/src/schemas/index.ts +59 -0
  113. package/packages/daemon/src/schemas/inspect.ts +67 -0
  114. package/packages/daemon/src/schemas/registry.ts +55 -0
  115. package/packages/daemon/src/schemas/ws-events.ts +135 -0
  116. package/packages/daemon/src/send.ts +171 -0
  117. package/packages/daemon/src/server/README.md +15 -0
  118. package/packages/daemon/src/server/index.ts +74 -0
  119. package/packages/daemon/src/server/pty-bridge.ts +532 -0
  120. package/packages/daemon/src/server/standalone.ts +18 -0
  121. package/packages/daemon/src/server/ws-route.ts +483 -0
  122. package/packages/daemon/src/status.ts +59 -0
  123. package/packages/daemon/src/stop.ts +28 -0
  124. package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
  125. package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
  126. package/packages/daemon/src/terminal/README.md +92 -0
  127. package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
  128. package/packages/daemon/src/tui/chrome/chip.ts +30 -0
  129. package/packages/daemon/src/tui/chrome/events.ts +119 -0
  130. package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
  131. package/packages/daemon/src/tui/chrome/menu.ts +289 -0
  132. package/packages/daemon/src/tui/chrome/notify.ts +382 -0
  133. package/packages/daemon/src/tui/chrome/panels.ts +111 -0
  134. package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
  135. package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
  136. package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
  137. package/packages/daemon/src/tui/chrome/updater.ts +510 -0
  138. package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
  139. package/packages/daemon/src/tui/compiled.ts +121 -0
  140. package/packages/daemon/src/tui/detect/classify.ts +208 -0
  141. package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
  142. package/packages/daemon/src/tui/detect/manifest.ts +199 -0
  143. package/packages/daemon/src/tui/detect/manifests.ts +354 -0
  144. package/packages/daemon/src/tui/detect/process-tree.ts +217 -0
  145. package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
  146. package/packages/daemon/src/tui/integrations/claude.ts +176 -0
  147. package/packages/daemon/src/tui/integrations/offer.ts +145 -0
  148. package/packages/daemon/src/tui/main.ts +82 -0
  149. package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
  150. package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
  151. package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
  152. package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
  153. package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
  154. package/packages/daemon/src/tui/mirror/blit.ts +186 -0
  155. package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
  156. package/packages/daemon/src/tui/mirror/control.ts +97 -0
  157. package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
  158. package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
  159. package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
  160. package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
  161. package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
  162. package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
  163. package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
  164. package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
  165. package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
  166. package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
  167. package/packages/daemon/src/tui/mirror/palette.ts +274 -0
  168. package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
  169. package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
  170. package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
  171. package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
  172. package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
  173. package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
  174. package/packages/daemon/src/tui/mirror/selection.ts +262 -0
  175. package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
  176. package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
  177. package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
  178. package/packages/daemon/src/tui/mirror/spans.ts +46 -0
  179. package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
  180. package/packages/daemon/src/tui/team/CONTROL.md +50 -0
  181. package/packages/daemon/src/tui/team/entry.ts +38 -0
  182. package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
  183. package/packages/daemon/src/tui/team/home.ts +170 -0
  184. package/packages/daemon/src/tui/team/index.tsx +1521 -0
  185. package/packages/daemon/src/tui/team/input.ts +34 -0
  186. package/packages/daemon/src/tui/team/keymap.ts +127 -0
  187. package/packages/daemon/src/tui/team/mouse.ts +29 -0
  188. package/packages/daemon/src/tui/team/nav.ts +31 -0
  189. package/packages/daemon/src/tui/team/preview.ts +34 -0
  190. package/packages/daemon/src/tui/team/projects.ts +191 -0
  191. package/packages/daemon/src/tui/team/report.ts +83 -0
  192. package/packages/daemon/src/tui/team/sessions.ts +433 -0
  193. package/packages/daemon/src/tui/team/tree.ts +62 -0
  194. package/packages/daemon/src/types.ts +13 -0
  195. package/packages/daemon/src/ui/index.ts +32 -0
  196. package/packages/daemon/src/ui/terminal/index.ts +9 -0
  197. package/packages/daemon/src/ui/types.ts +91 -0
  198. package/packages/daemon/src/ui/web/base.css +80 -0
  199. package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
  200. package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
  201. package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
  202. package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
  203. package/packages/daemon/src/ui/web/hooks.ts +106 -0
  204. package/packages/daemon/src/ui/web/index.ts +27 -0
  205. package/packages/daemon/src/ui/web/render.ts +77 -0
  206. package/packages/daemon/src/ui/web/utils/color.ts +27 -0
  207. package/packages/daemon/src/validate.ts +217 -0
  208. package/packages/daemon/src/widgets/changes/README.md +3 -0
  209. package/packages/daemon/src/widgets/changes/index.tsx +691 -0
  210. package/packages/daemon/src/widgets/config/README.md +3 -0
  211. package/packages/daemon/src/widgets/config/index.tsx +481 -0
  212. package/packages/daemon/src/widgets/explorer/README.md +3 -0
  213. package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
  214. package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
  215. package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
  216. package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
  217. package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
  218. package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
  219. package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
  220. package/packages/daemon/src/widgets/lib/files.ts +88 -0
  221. package/packages/daemon/src/widgets/lib/git.ts +88 -0
  222. package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
  223. package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
  224. package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
  225. package/packages/daemon/src/widgets/lib/theme.ts +194 -0
  226. package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
  227. package/packages/daemon/src/widgets/preview/README.md +3 -0
  228. package/packages/daemon/src/widgets/preview/index.tsx +416 -0
  229. package/packages/daemon/src/widgets/resolve.ts +121 -0
  230. package/packages/daemon/src/widgets/setup/README.md +3 -0
  231. package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
  232. package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
  233. package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
  234. package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
  235. package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
  236. package/packages/daemon/src/widgets/setup/index.tsx +341 -0
  237. package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
  238. package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
  239. package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
  240. package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
  241. package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
  242. package/packages/tmux-bridge/package.json +22 -0
  243. package/packages/tmux-bridge/src/errors.ts +28 -0
  244. package/packages/tmux-bridge/src/index.ts +31 -0
  245. package/packages/tmux-bridge/src/monitor.ts +77 -0
  246. package/packages/tmux-bridge/src/panes.ts +136 -0
  247. package/packages/tmux-bridge/src/runner.test.ts +501 -0
  248. package/packages/tmux-bridge/src/runner.ts +91 -0
  249. package/packages/tmux-bridge/src/sessions.ts +126 -0
  250. package/packages/tmux-bridge/src/targeting.test.ts +107 -0
  251. package/packages/tmux-bridge/src/targeting.ts +90 -0
  252. package/scripts/build-tui.mjs +11 -4
  253. package/scripts/perf-mirror.mjs +313 -0
  254. package/scripts/postinstall.js +26 -2
  255. package/skill/SKILL.md +22 -0
  256. package/templates/AGENTS.md +14 -7
  257. package/templates/agent-team-monorepo.yml +8 -0
  258. package/templates/agent-team-nextjs.yml +8 -0
  259. package/templates/agent-team.yml +10 -0
  260. package/templates/convex.yml +2 -0
  261. package/templates/default.yml +11 -5
  262. package/templates/go.yml +4 -0
  263. package/templates/missions.yml +6 -0
  264. package/templates/nextjs.yml +4 -0
  265. package/templates/python.yml +4 -0
  266. package/templates/skills/backend.md +5 -12
  267. package/templates/skills/frontend.md +5 -12
  268. package/templates/skills/general-worker.md +5 -12
  269. package/templates/skills/researcher.md +7 -12
  270. package/templates/skills/reviewer.md +7 -16
  271. package/templates/vite.yml +4 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Resolution for the TUI surfaces across BOTH distribution modes.
3
+ *
4
+ * Dev checkout: the surfaces are OpenTUI/Solid `.tsx` spawned by `bun` (the
5
+ * bunfig preload supplies the JSX transform). Installed via npm/pnpm/bun: there
6
+ * is no checkout and no `bun`, so we fall back to the compiled `tmux-ide-tui`
7
+ * binary (see scripts/build-tui.mjs) which bundles every surface behind a
8
+ * `<surface> [flags]` argv dispatcher and needs no runtime.
9
+ *
10
+ * Order is "checkout first, binary second": a dev machine that happens to have
11
+ * built the binary still uses its live `.tsx` sources. The binary is consulted
12
+ * only when the checkout sources or `bun` are missing. {@link findCompiledTui}
13
+ * probes the shipped/local compiled binary first, then a per-platform binary
14
+ * downloaded at runtime into `~/.tmux-ide/bin/` (see lib/tui-binary.ts).
15
+ *
16
+ * {@link resolveTuiLaunch} is a PURE decision (unit-tested); {@link findCompiledTui}
17
+ * and {@link isBunAvailable} are the thin io probes that feed it.
18
+ */
19
+ import { existsSync } from "node:fs";
20
+ import { dirname, resolve } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { execFileSync } from "node:child_process";
23
+ import { findDownloadedTui } from "../lib/tui-binary.ts";
24
+
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+
27
+ export type TuiLaunch =
28
+ | { mode: "bun"; bin: "bun"; argv: string[] }
29
+ | { mode: "binary"; bin: string; argv: string[] }
30
+ | { mode: "unavailable"; reasons: string[] };
31
+
32
+ export interface TuiResolveInput {
33
+ /** Dispatcher surface token: team | explorer | changes | preview | config | setup | sidebar. */
34
+ surface: string;
35
+ /** The checkout `.tsx` entry for this surface. */
36
+ scriptPath: string;
37
+ /** Surface flags (`--session=…`, `--dir=…`, `--theme=…`, …). */
38
+ args: string[];
39
+ /** Whether {@link scriptPath} exists (checkout present). */
40
+ checkoutExists: boolean;
41
+ /** Whether the `bun` runtime is on PATH. */
42
+ bunAvailable: boolean;
43
+ /** Absolute path to the compiled `tmux-ide-tui`, or null if not found. */
44
+ compiledBinary: string | null;
45
+ }
46
+
47
+ /**
48
+ * PURE — decide how to launch a surface. Bun-from-checkout wins when both are
49
+ * present (dev); the compiled binary is the installed fallback; otherwise the
50
+ * caller surfaces an actionable message built from `reasons`.
51
+ */
52
+ export function resolveTuiLaunch(input: TuiResolveInput): TuiLaunch {
53
+ if (input.checkoutExists && input.bunAvailable) {
54
+ return { mode: "bun", bin: "bun", argv: [input.scriptPath, ...input.args] };
55
+ }
56
+ if (input.compiledBinary) {
57
+ return { mode: "binary", bin: input.compiledBinary, argv: [input.surface, ...input.args] };
58
+ }
59
+ const reasons: string[] = [];
60
+ if (!input.checkoutExists) {
61
+ reasons.push(
62
+ "the TUI widget sources are absent (reinstall tmux-ide — releases since v2.6.1 ship them)",
63
+ );
64
+ }
65
+ if (!input.bunAvailable) {
66
+ reasons.push("the `bun` runtime is not installed (https://bun.sh)");
67
+ }
68
+ reasons.push(
69
+ "no compiled `tmux-ide-tui` binary was found (build one with `pnpm build:tui`, download it with `tmux-ide update --tui-binary`, or reinstall a release that ships it)",
70
+ );
71
+ return { mode: "unavailable", reasons };
72
+ }
73
+
74
+ // Candidate locations for the compiled binary, relative to an anchor dir. The
75
+ // installed tarball ships it at packages/daemon/dist/tui/tmux-ide-tui and the
76
+ // bin is bin/cli.js, so a bin-anchored `../packages/daemon/dist/...` hits it;
77
+ // the other rels cover unbundled daemon layouts and a co-located binary.
78
+ const BINARY_RELS = [
79
+ "../packages/daemon/dist/tui/tmux-ide-tui",
80
+ "../../dist/tui/tmux-ide-tui",
81
+ "../dist/tui/tmux-ide-tui",
82
+ "dist/tui/tmux-ide-tui",
83
+ "tmux-ide-tui",
84
+ ];
85
+
86
+ /**
87
+ * io — locate the compiled `tmux-ide-tui`. Honors `TMUX_IDE_TUI_BIN` (absolute
88
+ * override, e.g. for tests / custom installs), then probes bin- and
89
+ * module-relative candidates. Returns null when none exists.
90
+ */
91
+ export function findCompiledTui(): string | null {
92
+ const override = process.env.TMUX_IDE_TUI_BIN;
93
+ if (override) return existsSync(override) ? override : null;
94
+
95
+ const anchors: string[] = [];
96
+ if (process.argv[1]) anchors.push(dirname(process.argv[1]));
97
+ anchors.push(__dirname);
98
+
99
+ for (const anchor of anchors) {
100
+ for (const rel of BINARY_RELS) {
101
+ const candidate = resolve(anchor, rel);
102
+ if (existsSync(candidate)) return candidate;
103
+ }
104
+ }
105
+
106
+ // Last: a per-platform binary fetched at runtime by `tmux-ide update
107
+ // --tui-binary` into `~/.tmux-ide/bin/` — the fallback for an npm install with
108
+ // no bun and no shipped compiled binary. Version-stamped, so it only matches
109
+ // the running version (see lib/tui-binary.ts).
110
+ return findDownloadedTui();
111
+ }
112
+
113
+ /** io — is the `bun` runtime callable? */
114
+ export function isBunAvailable(): boolean {
115
+ try {
116
+ execFileSync("bun", ["--version"], { stdio: "ignore" });
117
+ return true;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * The 4-state agent classifier.
3
+ *
4
+ * Turns snapshot + manifest evidence into a final per-pane status. The
5
+ * instantaneous half (`classifyInstant`) is pure: it maps a single snapshot
6
+ * through a manifest to blocked/working/idle/unknown. The `done` state,
7
+ * however, is inherently cross-tick — an agent is "done" only when it WAS
8
+ * working and has since gone quiet without being viewed. That temporal logic
9
+ * lives in the small stateful `StatusTracker`, which layers `done` + seen
10
+ * acknowledgement on top of the instantaneous state.
11
+ */
12
+ import type { AgentManifest } from "./manifest.ts";
13
+ import { evaluateManifest, pickManifest } from "./manifest.ts";
14
+ import { BUNDLED_MANIFESTS } from "./manifests.ts";
15
+ import type { PaneSnapshot } from "./snapshot.ts";
16
+
17
+ /** The final per-pane status surfaced to the TUI. */
18
+ export type AgentStatus = "blocked" | "working" | "done" | "idle" | "unknown";
19
+
20
+ /** The instantaneous status derivable from a single snapshot (no history). */
21
+ export type InstantState = "blocked" | "working" | "idle" | "unknown";
22
+
23
+ /**
24
+ * Working/blocked reports older than this are considered stale — the agent
25
+ * (or its hook) probably died mid-turn, so we fall back to screen scraping.
26
+ * Done/idle are terminal until the next event and never go stale.
27
+ */
28
+ const AUTHORITY_STALE_SECONDS = 600;
29
+
30
+ const AUTHORITY_STATES = new Set(["working", "blocked", "done", "idle"]);
31
+
32
+ /**
33
+ * Parse an authoritative `@agent_state` pane option (`"<state>:<epoch>"`,
34
+ * written by lifecycle-hook integrations or any self-reporting agent).
35
+ * PURE — returns the reported status, or null when the value is absent,
36
+ * malformed, or stale (working/blocked past {@link AUTHORITY_STALE_SECONDS}).
37
+ * A fresh authority report OUTRANKS screen-manifest scraping.
38
+ */
39
+ export function parseAuthority(raw: string | undefined, nowSec: number): AgentStatus | null {
40
+ if (!raw) return null;
41
+ const sep = raw.lastIndexOf(":");
42
+ if (sep === -1) return null;
43
+ const state = raw.slice(0, sep);
44
+ const epoch = Number(raw.slice(sep + 1));
45
+ if (!AUTHORITY_STATES.has(state) || !Number.isFinite(epoch)) return null;
46
+ if ((state === "working" || state === "blocked") && nowSec - epoch > AUTHORITY_STALE_SECONDS) {
47
+ return null;
48
+ }
49
+ return state as AgentStatus;
50
+ }
51
+
52
+ /**
53
+ * Extract the epoch STAMP from an `@agent_state` value (`"<state>:<epoch>"`) —
54
+ * the "since" timestamp of the reported state. PURE — returns the epoch, or
55
+ * null when the value is absent or its stamp isn't a finite number. Staleness
56
+ * is NOT considered here (that's {@link parseAuthority}'s job); callers pair
57
+ * this with a fresh `parseAuthority` result to surface a state's `since`.
58
+ */
59
+ export function parseAuthorityEpoch(raw: string | undefined): number | null {
60
+ if (!raw) return null;
61
+ const sep = raw.lastIndexOf(":");
62
+ if (sep === -1) return null;
63
+ const epoch = Number(raw.slice(sep + 1));
64
+ return Number.isFinite(epoch) ? epoch : null;
65
+ }
66
+
67
+ /**
68
+ * Classify a single snapshot against a manifest — PURE, never throws.
69
+ *
70
+ * - no manifest → `"unknown"` (we can't reason about an unrecognized command)
71
+ * - manifest reports `blocked` → `"blocked"`
72
+ * - manifest reports `working` → `"working"`
73
+ * - manifest reports `done` or nothing → `"idle"`
74
+ *
75
+ * A manifest-detected `done` is deliberately collapsed to `idle` here; the
76
+ * real, cross-tick `done` is produced by {@link StatusTracker}.
77
+ */
78
+ export function classifyInstant(
79
+ snapshot: PaneSnapshot & { title?: string },
80
+ manifest: AgentManifest | undefined,
81
+ ): InstantState {
82
+ if (!manifest) return "unknown";
83
+
84
+ const { state } = evaluateManifest(snapshot, manifest);
85
+ switch (state) {
86
+ case "blocked":
87
+ return "blocked";
88
+ case "working":
89
+ return "working";
90
+ // "done" (instantaneous) and null both fall through to idle.
91
+ default:
92
+ return "idle";
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Convenience wrapper: pick the manifest for a pane command, then classify.
98
+ * Returns `"unknown"` when no manifest applies to the command.
99
+ */
100
+ export function classifyPaneCommand(
101
+ snapshot: PaneSnapshot & { title?: string },
102
+ command: string,
103
+ manifests: AgentManifest[] = BUNDLED_MANIFESTS,
104
+ ): InstantState {
105
+ return classifyInstant(snapshot, pickManifest(command, manifests));
106
+ }
107
+
108
+ /** Per-pane bookkeeping the tracker keeps between ticks. */
109
+ interface PaneState {
110
+ /** Whether the previous tick observed `working`. */
111
+ wasWorking: boolean;
112
+ /** A working→idle transition happened and hasn't been acknowledged. */
113
+ doneUnseen: boolean;
114
+ }
115
+
116
+ /**
117
+ * Stateful layer that adds `done` + seen-acknowledgement on top of the
118
+ * instantaneous state. Deterministic given its call sequence.
119
+ */
120
+ export interface StatusTracker {
121
+ /**
122
+ * Fold a fresh instantaneous state into the pane's history and return the
123
+ * final status. `opts.seen` marks the pane as currently viewed/attached,
124
+ * which acknowledges (and suppresses) a pending `done`.
125
+ */
126
+ update(paneId: string, instant: InstantState, opts?: { seen?: boolean }): AgentStatus;
127
+ /** Acknowledge a pane — clears any pending `done`. */
128
+ markSeen(paneId: string): void;
129
+ /** Drop a pane's state entirely (e.g. its session closed). */
130
+ forget(paneId: string): void;
131
+ }
132
+
133
+ /**
134
+ * Create a {@link StatusTracker}.
135
+ *
136
+ * State transitions (given the previous tick's bookkeeping):
137
+ * - `working` → clear `doneUnseen`, remember working → `"working"`.
138
+ * - `blocked` → clear `doneUnseen` (blocked outranks a pending done) →
139
+ * `"blocked"`.
140
+ * - `idle`:
141
+ * - a working→idle transition sets `doneUnseen`.
142
+ * - `doneUnseen` && not seen → `"done"`.
143
+ * - otherwise → `"idle"`.
144
+ * - `unknown` → `"unknown"` (leaves `doneUnseen` untouched).
145
+ *
146
+ * `opts.seen === true` always clears `doneUnseen`, and downgrades a would-be
147
+ * `done` to `"idle"` — seeing a finished pane acknowledges it.
148
+ */
149
+ export function createStatusTracker(): StatusTracker {
150
+ const states = new Map<string, PaneState>();
151
+
152
+ function get(paneId: string): PaneState {
153
+ let s = states.get(paneId);
154
+ if (!s) {
155
+ s = { wasWorking: false, doneUnseen: false };
156
+ states.set(paneId, s);
157
+ }
158
+ return s;
159
+ }
160
+
161
+ return {
162
+ update(paneId, instant, opts) {
163
+ const seen = opts?.seen === true;
164
+ const s = get(paneId);
165
+
166
+ switch (instant) {
167
+ case "working":
168
+ s.doneUnseen = false;
169
+ s.wasWorking = true;
170
+ return "working";
171
+
172
+ case "blocked":
173
+ s.doneUnseen = false;
174
+ s.wasWorking = false;
175
+ return "blocked";
176
+
177
+ case "idle": {
178
+ // A working→idle transition means the agent just finished.
179
+ if (s.wasWorking) s.doneUnseen = true;
180
+ s.wasWorking = false;
181
+
182
+ // Viewing the pane acknowledges any pending done.
183
+ if (seen) {
184
+ s.doneUnseen = false;
185
+ return "idle";
186
+ }
187
+ return s.doneUnseen ? "done" : "idle";
188
+ }
189
+
190
+ case "unknown":
191
+ default:
192
+ // Leave doneUnseen untouched; unknown is not a transition signal.
193
+ s.wasWorking = false;
194
+ if (seen) s.doneUnseen = false;
195
+ return "unknown";
196
+ }
197
+ },
198
+
199
+ markSeen(paneId) {
200
+ const s = states.get(paneId);
201
+ if (s) s.doneUnseen = false;
202
+ },
203
+
204
+ forget(paneId) {
205
+ states.delete(paneId);
206
+ },
207
+ };
208
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Manifest loading with user overrides.
3
+ *
4
+ * The bundled manifests (`manifests.ts`) cover the agents we ship tuning for.
5
+ * Users can add or replace tuning WITHOUT a code change by dropping JSON files
6
+ * in `~/.tmux-ide/agent-detection/` — one {@link AgentManifest} per file:
7
+ *
8
+ * ~/.tmux-ide/agent-detection/my-agent.json
9
+ * {
10
+ * "id": "my-agent",
11
+ * "commands": ["my-agent", "my-agent-cli"],
12
+ * "states": {
13
+ * "working": { "any": [{ "contains": "esc to interrupt", "caseInsensitive": true }] },
14
+ * "blocked": { "any": [{ "contains": "(y/n)", "caseInsensitive": true }] }
15
+ * }
16
+ * }
17
+ *
18
+ * An override whose `id` matches a bundled manifest REPLACES it (so you can
19
+ * re-tune claude/codex); a new `id` is APPENDED. Invalid files are skipped with
20
+ * a one-time stderr warning — a typo in one override never breaks detection.
21
+ *
22
+ * Related: a per-pane `@agent_hint` tmux option (e.g.
23
+ * `tmux set-option -p @agent_hint claude`) forces a specific manifest for a
24
+ * pane, bypassing process-tree resolution — see `resolveAgentCommand`. That is
25
+ * the escape hatch for sandboxes/wrappers where the process tree is opaque.
26
+ */
27
+ import { readdirSync, readFileSync } from "node:fs";
28
+ import { homedir } from "node:os";
29
+ import { join } from "node:path";
30
+ import type { AgentManifest, Rule, StateRules } from "./manifest.ts";
31
+ import { BUNDLED_MANIFESTS } from "./manifests.ts";
32
+
33
+ /** Directory scanned for user override manifests. */
34
+ export function overrideDir(): string {
35
+ return join(homedir(), ".tmux-ide", "agent-detection");
36
+ }
37
+
38
+ /**
39
+ * Light structural validator — enough to reject a malformed override without a
40
+ * full schema. Requires a non-empty `id`, a non-empty `commands: string[]`, and
41
+ * a `states` object whose present state rules are shaped `{ all?: [], any?: [] }`
42
+ * with matcher-like entries. PURE — never throws.
43
+ */
44
+ export function validateManifestShape(value: unknown): value is AgentManifest {
45
+ if (typeof value !== "object" || value === null) return false;
46
+ const m = value as Record<string, unknown>;
47
+
48
+ if (typeof m.id !== "string" || m.id.trim().length === 0) return false;
49
+
50
+ if (!Array.isArray(m.commands) || m.commands.length === 0) return false;
51
+ if (!m.commands.every((c) => typeof c === "string" && c.length > 0)) return false;
52
+
53
+ if (typeof m.states !== "object" || m.states === null) return false;
54
+ const states = m.states as Record<string, unknown>;
55
+ for (const key of ["blocked", "working", "done"] as const) {
56
+ if (!(key in states)) continue;
57
+ if (!isRuleShape(states[key])) return false;
58
+ }
59
+ return true;
60
+ }
61
+
62
+ /** A rule is `{ all?: Matcher[]; any?: Matcher[] }` with matcher-shaped entries. */
63
+ function isRuleShape(value: unknown): value is Rule {
64
+ if (typeof value !== "object" || value === null) return false;
65
+ const r = value as Record<string, unknown>;
66
+ for (const key of ["all", "any"] as const) {
67
+ if (!(key in r)) continue;
68
+ const arr = r[key];
69
+ if (!Array.isArray(arr) || !arr.every(isMatcherShape)) return false;
70
+ }
71
+ return true;
72
+ }
73
+
74
+ /** A matcher must carry a string `contains` or `regex` (the two probe forms). */
75
+ function isMatcherShape(value: unknown): boolean {
76
+ if (typeof value !== "object" || value === null) return false;
77
+ const m = value as Record<string, unknown>;
78
+ return typeof m.contains === "string" || typeof m.regex === "string";
79
+ }
80
+
81
+ /**
82
+ * Merge bundled manifests with user overrides. PURE.
83
+ *
84
+ * - An override whose `id` matches a bundled manifest REPLACES that entry
85
+ * in-place (position preserved, so pickManifest/tree priority is stable).
86
+ * - A later override with a not-yet-seen `id` is APPENDED (in encounter order).
87
+ * - Among overrides sharing an `id`, the LAST one wins.
88
+ */
89
+ export function mergeManifests(
90
+ bundled: AgentManifest[],
91
+ overrides: AgentManifest[],
92
+ ): AgentManifest[] {
93
+ const byId = new Map<string, AgentManifest>();
94
+ for (const o of overrides) byId.set(o.id, o);
95
+
96
+ const result: AgentManifest[] = [];
97
+ const consumed = new Set<string>();
98
+
99
+ for (const b of bundled) {
100
+ const override = byId.get(b.id);
101
+ if (override) {
102
+ result.push(override);
103
+ consumed.add(b.id);
104
+ } else {
105
+ result.push(b);
106
+ }
107
+ }
108
+ // Append new ids in the order first encountered — but use the last-wins entry
109
+ // from `byId`, so a duplicate id still resolves to the final override.
110
+ for (const o of overrides) {
111
+ if (!consumed.has(o.id)) {
112
+ result.push(byId.get(o.id)!);
113
+ consumed.add(o.id);
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+
119
+ /** Paths we have already warned about, so a bad override warns only once. */
120
+ const warned = new Set<string>();
121
+
122
+ /**
123
+ * Read and validate override manifests from `dir`. Thin io wrapper — a missing
124
+ * directory yields `[]`, and each invalid/malformed file is skipped with a
125
+ * one-time stderr warning. Never throws.
126
+ */
127
+ export function readOverrideManifests(dir = overrideDir()): AgentManifest[] {
128
+ let files: string[];
129
+ try {
130
+ files = readdirSync(dir).filter((f) => f.endsWith(".json"));
131
+ } catch {
132
+ // Missing directory (the common case) → no overrides.
133
+ return [];
134
+ }
135
+
136
+ const overrides: AgentManifest[] = [];
137
+ for (const file of files.sort()) {
138
+ const path = join(dir, file);
139
+ try {
140
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
141
+ if (validateManifestShape(parsed)) {
142
+ overrides.push(normalizeStates(parsed));
143
+ } else {
144
+ warnOnce(path, "not a valid AgentManifest (need id, commands[], states)");
145
+ }
146
+ } catch (err) {
147
+ warnOnce(path, err instanceof Error ? err.message : String(err));
148
+ }
149
+ }
150
+ return overrides;
151
+ }
152
+
153
+ /** Drop unknown state keys — keep only blocked/working/done. */
154
+ function normalizeStates(m: AgentManifest): AgentManifest {
155
+ const states: StateRules = {};
156
+ if (m.states.blocked) states.blocked = m.states.blocked;
157
+ if (m.states.working) states.working = m.states.working;
158
+ if (m.states.done) states.done = m.states.done;
159
+ const confidence = m.confidence === "tuned" ? "tuned" : "conservative";
160
+ return { id: m.id, commands: m.commands, states, confidence };
161
+ }
162
+
163
+ function warnOnce(path: string, reason: string): void {
164
+ if (warned.has(path)) return;
165
+ warned.add(path);
166
+ process.stderr.write(`tmux-ide: skipping agent-detection override ${path}: ${reason}\n`);
167
+ }
168
+
169
+ /**
170
+ * Load the full manifest set: bundled + user overrides, merged. Does io on
171
+ * every call — prefer {@link getManifests} for hot paths.
172
+ */
173
+ export function loadManifests(): AgentManifest[] {
174
+ return mergeManifests(BUNDLED_MANIFESTS, readOverrideManifests());
175
+ }
176
+
177
+ /** Process-lifetime cache for the merged manifest set. */
178
+ let cache: AgentManifest[] | undefined;
179
+
180
+ /**
181
+ * Cached {@link loadManifests}. The override directory is read once per
182
+ * process; call {@link _resetForTests} to force a re-read.
183
+ */
184
+ export function getManifests(): AgentManifest[] {
185
+ if (!cache) cache = loadManifests();
186
+ return cache;
187
+ }
188
+
189
+ /** Test hook: clear the cache and the one-time-warning ledger. */
190
+ export function _resetForTests(): void {
191
+ cache = undefined;
192
+ warned.clear();
193
+ }