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
package/bin/cli.ts CHANGED
@@ -9,8 +9,17 @@ import { existsSync } from "node:fs";
9
9
  import { fileURLToPath } from "node:url";
10
10
 
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ // The node-runnable CLI path that spawned TUI surfaces shell back to (the app's
13
+ // async fleet poll + `detect --write`). When we're the published `bin/cli.js`
14
+ // this file IS it; under dev (`bun bin/cli.ts`) it's the built sibling
15
+ // `bin/cli.js`. Forwarded to surfaces as TMUX_IDE_CLI so the COMPILED TUI binary
16
+ // — whose own `import.meta.url` is a virtual bunfs path with no real cli.js next
17
+ // to it — can still find the CLI to run its subprocesses.
18
+ const selfPath = fileURLToPath(import.meta.url);
19
+ const nodeCliPath = selfPath.endsWith(".js") ? selfPath : resolve(__dirname, "cli.js");
12
20
  import { launch } from "../packages/daemon/src/launch.ts";
13
- import { shouldOpenCockpit } from "../packages/daemon/src/tui/team/entry.ts";
21
+ import { resolveEntry } from "../packages/daemon/src/tui/team/entry.ts";
22
+ import { loadAppConfig } from "../packages/daemon/src/lib/app-config.ts";
14
23
  import {
15
24
  resolveTuiLaunch,
16
25
  findCompiledTui,
@@ -110,6 +119,7 @@ const knownCommands = new Set([
110
119
  "send",
111
120
  "settings",
112
121
  "team",
122
+ "app",
113
123
  "switcher",
114
124
  "wait",
115
125
  "events",
@@ -177,6 +187,7 @@ ${bold("Usage:")}
177
187
  ${dim("(--resume-agents revives claude conversations via claude --resume)")}
178
188
  ${cyan("tmux-ide attach")} ${dim("Reattach to a running session")}
179
189
  ${cyan("tmux-ide team")} [--json] ${dim("TUI over all tmux sessions (--json prints fleet state)")}
190
+ ${cyan("tmux-ide app")} [session] ${dim("Unified app: fleet home + live session mirror (bare = home)")}
180
191
  ${cyan("tmux-ide switcher")} ${dim("Compact session picker (opens in the M-p popup on adopted sessions)")}
181
192
  ${cyan("tmux-ide wait agent-status")} <session> --status <s> [--timeout <ms>]
182
193
  ${dim("Block until a session reaches a status (exit 0 match / 1 timeout)")}
@@ -264,12 +275,17 @@ function execBunWidget(
264
275
  if (launch.mode === "unavailable") {
265
276
  throw new IdeError(
266
277
  `\`tmux-ide ${commandLabel}\` is unavailable because ${launch.reasons.join(" and ")}.\n` +
267
- `Run it from a cloned tmux-ide checkout with bun installed, or install a release that ships the compiled binary.`,
278
+ `Install bun (https://bun.sh) — the TUI surfaces run on it. Sources ship with the npm package since v2.6.1.`,
268
279
  { code: "USAGE", exitCode: 1 },
269
280
  );
270
281
  }
271
282
 
272
- const env = { ...process.env, TMUX_IDE_CWD: process.cwd(), ...extraEnv };
283
+ const env = {
284
+ ...process.env,
285
+ TMUX_IDE_CWD: process.cwd(),
286
+ TMUX_IDE_CLI: nodeCliPath,
287
+ ...extraEnv,
288
+ };
273
289
  if (launch.mode === "bun") {
274
290
  // Spawn from the repo root so bun finds `bunfig.toml` (the @opentui/solid
275
291
  // JSX preload). Without this, running from any other cwd — e.g. bare
@@ -302,6 +318,7 @@ async function printFleetJson(): Promise<void> {
302
318
  }
303
319
 
304
320
  const teamScriptPath = resolve(__dirname, "../packages/daemon/src/tui/team/index.tsx");
321
+ const appScriptPath = resolve(__dirname, "../packages/daemon/src/tui/mirror/app.tsx");
305
322
 
306
323
  // `tmux-ide team` runs the standalone full-screen cockpit (the OpenTUI app owns
307
324
  // the whole terminal). The floating switcher popup (M-p on adopted sessions)
@@ -310,6 +327,13 @@ function launchTeamCockpit(): void {
310
327
  execBunWidget("team", teamScriptPath, [], "team");
311
328
  }
312
329
 
330
+ // The unified app as the front door (M22.6): bare `tmux-ide` opens `tmux-ide
331
+ // app`'s HOME panel when `app.frontDoor` is on and there's nothing else to
332
+ // launch. Same entry as the explicit `app` command with no session positional.
333
+ function launchApp(): void {
334
+ execBunWidget("app", appScriptPath, [], "app");
335
+ }
336
+
313
337
  try {
314
338
  switch (command) {
315
339
  case "start": {
@@ -332,13 +356,23 @@ try {
332
356
  }
333
357
  const targetDir = resolve(startTargetDir || ".");
334
358
  const hasIdeYml = existsSync(join(targetDir, "ide.yml"));
335
- if (shouldOpenCockpit(hasIdeYml, values.team === true)) {
336
- // No project here (or --team): the cockpit is the front door.
359
+ // M22.6 — the front-door decision. `--team` always means the classic
360
+ // cockpit; a present ide.yml still auto-launches the project; otherwise
361
+ // `app.frontDoor` flips the default no-project entry to the unified app.
362
+ const entry = resolveEntry({
363
+ hasIdeYml,
364
+ teamFlag: values.team === true,
365
+ frontDoor: loadAppConfig().app.frontDoor,
366
+ });
367
+ if (entry !== "project") {
368
+ // No project to launch here. `--json` is a scripting surface — it always
369
+ // prints the fleet, whichever interactive front door is configured.
337
370
  if (json) {
338
371
  await printFleetJson();
339
372
  break;
340
373
  }
341
- launchTeamCockpit();
374
+ if (entry === "app") launchApp();
375
+ else launchTeamCockpit();
342
376
  break;
343
377
  }
344
378
  await launch(startTargetDir, { json });
@@ -491,6 +525,16 @@ try {
491
525
  break;
492
526
  }
493
527
 
528
+ case "app": {
529
+ // The unified app (M18.1): sidebar fleet + a live tmux-session mirror.
530
+ // Bare `tmux-ide app` opens the HOME panel (fleet cards); an optional
531
+ // session positional boots straight into that session's mirror.
532
+ const session = positionals[1];
533
+ const appArgs = session ? [`--target=${session}`] : [];
534
+ execBunWidget("app", appScriptPath, appArgs, "app");
535
+ break;
536
+ }
537
+
494
538
  case "switcher": {
495
539
  // Runs the team app in PICKER mode inside a tmux `display-popup` (bound to
496
540
  // `M-p` on adopt). Picking a session `switch-client`s the invoking client
@@ -1338,6 +1382,19 @@ try {
1338
1382
  }
1339
1383
 
1340
1384
  case "update": {
1385
+ // `--tui-binary`: download the per-platform TUI binary (the fallback that
1386
+ // lets an npm install with no bun run the full cockpit). Explicit opt-in —
1387
+ // never auto-fetched on install (it's ~70MB). See lib/tui-binary.ts.
1388
+ if (values["tui-binary"] === true) {
1389
+ const { downloadTuiBinary } = await import("../packages/daemon/src/lib/tui-binary.ts");
1390
+ const { path } = await downloadTuiBinary({ log: (m) => console.error(m) });
1391
+ if (json) {
1392
+ console.log(JSON.stringify({ ok: true, path }, null, 2));
1393
+ } else {
1394
+ console.log(`TUI binary ready: ${path}`);
1395
+ }
1396
+ break;
1397
+ }
1341
1398
  // Detect how tmux-ide was installed and act on the pending update: a dev
1342
1399
  // checkout prints the `git pull` hint; a global install prints (with
1343
1400
  // --dry-run) or runs its package manager's update command. `__dirname` is
package/bunfig.toml ADDED
@@ -0,0 +1,4 @@
1
+ preload = ["@opentui/solid/preload"]
2
+
3
+ [test]
4
+ root = "./src"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Turn any project into a tmux-powered terminal IDE with a simple ide.yml",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,13 @@
12
12
  "skill",
13
13
  "templates",
14
14
  "packages/daemon/dist",
15
- "!packages/daemon/dist/tui"
15
+ "!packages/daemon/dist/tui",
16
+ "packages/daemon/src",
17
+ "bunfig.toml",
18
+ "packages/contracts/src",
19
+ "packages/tmux-bridge/src",
20
+ "packages/tmux-bridge/package.json",
21
+ "packages/contracts/package.json"
16
22
  ],
17
23
  "scripts": {
18
24
  "build": "pnpm build:cli",
@@ -59,16 +65,15 @@
59
65
  "dependencies": {
60
66
  "@hono/node-server": "^1.19.11",
61
67
  "@hono/zod-validator": "^0.7.6",
62
- "@opentui/core": "^0.1.88",
63
- "@opentui/core-darwin-arm64": "^0.1.88",
64
- "@opentui/solid": "^0.1.88",
68
+ "@opentui/core": "^0.4.3",
69
+ "@opentui/solid": "^0.4.3",
65
70
  "@parcel/watcher": "^2.5.6",
66
71
  "@types/ws": "^8.18.1",
67
72
  "hono": "^4.12.8",
68
73
  "ignore": "^7.0.5",
69
74
  "js-yaml": "^4.1.1",
70
75
  "node-pty": "1.2.0-beta.12",
71
- "solid-js": "^1.9.11",
76
+ "solid-js": "1.9.12",
72
77
  "ws": "^8.20.0",
73
78
  "zod": "^4.3.6"
74
79
  },
@@ -77,7 +82,10 @@
77
82
  "@parcel/watcher",
78
83
  "esbuild",
79
84
  "node-pty"
80
- ]
85
+ ],
86
+ "overrides": {
87
+ "zod": "^4.3.6"
88
+ }
81
89
  },
82
90
  "devDependencies": {
83
91
  "@eslint/js": "^10.0.1",
@@ -93,5 +101,8 @@
93
101
  "turbo": "^2.3.3",
94
102
  "typescript": "^5.9.3",
95
103
  "vitest": "^4.1.0"
104
+ },
105
+ "optionalDependencies": {
106
+ "@opentui/core-darwin-arm64": "^0.4.3"
96
107
  }
97
108
  }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@tmux-ide/contracts",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "lint": "eslint src",
12
+ "typecheck": "tsc --noEmit",
13
+ "test": "vitest run"
14
+ },
15
+ "dependencies": {
16
+ "zod": "^4.3.6"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.9.3",
20
+ "vitest": "^4.1.0"
21
+ }
22
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Schema tests for the `sidebar` ide.yml sugar (M15.2).
3
+ */
4
+ import { describe, expect, it } from "vitest";
5
+ import { IdeConfigSchema, SidebarConfigSchema } from "../ide-config";
6
+
7
+ const base = { rows: [{ panes: [{ title: "shell" }] }] };
8
+
9
+ describe("SidebarConfigSchema", () => {
10
+ it("accepts the boolean form", () => {
11
+ expect(SidebarConfigSchema.parse(true)).toBe(true);
12
+ expect(SidebarConfigSchema.parse(false)).toBe(false);
13
+ });
14
+
15
+ it("accepts the object form with an optional width", () => {
16
+ expect(SidebarConfigSchema.parse({ width: "30" })).toEqual({ width: "30" });
17
+ expect(SidebarConfigSchema.parse({})).toEqual({});
18
+ });
19
+
20
+ it("rejects a bare number", () => {
21
+ expect(() => SidebarConfigSchema.parse(30)).toThrow();
22
+ });
23
+ });
24
+
25
+ describe("IdeConfigSchema — sidebar", () => {
26
+ it("parses `sidebar: true`", () => {
27
+ const cfg = IdeConfigSchema.parse({ ...base, sidebar: true });
28
+ expect(cfg.sidebar).toBe(true);
29
+ });
30
+
31
+ it("parses `sidebar: { width }`", () => {
32
+ const cfg = IdeConfigSchema.parse({ ...base, sidebar: { width: "40" } });
33
+ expect(cfg.sidebar).toEqual({ width: "40" });
34
+ });
35
+
36
+ it("leaves sidebar undefined when omitted", () => {
37
+ expect(IdeConfigSchema.parse(base).sidebar).toBeUndefined();
38
+ });
39
+
40
+ it("accepts a pane of type sidebar", () => {
41
+ const cfg = IdeConfigSchema.parse({
42
+ rows: [{ panes: [{ title: "nav", type: "sidebar" }] }],
43
+ });
44
+ expect(cfg.rows[0]!.panes[0]!.type).toBe("sidebar");
45
+ });
46
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `createScriptTerminalId` determinism tests (G20-P1).
3
+ *
4
+ * The whole point of the script-derived id is determinism — two
5
+ * callers asking for "the run script for project X, scope Y, script
6
+ * Z" must collapse to one bridge. These tests pin that contract.
7
+ */
8
+
9
+ import { describe, expect, it } from "vitest";
10
+ import { createScriptTerminalId } from "../terminals";
11
+
12
+ describe("createScriptTerminalId", () => {
13
+ it("is deterministic for the same input", async () => {
14
+ const a = await createScriptTerminalId({
15
+ projectId: "proj",
16
+ scopeId: "feat-x",
17
+ kind: "run",
18
+ script: "pnpm dev",
19
+ });
20
+ const b = await createScriptTerminalId({
21
+ projectId: "proj",
22
+ scopeId: "feat-x",
23
+ kind: "run",
24
+ script: "pnpm dev",
25
+ });
26
+ expect(a).toBe(b);
27
+ expect(a).toHaveLength(32);
28
+ });
29
+
30
+ it("changes when scopeId changes (same project + script)", async () => {
31
+ const a = await createScriptTerminalId({
32
+ projectId: "proj",
33
+ scopeId: "feat-x",
34
+ kind: "run",
35
+ script: "pnpm dev",
36
+ });
37
+ const b = await createScriptTerminalId({
38
+ projectId: "proj",
39
+ scopeId: "feat-y",
40
+ kind: "run",
41
+ script: "pnpm dev",
42
+ });
43
+ expect(a).not.toBe(b);
44
+ });
45
+
46
+ it("changes when kind changes", async () => {
47
+ const run = await createScriptTerminalId({
48
+ projectId: "proj",
49
+ scopeId: "feat-x",
50
+ kind: "run",
51
+ script: "pnpm dev",
52
+ });
53
+ const setup = await createScriptTerminalId({
54
+ projectId: "proj",
55
+ scopeId: "feat-x",
56
+ kind: "setup",
57
+ script: "pnpm dev",
58
+ });
59
+ expect(run).not.toBe(setup);
60
+ });
61
+
62
+ it("accepts taskId as a backward-compat alias for scopeId", async () => {
63
+ const viaScope = await createScriptTerminalId({
64
+ projectId: "proj",
65
+ scopeId: "feat-x",
66
+ kind: "run",
67
+ script: "pnpm dev",
68
+ });
69
+ const viaTask = await createScriptTerminalId({
70
+ projectId: "proj",
71
+ taskId: "feat-x",
72
+ kind: "run",
73
+ script: "pnpm dev",
74
+ });
75
+ expect(viaScope).toBe(viaTask);
76
+ });
77
+
78
+ it("rejects calls without scope or task", async () => {
79
+ await expect(
80
+ createScriptTerminalId({
81
+ projectId: "proj",
82
+ kind: "run",
83
+ script: "pnpm dev",
84
+ }),
85
+ ).rejects.toThrow(/scopeId/);
86
+ });
87
+ });
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Action contract for the v2 dispatcher.
3
+ *
4
+ * CLI and command-center clients import the schemas in this file — input +
5
+ * output Zod shapes for every action. The server-side dispatcher wires each
6
+ * name to a handler that consumes the input and returns the typed result.
7
+ *
8
+ * Adding a new action:
9
+ * 1. Define `<Name>InputSchemaZ` and `<Name>ResultSchemaZ` here
10
+ * 2. Add it to {@link ActionContractsZ} below
11
+ * 3. Add a handler in `handlers/` and register it in `registry.ts`
12
+ *
13
+ * Invariants:
14
+ * - Names are dot-namespaced (`<noun>.<verb>`)
15
+ * - Both schemas live here so the wire format never drifts between clients
16
+ * - Action names ARE the discriminator — keep them stable; never rename
17
+ */
18
+
19
+ import { z } from "zod";
20
+ import { IdeConfigSchema, PaneSchema } from "./ide-config.ts";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // project.openTerminal
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export const ProjectOpenTerminalInputZ = z.object({
27
+ name: z.string().min(1),
28
+ });
29
+ export const ProjectOpenTerminalResultZ = z.object({
30
+ sessionName: z.string(),
31
+ cwd: z.string().min(1),
32
+ terminalTabId: z.string(),
33
+ /**
34
+ * `true` when the dispatcher had to launch the tmux session as part of
35
+ * resolving the terminal. `false` when the session was already running.
36
+ */
37
+ launched: z.boolean(),
38
+ });
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // project.launch
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export const ProjectLaunchInputZ = z.object({
45
+ name: z.string().min(1),
46
+ });
47
+ export const ProjectLaunchResultZ = z.object({
48
+ sessionName: z.string(),
49
+ /**
50
+ * `false` when the session was already running (idempotent no-op),
51
+ * `true` when this call started a fresh session.
52
+ */
53
+ started: z.boolean(),
54
+ });
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // project.stop
58
+ // ---------------------------------------------------------------------------
59
+
60
+ export const ProjectStopInputZ = z.object({
61
+ name: z.string().min(1),
62
+ });
63
+ export const ProjectStopResultZ = z.object({
64
+ sessionName: z.string(),
65
+ /**
66
+ * `false` when no session was running (idempotent no-op),
67
+ * `true` when this call killed a session.
68
+ */
69
+ stopped: z.boolean(),
70
+ });
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // project.restart
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export const ProjectRestartInputZ = z.object({
77
+ name: z.string().min(1),
78
+ });
79
+ export const ProjectRestartResultZ = z.object({
80
+ sessionName: z.string(),
81
+ restarted: z.literal(true),
82
+ });
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // project.activate
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export const ProjectActivateInputZ = z.object({
89
+ name: z.string().min(1),
90
+ });
91
+ export const ProjectActivateResultZ = z.object({
92
+ active: z.boolean(),
93
+ projectName: z.string(),
94
+ });
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // terminal.respawn
98
+ // ---------------------------------------------------------------------------
99
+
100
+ export const TerminalRespawnInputZ = z.object({
101
+ sessionName: z.string().min(1),
102
+ terminalId: z.string().min(1),
103
+ /**
104
+ * Optional cwd override. Omit to respawn at the bridge's current cwd
105
+ * (re-using the `lastCwd` recorded by the PTY bridge).
106
+ */
107
+ cwd: z.string().min(1).optional(),
108
+ });
109
+ export const TerminalRespawnResultZ = z.object({
110
+ respawned: z.literal(true),
111
+ cwd: z.string().min(1),
112
+ });
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // terminal.stop
116
+ // ---------------------------------------------------------------------------
117
+
118
+ export const TerminalStopInputZ = z.object({
119
+ sessionName: z.string().min(1),
120
+ terminalId: z.string().min(1),
121
+ });
122
+ export const TerminalStopResultZ = z.object({
123
+ stopped: z.literal(true),
124
+ });
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // config.*
128
+ // ---------------------------------------------------------------------------
129
+
130
+ export const ConfigSetInputZ = z.object({
131
+ projectName: z.string().min(1).optional(),
132
+ path: z.string().min(1),
133
+ value: z.unknown(),
134
+ });
135
+ export const ConfigResultZ = z.object({
136
+ config: IdeConfigSchema,
137
+ });
138
+
139
+ export const ConfigAddPaneInputZ = PaneSchema.partial().extend({
140
+ projectName: z.string().min(1).optional(),
141
+ rowIndex: z.number().int().min(0),
142
+ });
143
+ export const ConfigAddPaneResultZ = ConfigResultZ;
144
+
145
+ export const ConfigRemovePaneInputZ = z.object({
146
+ projectName: z.string().min(1).optional(),
147
+ rowIndex: z.number().int().min(0),
148
+ paneIndex: z.number().int().min(0),
149
+ });
150
+ export const ConfigRemovePaneResultZ = ConfigResultZ;
151
+
152
+ export const ConfigAddRowInputZ = z.object({
153
+ projectName: z.string().min(1).optional(),
154
+ size: z.string().optional(),
155
+ });
156
+ export const ConfigAddRowResultZ = ConfigResultZ;
157
+
158
+ export const ConfigEnableTeamInputZ = z.object({
159
+ projectName: z.string().min(1).optional(),
160
+ name: z.string().min(1).optional(),
161
+ });
162
+ export const ConfigEnableTeamResultZ = ConfigResultZ;
163
+
164
+ export const ConfigDisableTeamInputZ = z.object({
165
+ projectName: z.string().min(1).optional(),
166
+ });
167
+ export const ConfigDisableTeamResultZ = ConfigResultZ;
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // app.*
171
+ // ---------------------------------------------------------------------------
172
+
173
+ export const AppSetRemoteAccessInputZ = z.object({
174
+ enabled: z.boolean(),
175
+ });
176
+ export const AppSetRemoteAccessResultZ = z.object({
177
+ enabled: z.boolean(),
178
+ url: z.string().nullable(),
179
+ token: z.string().nullable(),
180
+ qrPayload: z.string().nullable(),
181
+ });
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // daemon.*
185
+ // ---------------------------------------------------------------------------
186
+
187
+ export const DaemonShutdownInputZ = z.object({
188
+ reason: z.string().optional(),
189
+ });
190
+ export const DaemonShutdownResultZ = z.object({
191
+ stopping: z.literal(true),
192
+ });
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Registry of action contracts (name → input/output schemas)
196
+ // ---------------------------------------------------------------------------
197
+
198
+ export const ActionContractsZ = {
199
+ "project.openTerminal": {
200
+ input: ProjectOpenTerminalInputZ,
201
+ result: ProjectOpenTerminalResultZ,
202
+ },
203
+ "project.launch": {
204
+ input: ProjectLaunchInputZ,
205
+ result: ProjectLaunchResultZ,
206
+ },
207
+ "project.stop": {
208
+ input: ProjectStopInputZ,
209
+ result: ProjectStopResultZ,
210
+ },
211
+ "project.restart": {
212
+ input: ProjectRestartInputZ,
213
+ result: ProjectRestartResultZ,
214
+ },
215
+ "project.activate": {
216
+ input: ProjectActivateInputZ,
217
+ result: ProjectActivateResultZ,
218
+ },
219
+ "terminal.respawn": {
220
+ input: TerminalRespawnInputZ,
221
+ result: TerminalRespawnResultZ,
222
+ },
223
+ "terminal.stop": {
224
+ input: TerminalStopInputZ,
225
+ result: TerminalStopResultZ,
226
+ },
227
+ "config.set": {
228
+ input: ConfigSetInputZ,
229
+ result: ConfigResultZ,
230
+ },
231
+ "config.addPane": {
232
+ input: ConfigAddPaneInputZ,
233
+ result: ConfigAddPaneResultZ,
234
+ },
235
+ "config.removePane": {
236
+ input: ConfigRemovePaneInputZ,
237
+ result: ConfigRemovePaneResultZ,
238
+ },
239
+ "config.addRow": {
240
+ input: ConfigAddRowInputZ,
241
+ result: ConfigAddRowResultZ,
242
+ },
243
+ "config.enableTeam": {
244
+ input: ConfigEnableTeamInputZ,
245
+ result: ConfigEnableTeamResultZ,
246
+ },
247
+ "config.disableTeam": {
248
+ input: ConfigDisableTeamInputZ,
249
+ result: ConfigDisableTeamResultZ,
250
+ },
251
+ "app.setRemoteAccess": {
252
+ input: AppSetRemoteAccessInputZ,
253
+ result: AppSetRemoteAccessResultZ,
254
+ },
255
+ "daemon.shutdown": {
256
+ input: DaemonShutdownInputZ,
257
+ result: DaemonShutdownResultZ,
258
+ },
259
+ } as const;
260
+
261
+ export type ActionName = keyof typeof ActionContractsZ;
262
+
263
+ export const ACTION_NAMES = Object.keys(ActionContractsZ) as ActionName[];
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Typed input / result helpers
267
+ // ---------------------------------------------------------------------------
268
+
269
+ export type ActionInput<N extends ActionName> = z.infer<(typeof ActionContractsZ)[N]["input"]>;
270
+ export type ActionResult<N extends ActionName> = z.infer<(typeof ActionContractsZ)[N]["result"]>;
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Wire envelope (what the HTTP endpoint actually returns)
274
+ // ---------------------------------------------------------------------------
275
+
276
+ export interface ActionErrorEnvelope {
277
+ ok: false;
278
+ error: {
279
+ code: string;
280
+ message: string;
281
+ details?: unknown;
282
+ };
283
+ }
284
+
285
+ export interface ActionOkEnvelope<R> {
286
+ ok: true;
287
+ result: R;
288
+ }
289
+
290
+ export type ActionResponse<N extends ActionName> =
291
+ | ActionOkEnvelope<ActionResult<N>>
292
+ | ActionErrorEnvelope;
293
+
294
+ /**
295
+ * Validate that a string is a known action name. Used by the dispatcher to
296
+ * narrow URL params before looking up the handler.
297
+ */
298
+ export function isActionName(name: string): name is ActionName {
299
+ return name in ActionContractsZ;
300
+ }
301
+
302
+ /**
303
+ * Helper for a tagged WS broadcast frame so subscribers can decode without
304
+ * special-casing every action name.
305
+ */
306
+ export interface ActionCompleteFrame<N extends ActionName = ActionName> {
307
+ type: "action.complete";
308
+ name: N;
309
+ result: ActionResult<N>;
310
+ }