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,217 @@
1
+ /**
2
+ * Process-tree resolution for the snapshot-scraping fallback.
3
+ *
4
+ * A tmux pane's `pane_current_command` is only its IMMEDIATE child process —
5
+ * for a real agent that is usually `node`/`bun`/`sh`, never `claude`/`codex`.
6
+ * So the detection manifests picked purely by `pane_current_command` almost
7
+ * never match in practice. To fix that we walk the pane's process TREE and
8
+ * resolve what is actually running underneath it.
9
+ *
10
+ * Everything here is pure except the thin `readProcessTable` io wrapper, and
11
+ * nothing ever throws — a missing/garbled `ps` simply yields no matches, and
12
+ * resolution falls back to the fast `pane_current_command` path.
13
+ */
14
+ import { execFileSync } from "node:child_process";
15
+ import { pickManifest, type AgentManifest } from "./manifest.ts";
16
+ import { getManifests } from "./manifest-loader.ts";
17
+
18
+ /** One row of the process table. */
19
+ export interface ProcEntry {
20
+ pid: number;
21
+ ppid: number;
22
+ /** Full command line (argv joined), as `ps` printed it. */
23
+ command: string;
24
+ }
25
+
26
+ /**
27
+ * Parse `ps -axo pid=,ppid=,command=` output. Each line is
28
+ * `<pid> <ppid> <command line…>` with leading padding on the numeric columns.
29
+ * Malformed lines are skipped; never throws.
30
+ */
31
+ export function parsePsOutput(raw: string): ProcEntry[] {
32
+ const entries: ProcEntry[] = [];
33
+ for (const line of raw.split("\n")) {
34
+ const trimmed = line.replace(/^\s+/, "");
35
+ if (trimmed.length === 0) continue;
36
+ // pid, ppid, then the rest is the command line (which may contain spaces).
37
+ const match = /^(\d+)\s+(\d+)\s+(.*)$/.exec(trimmed);
38
+ if (!match) continue;
39
+ const pid = Number(match[1]);
40
+ const ppid = Number(match[2]);
41
+ const command = match[3] ?? "";
42
+ if (!Number.isFinite(pid) || !Number.isFinite(ppid) || command.length === 0) continue;
43
+ entries.push({ pid, ppid, command });
44
+ }
45
+ return entries;
46
+ }
47
+
48
+ /**
49
+ * Collect the command lines in the subtree rooted at `rootPid`, INCLUDING the
50
+ * root process itself. Deeper (more specific) processes come FIRST — a DFS that
51
+ * emits children before their parent — so the most-specific agent command is
52
+ * seen before the generic `node`/`bun`/`sh` shims above it. Cycles and runaway
53
+ * depth are guarded.
54
+ */
55
+ export function subtreeCommands(entries: ProcEntry[], rootPid: number, maxDepth = 6): string[] {
56
+ const childrenByParent = new Map<number, ProcEntry[]>();
57
+ const byPid = new Map<number, ProcEntry>();
58
+ for (const entry of entries) {
59
+ byPid.set(entry.pid, entry);
60
+ const siblings = childrenByParent.get(entry.ppid) ?? [];
61
+ siblings.push(entry);
62
+ childrenByParent.set(entry.ppid, siblings);
63
+ }
64
+
65
+ const root = byPid.get(rootPid);
66
+ if (!root) return [];
67
+
68
+ const commands: string[] = [];
69
+ const visited = new Set<number>();
70
+
71
+ const walk = (pid: number, depth: number): void => {
72
+ if (depth > maxDepth || visited.has(pid)) return;
73
+ visited.add(pid);
74
+ // Emit children first (deepest → shallowest) so the root lands last.
75
+ for (const child of childrenByParent.get(pid) ?? []) {
76
+ walk(child.pid, depth + 1);
77
+ }
78
+ const self = byPid.get(pid);
79
+ if (self) commands.push(self.command);
80
+ };
81
+
82
+ walk(rootPid, 0);
83
+ return commands;
84
+ }
85
+
86
+ /**
87
+ * Summarize the executable-ish tokens seen in a pane's process subtree, in the
88
+ * order the tree-walk visits them (deepest first), de-duplicated. Pure.
89
+ *
90
+ * This is a DIAGNOSTIC for `agent explain`: when no manifest resolves, showing
91
+ * "process-tree saw: node, sleep" tells the user WHY nothing matched and what
92
+ * to point an `@agent_hint` at. Tokens are the same basenames
93
+ * `resolveAgentCommand` matches against, so the report reflects reality.
94
+ */
95
+ export function describeSubtree(entries: ProcEntry[], rootPid: number, limit = 8): string[] {
96
+ const seen: string[] = [];
97
+ for (const command of subtreeCommands(entries, rootPid)) {
98
+ for (const token of commandTokens(command)) {
99
+ if (!seen.includes(token)) seen.push(token);
100
+ if (seen.length >= limit) return seen;
101
+ }
102
+ }
103
+ return seen;
104
+ }
105
+
106
+ /**
107
+ * Read the live process table. Thin io wrapper — on any failure returns `[]`
108
+ * so callers degrade to the fast path rather than throwing.
109
+ */
110
+ export function readProcessTable(): ProcEntry[] {
111
+ try {
112
+ const raw = execFileSync("ps", ["-axo", "pid=,ppid=,command="], {
113
+ encoding: "utf8",
114
+ stdio: ["ignore", "pipe", "ignore"],
115
+ timeout: 2000,
116
+ });
117
+ return parsePsOutput(raw);
118
+ } catch {
119
+ return [];
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Extract the candidate tokens a command line should be matched against.
125
+ *
126
+ * Full argv strings can incidentally contain an agent name (a path like
127
+ * `/Users/x/.claude/notes.md`, or `vim ~/.codex/todo`), which would produce a
128
+ * false positive if we ran `pickManifest` over the raw string. Instead we test
129
+ * only the meaningful executable-ish tokens: the basename of argv[0] and the
130
+ * basename of the second token (a script/subcommand path). Query strings and
131
+ * flags are ignored.
132
+ *
133
+ * e.g. `node /Users/x/.nvm/versions/node/v20/bin/claude --foo` → ["node", "claude"]
134
+ * `vim /Users/x/.claude/notes.md` → ["vim", "notes.md"]
135
+ */
136
+ export function commandTokens(command: string): string[] {
137
+ const parts = command.trim().split(/\s+/).filter(Boolean);
138
+ const tokens: string[] = [];
139
+ const argv0 = parts[0];
140
+ if (argv0) tokens.push(basename(argv0));
141
+ const argv1 = parts[1];
142
+ // Only treat the second token as a script path when it is not a flag.
143
+ if (argv1 && !argv1.startsWith("-")) tokens.push(basename(argv1));
144
+ return tokens;
145
+ }
146
+
147
+ /** Path basename without the node:path import — splits on `/` and takes the tail. */
148
+ function basename(pathLike: string): string {
149
+ const segments = pathLike.split("/");
150
+ return segments[segments.length - 1] ?? pathLike;
151
+ }
152
+
153
+ /**
154
+ * Resolve which agent manifest is actually running in a pane.
155
+ *
156
+ * 1. Fast path: `pane_current_command` may already BE the agent (e.g. `claude`
157
+ * via a shim). Try `pickManifest(paneCmd)` directly.
158
+ * 2. Tree path: otherwise walk the pane's process subtree and match extracted
159
+ * tokens against the manifests. Token extraction avoids the incidental-path
160
+ * false positives described above.
161
+ *
162
+ * The tree walk prefers the highest-PRIORITY manifest (its index in
163
+ * `manifests`, which is preference-ordered: claude, codex, …, then the `shell`
164
+ * catch-all) over merely the deepest match — an agent that has spawned a
165
+ * transient shell child (a Bash tool call) must still resolve to the agent, not
166
+ * to `shell`. Depth (deepest first) only breaks ties within the same manifest.
167
+ *
168
+ * Resolution order:
169
+ * 0. HINT — `opts.hint` (the pane's `@agent_hint` option): if it names a
170
+ * known manifest it WINS outright. This is the sandbox/wrapper escape
171
+ * hatch, set with `tmux set-option -p @agent_hint claude`.
172
+ * 1. FAST — `pane_current_command` may already be the agent (a shim).
173
+ * 2. TREE — walk the process subtree and match extracted tokens.
174
+ *
175
+ * `manifests` defaults to the loaded set (bundled + user overrides).
176
+ * `matchedCommand` is the token/command that produced the hit and `source`
177
+ * records which path won — both surfaced by `agent explain`. Returns an
178
+ * undefined manifest when nothing matches.
179
+ */
180
+ export type ResolveSource = "hint" | "fast" | "tree" | "none";
181
+
182
+ export function resolveAgentCommand(
183
+ paneCmd: string,
184
+ panePid: number,
185
+ table: ProcEntry[],
186
+ opts: { manifests?: AgentManifest[]; hint?: string } = {},
187
+ ): { manifest: AgentManifest | undefined; matchedCommand: string; source: ResolveSource } {
188
+ const manifests = opts.manifests ?? getManifests();
189
+
190
+ // 0. Hint: a per-pane @agent_hint forces a manifest, bypassing resolution.
191
+ const hint = opts.hint?.trim();
192
+ if (hint) {
193
+ const hinted = pickManifest(hint, manifests);
194
+ if (hinted) return { manifest: hinted, matchedCommand: hint, source: "hint" };
195
+ }
196
+
197
+ const fast = pickManifest(paneCmd, manifests);
198
+ if (fast) return { manifest: fast, matchedCommand: paneCmd, source: "fast" };
199
+
200
+ let best: { manifest: AgentManifest; matchedCommand: string; rank: number } | undefined;
201
+ for (const command of subtreeCommands(table, panePid)) {
202
+ for (const token of commandTokens(command)) {
203
+ const hit = pickManifest(token, manifests);
204
+ if (!hit) continue;
205
+ const rank = manifests.indexOf(hit);
206
+ // Keep the first (deepest) match at a given rank; only replace when a
207
+ // strictly higher-priority manifest turns up.
208
+ if (!best || rank < best.rank) best = { manifest: hit, matchedCommand: token, rank };
209
+ if (best.rank === 0)
210
+ return { manifest: best.manifest, matchedCommand: best.matchedCommand, source: "tree" };
211
+ }
212
+ }
213
+
214
+ return best
215
+ ? { manifest: best.manifest, matchedCommand: best.matchedCommand, source: "tree" }
216
+ : { manifest: undefined, matchedCommand: "", source: "none" };
217
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Pane snapshot reader for agent-state detection.
3
+ *
4
+ * Reads the bottom-buffer text of a tmux pane and normalizes it into a
5
+ * `PaneSnapshot` — the raw evidence later detectors classify into
6
+ * blocked/working/done/idle. The parsing (`parseSnapshot`) is pure and
7
+ * unit-tested; `readPaneSnapshot` is the thin tmux I/O wrapper.
8
+ */
9
+ import { captureRecent } from "@tmux-ide/tmux-bridge";
10
+
11
+ export interface PaneSnapshot {
12
+ /** Last N non-empty lines, ANSI-stripped, trailing whitespace trimmed. */
13
+ bottomNonEmpty: string[];
14
+ /** ANSI-stripped joined snapshot text. */
15
+ text: string;
16
+ /** Original raw capture, kept for ansi-format debugging. */
17
+ raw: string;
18
+ }
19
+
20
+ // Well-known ANSI escape matcher (ansi-regex): SGR/cursor CSI sequences plus
21
+ // OSC strings terminated by BEL.
22
+ /* eslint-disable no-control-regex */
23
+ const ANSI =
24
+ /[›][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
25
+
26
+ /* eslint-enable no-control-regex */
27
+
28
+ /** Strip ANSI escape sequences from a string. */
29
+ function stripAnsi(input: string): string {
30
+ return input.replace(ANSI, "");
31
+ }
32
+
33
+ const DEFAULT_LINES = 20;
34
+
35
+ /**
36
+ * Normalize raw pane text into a `PaneSnapshot`. Pure — never throws.
37
+ *
38
+ * @param raw Raw pane capture (may contain ANSI escapes).
39
+ * @param opts.lines Number of trailing non-empty lines to keep (default 20).
40
+ */
41
+ export function parseSnapshot(raw: string, opts: { lines?: number } = {}): PaneSnapshot {
42
+ const lines = opts.lines ?? DEFAULT_LINES;
43
+ const text = stripAnsi(raw ?? "");
44
+
45
+ const nonEmpty = text
46
+ .split("\n")
47
+ .map((line) => line.replace(/\s+$/, ""))
48
+ .filter((line) => line.length > 0);
49
+
50
+ const bottomNonEmpty = lines > 0 ? nonEmpty.slice(-lines) : [];
51
+
52
+ return { bottomNonEmpty, text, raw: raw ?? "" };
53
+ }
54
+
55
+ /**
56
+ * Capture a pane's recent buffer and parse it into a `PaneSnapshot`.
57
+ * On any tmux error, returns an empty snapshot rather than throwing.
58
+ *
59
+ * @param target tmux pane target (session, window, or pane id).
60
+ * @param opts.lines Number of trailing non-empty lines to keep (default 20).
61
+ */
62
+ export function readPaneSnapshot(target: string, opts: { lines?: number } = {}): PaneSnapshot {
63
+ const lines = opts.lines ?? DEFAULT_LINES;
64
+ try {
65
+ const raw = captureRecent(target, lines);
66
+ return parseSnapshot(raw, { lines });
67
+ } catch {
68
+ return { bottomNonEmpty: [], text: "", raw: "" };
69
+ }
70
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Claude Code integration — the authoritative detection layer.
3
+ *
4
+ * Claude Code has a first-class hooks system. `tmux-ide integration install
5
+ * claude` writes a tiny POSIX hook script and registers it in the user's
6
+ * `~/.claude/settings.json` for the lifecycle events we care about. Each hook
7
+ * invocation stamps the CURRENT tmux pane with a pane-local user option:
8
+ *
9
+ * @agent_state "<working|blocked|done|idle>:<unix epoch>"
10
+ * @agent_session_id the Claude session id (future: --resume on restore)
11
+ *
12
+ * The detector treats a fresh `@agent_state` as GROUND TRUTH and only falls
13
+ * back to screen-manifest scraping when no authority is present — the same
14
+ * two-layer model the best agent terminals use. Any other agent can join the
15
+ * authority layer by writing the same option (`tmux set-option -p
16
+ * @agent_state working:$(date +%s)`) — no integration required.
17
+ *
18
+ * The settings merge is surgical and reversible: entries are tagged by the
19
+ * hook-script path, a one-time backup is written next to settings.json, and
20
+ * uninstall removes exactly our entries.
21
+ */
22
+ import {
23
+ chmodSync,
24
+ copyFileSync,
25
+ existsSync,
26
+ mkdirSync,
27
+ readFileSync,
28
+ writeFileSync,
29
+ } from "node:fs";
30
+ import { homedir } from "node:os";
31
+ import { dirname, join } from "node:path";
32
+
33
+ /** Marker every installed hook command contains — the removal key. */
34
+ export const HOOK_SCRIPT_RELPATH = ".tmux-ide/hooks/claude-state.sh";
35
+
36
+ export function hookScriptPath(): string {
37
+ return join(homedir(), HOOK_SCRIPT_RELPATH);
38
+ }
39
+
40
+ /**
41
+ * Absolute path to Claude Code's settings file: `TMUX_IDE_CLAUDE_SETTINGS` when
42
+ * set (tests / per-run overrides), else `~/.claude/settings.json`. The override
43
+ * lets install/offer flows be exercised against a scratch file so a test never
44
+ * reads or rewrites the user's real settings.
45
+ */
46
+ export function claudeSettingsPath(): string {
47
+ return process.env.TMUX_IDE_CLAUDE_SETTINGS ?? join(homedir(), ".claude", "settings.json");
48
+ }
49
+
50
+ /**
51
+ * The hook script. POSIX sh, no dependencies. Claude Code passes the event
52
+ * payload as JSON on stdin; the state to report comes in as $1 (each settings
53
+ * entry passes its own state, so the script never parses the event name).
54
+ * Outside tmux ($TMUX_PANE unset) it exits quietly. The session id is
55
+ * extracted with sed — good enough for a flat JSON string field.
56
+ */
57
+ export const HOOK_SCRIPT = `#!/bin/sh
58
+ # tmux-ide agent-state hook (installed by: tmux-ide integration install claude)
59
+ # $1 = state to report: working | blocked | done | idle
60
+ state="\${1:-idle}"
61
+ payload="$(cat 2>/dev/null || true)"
62
+ [ -n "$TMUX_PANE" ] || exit 0
63
+ tmux set-option -p -t "$TMUX_PANE" @agent_state "\${state}:$(date +%s)" 2>/dev/null || exit 0
64
+ sid="$(printf '%s' "$payload" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)"
65
+ [ -n "$sid" ] && tmux set-option -p -t "$TMUX_PANE" @agent_session_id "$sid" 2>/dev/null
66
+ exit 0
67
+ `;
68
+
69
+ /** Claude Code lifecycle events → the state each reports. */
70
+ export const EVENT_STATES: Array<{ event: string; state: string; matcher?: string }> = [
71
+ { event: "UserPromptSubmit", state: "working" },
72
+ { event: "PreToolUse", state: "working", matcher: "*" },
73
+ { event: "Notification", state: "blocked" },
74
+ { event: "Stop", state: "done" },
75
+ { event: "SessionEnd", state: "idle" },
76
+ ];
77
+
78
+ interface HookCommand {
79
+ type: "command";
80
+ command: string;
81
+ }
82
+ interface HookGroup {
83
+ matcher?: string;
84
+ hooks: HookCommand[];
85
+ }
86
+ type HooksConfig = Record<string, HookGroup[]>;
87
+ export type ClaudeSettings = Record<string, unknown> & { hooks?: HooksConfig };
88
+
89
+ /** Does a hook group belong to us? (its command references our script) */
90
+ function isOurs(group: HookGroup): boolean {
91
+ return group.hooks?.some((h) => h.command?.includes(HOOK_SCRIPT_RELPATH)) ?? false;
92
+ }
93
+
94
+ /**
95
+ * PURE — return a copy of `settings` with our hook entries merged in.
96
+ * Idempotent: existing tmux-ide entries are replaced, everything else is
97
+ * preserved untouched.
98
+ */
99
+ export function mergeHooks(settings: ClaudeSettings, scriptPath: string): ClaudeSettings {
100
+ const next: ClaudeSettings = { ...settings, hooks: { ...(settings.hooks ?? {}) } };
101
+ const hooks = next.hooks as HooksConfig;
102
+ for (const { event, state, matcher } of EVENT_STATES) {
103
+ const existing = (hooks[event] ?? []).filter((g) => !isOurs(g));
104
+ const group: HookGroup = {
105
+ ...(matcher !== undefined ? { matcher } : {}),
106
+ hooks: [{ type: "command", command: `${scriptPath} ${state}` }],
107
+ };
108
+ hooks[event] = [...existing, group];
109
+ }
110
+ return next;
111
+ }
112
+
113
+ /** PURE — return a copy of `settings` with exactly our entries removed. */
114
+ export function removeHooks(settings: ClaudeSettings): ClaudeSettings {
115
+ if (!settings.hooks) return { ...settings };
116
+ const hooks: HooksConfig = {};
117
+ for (const [event, groups] of Object.entries(settings.hooks)) {
118
+ const kept = groups.filter((g) => !isOurs(g));
119
+ if (kept.length > 0) hooks[event] = kept;
120
+ }
121
+ const next: ClaudeSettings = { ...settings, hooks };
122
+ if (Object.keys(hooks).length === 0) delete next.hooks;
123
+ return next;
124
+ }
125
+
126
+ /** PURE — is our integration present in these settings? */
127
+ export function isInstalled(settings: ClaudeSettings): boolean {
128
+ return Object.values(settings.hooks ?? {}).some((groups) => groups.some(isOurs));
129
+ }
130
+
131
+ function readSettings(path: string): ClaudeSettings {
132
+ if (!existsSync(path)) return {};
133
+ try {
134
+ return JSON.parse(readFileSync(path, "utf8")) as ClaudeSettings;
135
+ } catch {
136
+ throw new Error(`${path} is not valid JSON — fix or move it, then retry`);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Install: write the hook script, back up settings.json once, merge our
142
+ * entries. Takes effect for NEW Claude Code sessions (hooks are read at
143
+ * session start).
144
+ */
145
+ export function installClaudeIntegration(): { scriptPath: string; settingsPath: string } {
146
+ const script = hookScriptPath();
147
+ mkdirSync(dirname(script), { recursive: true });
148
+ writeFileSync(script, HOOK_SCRIPT, "utf8");
149
+ chmodSync(script, 0o755);
150
+
151
+ const settingsPath = claudeSettingsPath();
152
+ mkdirSync(dirname(settingsPath), { recursive: true });
153
+ const settings = readSettings(settingsPath);
154
+ const backup = `${settingsPath}.tmux-ide.bak`;
155
+ if (existsSync(settingsPath) && !existsSync(backup)) copyFileSync(settingsPath, backup);
156
+ writeFileSync(settingsPath, `${JSON.stringify(mergeHooks(settings, script), null, 2)}\n`, "utf8");
157
+ return { scriptPath: script, settingsPath };
158
+ }
159
+
160
+ /** Uninstall: remove exactly our entries (script file is left, it's inert). */
161
+ export function uninstallClaudeIntegration(): { settingsPath: string; wasInstalled: boolean } {
162
+ const settingsPath = claudeSettingsPath();
163
+ const settings = readSettings(settingsPath);
164
+ const wasInstalled = isInstalled(settings);
165
+ if (wasInstalled) {
166
+ writeFileSync(settingsPath, `${JSON.stringify(removeHooks(settings), null, 2)}\n`, "utf8");
167
+ }
168
+ return { settingsPath, wasInstalled };
169
+ }
170
+
171
+ export function claudeIntegrationStatus(): { installed: boolean; scriptExists: boolean } {
172
+ return {
173
+ installed: isInstalled(readSettings(claudeSettingsPath())),
174
+ scriptExists: existsSync(hookScriptPath()),
175
+ };
176
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The one-time INTEGRATION OFFER — shown the first time tmux-ide adopts a
3
+ * session on a machine that has Claude Code but hasn't installed the lifecycle
4
+ * hook.
5
+ *
6
+ * The onboarding gap: the Claude Code integration turns pane state from
7
+ * screen-scraping guesswork into ground truth, but nothing prompts the user to
8
+ * install it — they'd have to read the docs. This offer notices `claude` on
9
+ * PATH at adopt time and asks once: install now? [y/N]. `y` installs; anything
10
+ * else skips. Either way a marker is written so it never asks again.
11
+ *
12
+ * "Once" is enforced by a marker file (`<home>/integration-offered`, `<home>` =
13
+ * `TMUX_IDE_HOME` when set — so tests and the dev box never see it), exactly
14
+ * like the welcome card ({@link ../chrome/welcome.ts}). The offer is ALSO gated
15
+ * by config (`integrations.offer`), so it can be suppressed without touching the
16
+ * marker. {@link shouldOfferIntegration} is the PURE decision; the marker
17
+ * helpers and {@link maybeOfferIntegrationPopup} wire the io. It NEVER fails an
18
+ * adopt — every io path is best-effort.
19
+ */
20
+ import { execFileSync, spawn } from "node:child_process";
21
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
22
+ import { homedir } from "node:os";
23
+ import { dirname, join } from "node:path";
24
+ import { getAppConfig } from "../../lib/app-config.ts";
25
+ import { claudeIntegrationStatus } from "./claude.ts";
26
+
27
+ /**
28
+ * Absolute path to the "already offered" marker: `<home>/integration-offered`,
29
+ * where `<home>` is `TMUX_IDE_HOME` when set (tests / per-run overrides), else
30
+ * `~/.tmux-ide`. The env override lets a live test point the marker at a scratch
31
+ * dir so it never touches — or is confused by — the real user's marker.
32
+ */
33
+ export function integrationOfferMarkerPath(): string {
34
+ const home = process.env.TMUX_IDE_HOME ?? join(homedir(), ".tmux-ide");
35
+ return join(home, "integration-offered");
36
+ }
37
+
38
+ /**
39
+ * PURE — the first-adopt offer decision. Show the offer only when ALL hold:
40
+ * - `claudeOnPath` Claude Code is installed on this machine,
41
+ * - `integrationInstalled` is false (nothing to offer if it's already hooked),
42
+ * - `markerPresent` is false (we've never offered before), and
43
+ * - `offerEnabled` config hasn't disabled the offer.
44
+ * Kept pure so every gate is trivially unit-tested.
45
+ */
46
+ export function shouldOfferIntegration(input: {
47
+ claudeOnPath: boolean;
48
+ integrationInstalled: boolean;
49
+ markerPresent: boolean;
50
+ offerEnabled: boolean;
51
+ }): boolean {
52
+ return (
53
+ input.claudeOnPath && !input.integrationInstalled && !input.markerPresent && input.offerEnabled
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Create the marker so the offer shows only once. Best-effort — a marker we
59
+ * can't write means the offer may show again, but it must never crash adopt.
60
+ */
61
+ export function markIntegrationOffered(): void {
62
+ const path = integrationOfferMarkerPath();
63
+ try {
64
+ mkdirSync(dirname(path), { recursive: true });
65
+ writeFileSync(path, new Date().toISOString());
66
+ } catch {
67
+ // can't write the marker — degrade to "may offer again", never throw
68
+ }
69
+ }
70
+
71
+ /**
72
+ * PURE — the offer prompt text shown inside the popup. A single question with a
73
+ * one-line rationale; the `[y/N]` makes the safe default (skip) obvious.
74
+ */
75
+ export function buildOfferText(): string {
76
+ const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
77
+ const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
78
+ const head = (s: string) => `\x1b[1;36m${s}\x1b[0m`;
79
+ return [
80
+ head(" Claude Code detected"),
81
+ "",
82
+ " Install the tmux-ide integration for ground-truth agent status?",
83
+ dim(" It hooks Claude Code's lifecycle so pane state is exact, not guessed."),
84
+ "",
85
+ ` ${bold("[y]")} install ${bold("[N]")} skip (any other key)`,
86
+ dim(" Asked once — press a key."),
87
+ ].join("\n");
88
+ }
89
+
90
+ /**
91
+ * io — float the one-time offer popup on the CURRENT tmux client, best-effort.
92
+ *
93
+ * Called at the end of {@link ../chrome/statusline.ts adoptSession} (right after
94
+ * the welcome card). Gated by {@link shouldOfferIntegration} (PATH + integration
95
+ * status + marker + config) AND by being inside a tmux client — outside tmux
96
+ * there's nowhere to float the popup, so we neither show it nor burn the marker
97
+ * (the next in-tmux adopt still offers).
98
+ *
99
+ * The popup runs `tmux-ide integration offer`, which reads one key and installs
100
+ * on `y` (see bin/cli.ts). It's spawned DETACHED and unref'd so it never blocks
101
+ * the adopt. The marker is written immediately after the spawn attempt so a
102
+ * rapid `adopt --all` offers exactly once, not once per session — matching the
103
+ * welcome card's one-shot discipline.
104
+ */
105
+ export function maybeOfferIntegrationPopup(): void {
106
+ let offer: boolean;
107
+ try {
108
+ const status = claudeIntegrationStatus();
109
+ offer = shouldOfferIntegration({
110
+ claudeOnPath: claudeOnPath(),
111
+ integrationInstalled: status.installed,
112
+ markerPresent: existsSync(integrationOfferMarkerPath()),
113
+ offerEnabled: getAppConfig().integrations.offer,
114
+ });
115
+ } catch {
116
+ return; // any probe failure → never risk the adopt, just don't offer
117
+ }
118
+ if (!offer) return;
119
+ if (!process.env.TMUX) return;
120
+ try {
121
+ const child = spawn(
122
+ "tmux",
123
+ ["display-popup", "-E", "-w", "64", "-h", "12", "tmux-ide integration offer"],
124
+ { stdio: "ignore", detached: true },
125
+ );
126
+ child.unref();
127
+ } catch {
128
+ // tmux missing / no client — best-effort, still mark so we don't retry forever
129
+ }
130
+ markIntegrationOffered();
131
+ }
132
+
133
+ /**
134
+ * io — is `claude` on PATH? A which-probe capped at 2s, swallowing every failure
135
+ * to false. Local (not {@link ../../lib/agent-discovery.ts}) to keep the adopt
136
+ * path's offer check a single cheap probe rather than the full agent sweep.
137
+ */
138
+ function claudeOnPath(): boolean {
139
+ try {
140
+ execFileSync("which", ["claude"], { stdio: "ignore", timeout: 2000 });
141
+ return true;
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Dispatcher entry for the compiled `tmux-ide-tui` binary.
3
+ *
4
+ * `bun build --compile` bundles every TUI surface (the cockpit + widgets) into
5
+ * ONE standalone executable so the OpenTUI/Solid `.tsx` surfaces run on a clean
6
+ * `npm i -g tmux-ide` — no dev checkout, no `bun` runtime, no bunfig preload.
7
+ * The native OpenTUI dylib rides along via Bun's embedded-asset mechanism (the
8
+ * `import ... with { type: "file" }` in @opentui/core-*), and the Solid JSX
9
+ * transform happens at build time via the @opentui/solid bun plugin, so the
10
+ * binary carries only plain JS.
11
+ *
12
+ * Contract: `tmux-ide-tui <surface> [--flags…]`. The first positional selects
13
+ * the surface; the rest are the surface's own args (theme, session, dir, …),
14
+ * exactly as the `bun <entry> …` invocation passed them. We strip the surface
15
+ * token from `process.argv` before importing so each entry's top-level
16
+ * `parseArgs()` sees the same argv it always has.
17
+ *
18
+ * The `import()` calls use LITERAL specifiers on purpose: Bun's bundler only
19
+ * embeds dynamic imports it can resolve statically, and a literal switch keeps
20
+ * exactly one surface's top-level `render()` side effect from firing.
21
+ */
22
+
23
+ const SURFACES = [
24
+ "team",
25
+ "app",
26
+ "explorer",
27
+ "changes",
28
+ "preview",
29
+ "config",
30
+ "setup",
31
+ "sidebar",
32
+ ] as const;
33
+
34
+ type Surface = (typeof SURFACES)[number];
35
+
36
+ function isSurface(value: string | undefined): value is Surface {
37
+ return value !== undefined && (SURFACES as readonly string[]).includes(value);
38
+ }
39
+
40
+ async function main(): Promise<void> {
41
+ const surface = process.argv[2];
42
+
43
+ if (!isSurface(surface)) {
44
+ process.stderr.write(
45
+ `tmux-ide-tui: unknown surface ${surface ? `"${surface}"` : "(none given)"}.\n` +
46
+ `Usage: tmux-ide-tui <${SURFACES.join("|")}> [flags]\n`,
47
+ );
48
+ process.exit(2);
49
+ }
50
+
51
+ // Drop the surface token so each entry's `parseArgs()` sees only its flags.
52
+ process.argv.splice(2, 1);
53
+
54
+ switch (surface) {
55
+ case "team":
56
+ await import("./team/index.tsx");
57
+ break;
58
+ case "app":
59
+ await import("./mirror/app.tsx");
60
+ break;
61
+ case "explorer":
62
+ await import("../widgets/explorer/index.tsx");
63
+ break;
64
+ case "changes":
65
+ await import("../widgets/changes/index.tsx");
66
+ break;
67
+ case "preview":
68
+ await import("../widgets/preview/index.tsx");
69
+ break;
70
+ case "config":
71
+ await import("../widgets/config/index.tsx");
72
+ break;
73
+ case "setup":
74
+ await import("../widgets/setup/index.tsx");
75
+ break;
76
+ case "sidebar":
77
+ await import("../widgets/sidebar/index.tsx");
78
+ break;
79
+ }
80
+ }
81
+
82
+ void main();