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,349 @@
1
+ import { resolve } from "node:path";
2
+ import { execSync } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { readConfig, getSessionName } from "./lib/yaml-io.ts";
5
+ import { computeSizes, toSplitPercents } from "./lib/sizes.ts";
6
+ import { outputError } from "./lib/output.ts";
7
+ import { collectPaneStartupPlan } from "./lib/launch-plan.ts";
8
+ import { buildSessionOptions } from "./lib/session-options.ts";
9
+ import {
10
+ attachSession,
11
+ createDetachedSession,
12
+ getPaneCurrentCommand,
13
+ getSessionVariable,
14
+ hasSession,
15
+ runSessionCommand,
16
+ selectPane,
17
+ sendLiteral,
18
+ setPaneOption,
19
+ setPaneTitle,
20
+ setSessionEnvironment,
21
+ setSessionVariable,
22
+ splitPane,
23
+ } from "@tmux-ide/tmux-bridge";
24
+ import { validateConfig } from "./validate.ts";
25
+ import { resolveSidebarConfig } from "./tui/chrome/sidebar.ts";
26
+ import { resolveWidgetCommand } from "./widgets/resolve.ts";
27
+ import { shellEscape } from "./lib/shell.ts";
28
+ import type { IdeConfig, Row } from "./types.ts";
29
+
30
+ function stripWidgetPanes(rows: Row[]): Row[] {
31
+ return rows
32
+ .map((row) => ({
33
+ ...row,
34
+ panes: row.panes.filter((p) => !p.type),
35
+ }))
36
+ .filter((row) => row.panes.length > 0);
37
+ }
38
+
39
+ interface SplitPaneArgs {
40
+ targetPane: string;
41
+ direction: "vertical" | "horizontal";
42
+ cwd: string;
43
+ percent: number;
44
+ }
45
+
46
+ function sleepMs(ms: number): void {
47
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
48
+ }
49
+
50
+ function configHash(config: IdeConfig): string {
51
+ return createHash("sha256").update(JSON.stringify(config)).digest("hex").slice(0, 12);
52
+ }
53
+
54
+ export function waitForPaneCommand(
55
+ targetPane: string,
56
+ expectedCommands: string[],
57
+ {
58
+ attempts = 20,
59
+ delayMs = 100,
60
+ getCurrentCommand = getPaneCurrentCommand,
61
+ sleep = sleepMs,
62
+ }: {
63
+ attempts?: number;
64
+ delayMs?: number;
65
+ getCurrentCommand?: (pane: string) => string;
66
+ sleep?: (ms: number) => void;
67
+ } = {},
68
+ ): boolean {
69
+ const allowed = new Set(expectedCommands.map((command) => command.toLowerCase()));
70
+
71
+ for (let attempt = 0; attempt < attempts; attempt++) {
72
+ try {
73
+ const current = getCurrentCommand(targetPane)?.trim().toLowerCase();
74
+ if (current && allowed.has(current)) return true;
75
+ } catch {
76
+ // Fall through to retry; tmux can briefly report transitional state.
77
+ }
78
+
79
+ if (attempt < attempts - 1) {
80
+ sleep(delayMs);
81
+ }
82
+ }
83
+
84
+ return false;
85
+ }
86
+
87
+ export function buildPaneMap(
88
+ rows: Row[],
89
+ dir: string,
90
+ rootPaneId: string,
91
+ splitPaneFn: (args: SplitPaneArgs) => string,
92
+ ): { paneMap: string[][]; firstPanesOfRows: Set<string> } {
93
+ const rowSizes = computeSizes(rows);
94
+ const rowSplitPercents = toSplitPercents(rowSizes);
95
+
96
+ // Create all rows vertically first so each row spans the full width.
97
+ const rowPaneIds = [rootPaneId];
98
+ for (let rowIdx = 1; rowIdx < rows.length; rowIdx++) {
99
+ const splitFrom = rowPaneIds[rowIdx - 1]!;
100
+ const newPaneId = splitPaneFn({
101
+ targetPane: splitFrom,
102
+ direction: "vertical",
103
+ cwd: dir,
104
+ percent: rowSplitPercents[rowIdx - 1]!,
105
+ });
106
+ rowPaneIds.push(newPaneId);
107
+ }
108
+
109
+ const paneMap: string[][] = [];
110
+ const firstPanesOfRows = new Set(rowPaneIds);
111
+
112
+ for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
113
+ const row = rows[rowIdx]!;
114
+ const panes = row.panes ?? [];
115
+ const rowPaneId = rowPaneIds[rowIdx]!;
116
+ const rowPanes = [rowPaneId];
117
+
118
+ const paneSizes = computeSizes(panes);
119
+ const paneSplitPercents = toSplitPercents(paneSizes);
120
+
121
+ for (let paneIdx = 1; paneIdx < panes.length; paneIdx++) {
122
+ const pane = panes[paneIdx]!;
123
+ const targetPane = rowPanes[paneIdx - 1]!;
124
+ const paneDir = pane.dir ? resolve(dir, pane.dir) : dir;
125
+ const newPaneId = splitPaneFn({
126
+ targetPane,
127
+ direction: "horizontal",
128
+ cwd: paneDir,
129
+ percent: paneSplitPercents[paneIdx - 1]!,
130
+ });
131
+ rowPanes.push(newPaneId);
132
+ }
133
+
134
+ paneMap.push(rowPanes);
135
+ }
136
+
137
+ return { paneMap, firstPanesOfRows };
138
+ }
139
+
140
+ function loadLaunchConfig(dir: string): IdeConfig {
141
+ let config;
142
+
143
+ try {
144
+ ({ config } = readConfig(dir));
145
+ } catch (error) {
146
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
147
+ outputError(
148
+ `No ide.yml found in ${dir}. Run "tmux-ide init" or "tmux-ide detect --write" to create one.`,
149
+ "CONFIG_NOT_FOUND",
150
+ );
151
+ }
152
+
153
+ outputError(`Cannot read ide.yml: ${(error as Error).message}`, "READ_ERROR");
154
+ }
155
+
156
+ const errors = validateConfig(config);
157
+ if (errors.length > 0) {
158
+ outputError(
159
+ `Invalid ide.yml in ${dir}. Run "tmux-ide validate" for details.`,
160
+ "INVALID_CONFIG",
161
+ );
162
+ }
163
+
164
+ return config;
165
+ }
166
+
167
+ /**
168
+ * Best-effort: adopt the session into the native chrome (status bar + switcher
169
+ * popup + the shared background updater). A chrome failure must NEVER break
170
+ * launch, so it's fully swallowed; the import is dynamic to keep the hot path
171
+ * clean and the data-layer graph out of the common launch flow.
172
+ */
173
+ async function bestEffortAdopt(session: string): Promise<void> {
174
+ try {
175
+ const { adoptSession } = await import("./tui/chrome/statusline.ts");
176
+ adoptSession(session);
177
+ } catch {
178
+ // chrome is optional — never let it block the session
179
+ }
180
+ }
181
+
182
+ function runBeforeHook(command: string | undefined, dir: string): void {
183
+ if (!command) return;
184
+
185
+ console.log(`Running: ${command}`);
186
+
187
+ try {
188
+ execSync(command, { cwd: dir, stdio: "inherit", timeout: 60_000 });
189
+ } catch {
190
+ outputError(`The before hook failed: ${command}`, "BEFORE_HOOK_FAILED");
191
+ }
192
+ }
193
+
194
+ export async function launch(
195
+ targetDir: string | undefined,
196
+ {
197
+ json = false,
198
+ attach = true,
199
+ sessionName,
200
+ }: { json?: boolean; attach?: boolean; sessionName?: string } = {},
201
+ ): Promise<void> {
202
+ const dir = resolve(targetDir ?? ".");
203
+ const config = loadLaunchConfig(dir);
204
+
205
+ const { name: fallbackName } = getSessionName(dir);
206
+ // A `sessionName` override lets a worktree checkout run under its own session
207
+ // name (e.g. `app@branch`) instead of colliding with the parent repo's
208
+ // `config.name`; the whole flow keys off `session`, so the override threads
209
+ // through session creation, adoption, and drift detection unchanged.
210
+ const session = sessionName ?? config.name ?? fallbackName;
211
+ const headless = config.orchestrator?.widgets === false;
212
+ const rows = headless ? stripWidgetPanes(config.rows) : config.rows;
213
+ const theme = config.theme ?? {};
214
+ const team = config.team ?? null;
215
+
216
+ runBeforeHook(config.before, dir);
217
+
218
+ // If session already exists, check for config drift and attach
219
+ if (hasSession(session)) {
220
+ const currentHash = configHash(config);
221
+ const storedHash = getSessionVariable(session, "@config_hash");
222
+ const configChanged = Boolean(storedHash && currentHash !== storedHash);
223
+
224
+ if (json) {
225
+ console.log(JSON.stringify({ session, running: true, configChanged }));
226
+ } else if (configChanged) {
227
+ console.log(`Session "${session}" is running but ide.yml has changed.`);
228
+ console.log(`Run "tmux-ide restart" to apply changes.`);
229
+ } else {
230
+ console.log(`Session "${session}" is already running. Attaching...`);
231
+ }
232
+
233
+ // Keep the chrome in place across re-launches (idempotent).
234
+ await bestEffortAdopt(session);
235
+ if (attach) {
236
+ attachSession(session);
237
+ }
238
+ return;
239
+ }
240
+
241
+ // Get terminal dimensions
242
+ const cols = process.stdout.columns ?? 200;
243
+ const lines = process.stdout.rows ?? 50;
244
+
245
+ // Create session with first pane
246
+ const rootPaneId = createDetachedSession(session, dir, { cols, lines });
247
+
248
+ // Set agent teams env var if team config is present
249
+ if (team) {
250
+ setSessionEnvironment(session, "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", "1");
251
+ }
252
+
253
+ const { paneMap, firstPanesOfRows } = buildPaneMap(
254
+ rows,
255
+ dir,
256
+ rootPaneId,
257
+ ({ targetPane, direction, cwd, percent }) => splitPane(targetPane, direction, cwd, percent),
258
+ );
259
+
260
+ const { focusPane, paneActions } = collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir);
261
+
262
+ for (const action of paneActions) {
263
+ if (action.title) {
264
+ setPaneTitle(action.targetPane, action.title);
265
+ }
266
+
267
+ // Set pane identity options for discovery by orchestrator/widgets
268
+ setPaneOption(action.targetPane, "@ide_role", action.paneRole ?? "shell");
269
+ setPaneOption(action.targetPane, "@ide_name", action.title ?? "");
270
+ setPaneOption(action.targetPane, "@ide_type", action.paneType ?? "shell");
271
+
272
+ // Lock agent pane titles so Claude Code can't overwrite them
273
+ if (action.paneRole === "lead" || action.paneRole === "teammate") {
274
+ setPaneOption(action.targetPane, "allow-rename", "off");
275
+ }
276
+
277
+ if (action.chdir) {
278
+ sendLiteral(action.targetPane, `cd ${shellEscape(action.chdir)}`);
279
+ }
280
+
281
+ for (const exportCommand of action.exports) {
282
+ sendLiteral(action.targetPane, exportCommand);
283
+ }
284
+
285
+ if (action.widgetType) {
286
+ const widgetCmd = resolveWidgetCommand(action.widgetType, {
287
+ session,
288
+ dir,
289
+ target: action.widgetTarget ?? null,
290
+ theme: config.theme ?? null,
291
+ });
292
+ sendLiteral(action.targetPane, widgetCmd);
293
+ } else if (action.command) {
294
+ sendLiteral(action.targetPane, action.command);
295
+ }
296
+ }
297
+
298
+ for (const command of buildSessionOptions(session, { theme })) {
299
+ runSessionCommand(command);
300
+ }
301
+
302
+ // Store config hash for drift detection on re-launch
303
+ setSessionVariable(session, "@config_hash", configHash(config));
304
+
305
+ // Sidebar sugar: `sidebar: true` (or `{ width }`) injects the app nav column
306
+ // as a full-height left split of the whole window (`-h -b -f`), built AFTER
307
+ // the rows so `-f` spans their combined height. Best-effort — the layout must
308
+ // never fail because the chrome column couldn't open.
309
+ const sidebar = resolveSidebarConfig(config.sidebar);
310
+ if (sidebar.enabled) {
311
+ try {
312
+ const { openSidebarPane } = await import("./tui/chrome/sidebar.ts");
313
+ openSidebarPane(session, dir, sidebar.width, config.theme ?? null);
314
+ } catch {
315
+ // sidebar is optional chrome — never block launch
316
+ }
317
+ }
318
+
319
+ // Focus the correct pane (the sidebar split above steals focus to itself).
320
+ selectPane(focusPane);
321
+
322
+ // Launch summary
323
+ const totalPanes = rows.reduce((sum, r) => sum + (r.panes?.length ?? 0), 0);
324
+ console.log(
325
+ `Starting "${session}" (${rows.length} row${rows.length === 1 ? "" : "s"}, ${totalPanes} pane${totalPanes === 1 ? "" : "s"})...`,
326
+ );
327
+
328
+ // Surface the command-center URL so users know where the API lives.
329
+ // Read the canonical daemon info file the daemon writes on startup;
330
+ // tolerate its absence (daemon may still be coming up, or running
331
+ // sessionless). Print only when we have a real port to advertise.
332
+ try {
333
+ const { readCanonicalDaemonInfo } = await import("./lib/canonical-daemon.ts");
334
+ const info = readCanonicalDaemonInfo();
335
+ if (info) {
336
+ console.log(`Command center: http://${info.bindHostname}:${info.port}/`);
337
+ }
338
+ } catch {
339
+ // Non-fatal — the daemon may still be coming up.
340
+ }
341
+
342
+ // Adopt into the native chrome so the new session shows the tmux-ide bar.
343
+ await bestEffortAdopt(session);
344
+
345
+ // Attach
346
+ if (attach) {
347
+ attachSession(session);
348
+ }
349
+ }
@@ -0,0 +1,49 @@
1
+ export interface ProjectActivationOptions {
2
+ orchestrate?: boolean;
3
+ }
4
+
5
+ export interface ProjectActivationBackend {
6
+ activateProject(name: string, options?: ProjectActivationOptions): Promise<void>;
7
+ deactivateProject(name: string): Promise<void>;
8
+ }
9
+
10
+ let backend: ProjectActivationBackend | null = null;
11
+ const active = new Set<string>();
12
+
13
+ export function setActivationBackend(next: ProjectActivationBackend | null): void {
14
+ backend = next;
15
+ active.clear();
16
+ }
17
+
18
+ export function isProjectActive(name: string): boolean {
19
+ return active.has(name);
20
+ }
21
+
22
+ export async function activateProject(
23
+ name: string,
24
+ options: ProjectActivationOptions = {},
25
+ ): Promise<void> {
26
+ if (active.has(name) && !options.orchestrate) return;
27
+ if (!backend) {
28
+ throw new Error("No active-project backend is registered");
29
+ }
30
+ await backend.activateProject(name, options);
31
+ active.add(name);
32
+ }
33
+
34
+ export async function deactivateProject(name: string): Promise<void> {
35
+ if (!active.has(name)) return;
36
+ if (!backend) {
37
+ active.delete(name);
38
+ return;
39
+ }
40
+ try {
41
+ await backend.deactivateProject(name);
42
+ } finally {
43
+ active.delete(name);
44
+ }
45
+ }
46
+
47
+ export function listActiveProjects(): string[] {
48
+ return Array.from(active);
49
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Agent auto-discovery — which coding agents live on THIS machine, and do we
3
+ * have a lifecycle integration for each.
4
+ *
5
+ * The onboarding gap this closes: tmux-ide can hook Claude Code's lifecycle for
6
+ * ground-truth pane state (`tmux-ide integration install claude`), but nothing
7
+ * told the user that agent was even detected. Discovery makes the tool notice —
8
+ * doctor lists what it found, the first adopt offers the integration.
9
+ *
10
+ * {@link KNOWN_AGENTS} is the pure registry: each entry names an agent, the
11
+ * binary we probe for, and whether tmux-ide ships an installer for it
12
+ * (`integration: true` → we can hook its lifecycle; `false` → we only have
13
+ * screen-manifest detection). {@link discoverAgents} probes the current PATH via
14
+ * an injectable which-runner and, for agents we integrate, reports whether that
15
+ * integration is installed. Both the probe and the integration check are
16
+ * injectable so tests never shell out or read the real settings.
17
+ */
18
+ import { execFileSync } from "node:child_process";
19
+ import { claudeIntegrationStatus } from "../tui/integrations/claude.ts";
20
+
21
+ /** A coding agent tmux-ide knows how to detect. */
22
+ export interface KnownAgent {
23
+ /** Stable id — also the `tmux-ide integration <install|status> <id>` selector. */
24
+ id: string;
25
+ /** The binary we probe for on PATH. */
26
+ bin: string;
27
+ /** True → tmux-ide ships a lifecycle-integration installer for this agent. */
28
+ integration: boolean;
29
+ }
30
+
31
+ /**
32
+ * The agents tmux-ide recognizes. `integration: true` means we HAVE an installer
33
+ * (a real lifecycle hook → ground-truth pane state); the rest are detected via
34
+ * screen-manifest scraping only, with no lifecycle hook yet.
35
+ */
36
+ export const KNOWN_AGENTS: readonly KnownAgent[] = [
37
+ { id: "claude", bin: "claude", integration: true },
38
+ { id: "codex", bin: "codex", integration: false },
39
+ { id: "opencode", bin: "opencode", integration: false },
40
+ { id: "gemini", bin: "gemini", integration: false },
41
+ { id: "aider", bin: "aider", integration: false },
42
+ ];
43
+
44
+ /** One probed agent: its registry facts plus what the PATH/integration probe found. */
45
+ export interface DiscoveredAgent {
46
+ id: string;
47
+ bin: string;
48
+ /** Whether tmux-ide ships an installer for this agent (copied from the registry). */
49
+ integration: boolean;
50
+ /** Absolute path to the binary (first `which` hit), or null when absent from PATH. */
51
+ path: string | null;
52
+ /**
53
+ * The INTEGRATION-installed state: true only for an agent we integrate whose
54
+ * integration is actually installed. Always false for agents we don't
55
+ * integrate (there's nothing to install) and for any agent absent from PATH.
56
+ */
57
+ installed: boolean;
58
+ }
59
+
60
+ /** Resolve a binary to its absolute path, or null. Must never throw. */
61
+ export type WhichRunner = (bin: string) => string | null;
62
+
63
+ /** Report whether a given agent's integration is installed. Must never throw. */
64
+ export type IntegrationProbe = (agentId: string) => boolean;
65
+
66
+ /**
67
+ * Default which-runner: `which <bin>`, hard-capped at 2s, swallowing every
68
+ * failure (not-found, missing `which`, timeout) into `null`. Returns the FIRST
69
+ * line only — a shell function/alias shadowing plus a real binary can make
70
+ * `which` emit several.
71
+ */
72
+ const defaultWhich: WhichRunner = (bin) => {
73
+ try {
74
+ const out = execFileSync("which", [bin], {
75
+ encoding: "utf-8",
76
+ stdio: ["ignore", "pipe", "ignore"],
77
+ timeout: 2000,
78
+ }).trim();
79
+ if (out.length === 0) return null;
80
+ return out.split("\n")[0]!.trim() || null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ };
85
+
86
+ /**
87
+ * Default integration probe: only `claude` has an installer, so only it can be
88
+ * "installed". Reads the real Claude settings; any failure degrades to false so
89
+ * discovery never throws.
90
+ */
91
+ const defaultIntegrationProbe: IntegrationProbe = (agentId) => {
92
+ if (agentId !== "claude") return false;
93
+ try {
94
+ return claudeIntegrationStatus().installed;
95
+ } catch {
96
+ return false;
97
+ }
98
+ };
99
+
100
+ /**
101
+ * Probe the current PATH for every {@link KNOWN_AGENTS} entry. For agents we
102
+ * integrate, `installed` carries the real integration status; for the rest it's
103
+ * always false (nothing to install). Both the PATH probe and the integration
104
+ * check are injectable for tests; this never throws.
105
+ */
106
+ export function discoverAgents(
107
+ which: WhichRunner = defaultWhich,
108
+ isInstalled: IntegrationProbe = defaultIntegrationProbe,
109
+ ): DiscoveredAgent[] {
110
+ return KNOWN_AGENTS.map((agent) => {
111
+ const path = which(agent.bin);
112
+ const present = path !== null;
113
+ const installed = present && agent.integration ? isInstalled(agent.id) : false;
114
+ return { id: agent.id, bin: agent.bin, integration: agent.integration, path, installed };
115
+ });
116
+ }
117
+
118
+ /** The subset of discovered agents actually present on PATH (path resolved). */
119
+ export function presentAgents(agents: DiscoveredAgent[]): DiscoveredAgent[] {
120
+ return agents.filter((a) => a.path !== null);
121
+ }