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,514 @@
1
+ /**
2
+ * `tmux-ide restore` — rebuild the fleet from the last snapshot.
3
+ *
4
+ * The chrome updater continuously writes `~/.tmux-ide/snapshot.json`
5
+ * ({@link ./tui/chrome/snapshot.ts}); when the tmux server dies for real, this
6
+ * rebuilds every session it recorded — windows, split layouts, per-pane cwds,
7
+ * and titles — and re-adopts the sessions that were adopted.
8
+ *
9
+ * Safety first: an already-live session is NEVER clobbered (it's skipped), and
10
+ * recorded pane commands are NOT auto-run unless `--run-commands` is passed —
11
+ * a restored agent pane is a plain shell in the right directory with its title
12
+ * restored.
13
+ *
14
+ * `--resume-agents` (or `{ "restore": { "resumeAgents": true } }` in
15
+ * `~/.tmux-ide/config.json`) layers native agent-resume on top: a rebuilt
16
+ * claude pane that carries a recorded `@agent_session_id` relaunches as
17
+ * `claude --resume <id>`, reviving the actual conversation rather than opening
18
+ * a fresh shell. See {@link paneResumeCommand} for the exact decision table.
19
+ *
20
+ * {@link buildRestorePlan} + {@link paneResumeCommand} + {@link restorePrefs}
21
+ * are PURE (unit-tested); {@link restore} is the thin io wrapper that reads the
22
+ * snapshot + registry + live tmux + config, then executes the plan.
23
+ */
24
+ import { runTmux } from "@tmux-ide/tmux-bridge";
25
+ import { appConfigPath, loadAppConfig, parseAppConfig } from "./lib/app-config.ts";
26
+ import { IdeError } from "./lib/errors.ts";
27
+ import { listProjects } from "./lib/project-registry.ts";
28
+ import { adoptSession } from "./tui/chrome/statusline.ts";
29
+ import {
30
+ readSnapshot,
31
+ type FleetSnapshot,
32
+ type PaneSnapshot,
33
+ type SessionSnapshot,
34
+ } from "./tui/chrome/snapshot.ts";
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Pure plan
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * One restore action per snapshot session:
42
+ * - `skip` — a live session already owns the name; never clobber it.
43
+ * - `launch` — the session maps to a registry project WITH an ide.yml; the
44
+ * config is the source of truth, so relaunch it instead of a raw
45
+ * rebuild.
46
+ * - `rebuild` — raw reconstruction from the snapshot (windows/panes/layout).
47
+ */
48
+ export type RestoreAction =
49
+ | { kind: "skip"; session: string }
50
+ | { kind: "launch"; session: string; dir: string }
51
+ | { kind: "rebuild"; session: SessionSnapshot };
52
+
53
+ export interface RestorePlan {
54
+ actions: RestoreAction[];
55
+ /** Total panes across `rebuild` actions (what restore directly creates). */
56
+ paneCount: number;
57
+ }
58
+
59
+ /**
60
+ * PURE — decide what to do with each snapshot session.
61
+ *
62
+ * Sessions are processed in snapshot order. A name that's already live is
63
+ * skipped. Otherwise, when `ideProjects` maps the name to a directory (a
64
+ * registry project whose dir has an ide.yml), we prefer relaunching from that
65
+ * config; failing that, we raw-rebuild from the recorded windows/panes.
66
+ */
67
+ export function buildRestorePlan(
68
+ snapshot: FleetSnapshot,
69
+ liveSessionNames: Iterable<string>,
70
+ ideProjects: Map<string, string> = new Map(),
71
+ ): RestorePlan {
72
+ const live = new Set(liveSessionNames);
73
+ const actions: RestoreAction[] = [];
74
+ let paneCount = 0;
75
+ for (const session of snapshot.sessions) {
76
+ if (live.has(session.name)) {
77
+ actions.push({ kind: "skip", session: session.name });
78
+ continue;
79
+ }
80
+ const dir = ideProjects.get(session.name);
81
+ if (dir) {
82
+ actions.push({ kind: "launch", session: session.name, dir });
83
+ continue;
84
+ }
85
+ actions.push({ kind: "rebuild", session });
86
+ for (const window of session.windows) paneCount += window.panes.length;
87
+ }
88
+ return { actions, paneCount };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Pure — agent resume decision
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /** A session id is trusted only if it's the uuid alphabet — nothing shell-active. */
96
+ const SAFE_SESSION_ID = /^[A-Za-z0-9-]+$/;
97
+
98
+ /**
99
+ * PURE — the command to relaunch a pane so its agent conversation is revived,
100
+ * or `null` for "no resume — leave it a plain shell (or replay `command`)".
101
+ *
102
+ * Decision table (only fires when `resumeAgents` is on):
103
+ * - claude + a recorded `@agent_session_id` → `claude --resume <id>`. The id is
104
+ * a uuid, but we don't trust the snapshot: unless it matches
105
+ * {@link SAFE_SESSION_ID} (`[A-Za-z0-9-]`) we bail to `null` rather than risk
106
+ * feeding shell metacharacters into send-keys.
107
+ * - claude with NO session id → `null`. `claude --continue` would be too
108
+ * magical here (it resumes the most-recent conversation in the cwd, which may
109
+ * belong to a different pane); a fresh shell is the safe, predictable default.
110
+ * - any other agent → `null`. No native resume story yet.
111
+ * - `resumeAgents` off → always `null`.
112
+ */
113
+ export function paneResumeCommand(
114
+ pane: PaneSnapshot,
115
+ opts: { resumeAgents: boolean },
116
+ ): string | null {
117
+ if (!opts.resumeAgents) return null;
118
+ if (pane.agent !== "claude") return null;
119
+ const id = pane.agentSessionId;
120
+ if (!id || !SAFE_SESSION_ID.test(id)) return null;
121
+ return `claude --resume ${id}`;
122
+ }
123
+
124
+ /** PURE — how many of a session's panes would resume under `resumeAgents`. */
125
+ export function countResumableAgents(session: SessionSnapshot, resumeAgents: boolean): number {
126
+ let n = 0;
127
+ for (const window of session.windows) {
128
+ for (const pane of window.panes) {
129
+ if (paneResumeCommand(pane, { resumeAgents })) n++;
130
+ }
131
+ }
132
+ return n;
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Pure — restore preferences (~/.tmux-ide/config.json)
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /** Restore behaviour toggles (minimal, pre-M14). */
140
+ export interface RestorePrefs {
141
+ resumeAgents: boolean;
142
+ }
143
+
144
+ /** Default: don't resume agents unless asked (flag or config). */
145
+ export const DEFAULT_RESTORE_PREFS: RestorePrefs = { resumeAgents: false };
146
+
147
+ /**
148
+ * PURE — read `{ restore: { resumeAgents } }` out of a parsed config, falling
149
+ * back to {@link DEFAULT_RESTORE_PREFS} for anything missing or mistyped.
150
+ * Delegates to the shared {@link parseAppConfig} so restore parsing can't drift
151
+ * from the rest of the config.
152
+ */
153
+ export function restorePrefs(parsedConfig: unknown): RestorePrefs {
154
+ return parseAppConfig(parsedConfig).restore;
155
+ }
156
+
157
+ /** Absolute path to the shared config (honors `TMUX_IDE_CONFIG`). */
158
+ export function restoreConfigPath(): string {
159
+ return appConfigPath();
160
+ }
161
+
162
+ /** io — resolve restore prefs from the shared app config; missing/invalid → defaults. */
163
+ export function readRestorePrefs(): RestorePrefs {
164
+ return loadAppConfig().restore;
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // io — execute a rebuild action against live tmux
169
+ // ---------------------------------------------------------------------------
170
+
171
+ function tmuxCapture(args: string[]): string {
172
+ return runTmux(args, { encoding: "utf-8" }).toString().trim();
173
+ }
174
+
175
+ /** Options for one raw rebuild (see {@link rebuildSession}). */
176
+ interface RebuildOptions {
177
+ /** Replay each pane's recorded command (`--run-commands`). */
178
+ runCommands: boolean;
179
+ /** Relaunch resumable agent panes as `claude --resume <id>` (`--resume-agents`). */
180
+ resumeAgents: boolean;
181
+ }
182
+
183
+ /**
184
+ * Raw-rebuild one session from its snapshot: create the session + its first
185
+ * window, add the remaining windows, split each window's panes (with their
186
+ * cwds), apply the recorded layout, restore titles, set the active window, and
187
+ * relaunch each pane per {@link RebuildOptions}.
188
+ *
189
+ * Per pane, at most ONE command is ever sent: an agent-resume command (when
190
+ * eligible under {@link paneResumeCommand}) takes precedence and, if it fires,
191
+ * suppresses the `--run-commands` replay for that pane. Returns the titles of
192
+ * the panes that were resumed (for the report).
193
+ */
194
+ function rebuildSession(session: SessionSnapshot, opts: RebuildOptions): string[] {
195
+ const { runCommands, resumeAgents } = opts;
196
+ const resumedTitles: string[] = [];
197
+ const windows = session.windows;
198
+ if (windows.length === 0) {
199
+ runTmux(["new-session", "-d", "-s", session.name, "-c", session.cwd]);
200
+ return resumedTitles;
201
+ }
202
+
203
+ windows.forEach((window, w) => {
204
+ const windowCwd = window.panes[0]?.cwd || session.cwd;
205
+ // First window comes from new-session; the rest are added detached.
206
+ const windowId =
207
+ w === 0
208
+ ? tmuxCapture([
209
+ "new-session",
210
+ "-d",
211
+ "-P",
212
+ "-F",
213
+ "#{window_id}",
214
+ "-s",
215
+ session.name,
216
+ "-c",
217
+ windowCwd,
218
+ ])
219
+ : tmuxCapture([
220
+ "new-window",
221
+ "-d",
222
+ "-P",
223
+ "-F",
224
+ "#{window_id}",
225
+ "-t",
226
+ `${session.name}:`,
227
+ "-c",
228
+ windowCwd,
229
+ ]);
230
+
231
+ // The window opens with one pane (snapshot pane 0). Create the rest; the
232
+ // layout string below fixes geometry, so split direction is irrelevant.
233
+ const paneIds = [tmuxCapture(["display-message", "-p", "-t", windowId, "#{pane_id}"])];
234
+ for (let p = 1; p < window.panes.length; p++) {
235
+ const paneCwd = window.panes[p]!.cwd || windowCwd;
236
+ paneIds.push(
237
+ tmuxCapture([
238
+ "split-window",
239
+ "-d",
240
+ "-P",
241
+ "-F",
242
+ "#{pane_id}",
243
+ "-t",
244
+ paneIds[p - 1]!,
245
+ "-c",
246
+ paneCwd,
247
+ ]),
248
+ );
249
+ }
250
+
251
+ // Restore geometry from the recorded layout (tmux accepts the checksummed
252
+ // #{window_layout} string verbatim), then name the window.
253
+ if (window.layout) runTmux(["select-layout", "-t", windowId, window.layout]);
254
+ if (window.name) runTmux(["rename-window", "-t", windowId, window.name]);
255
+
256
+ // Restore per-pane titles (and optionally replay commands). Titles are
257
+ // paired by creation order, which matches the snapshot's index order.
258
+ window.panes.forEach((pane, p) => {
259
+ const paneId = paneIds[p];
260
+ if (!paneId) return;
261
+ if (pane.title) runTmux(["select-pane", "-t", paneId, "-T", pane.title]);
262
+ // Re-stamp agent identity so the next snapshot keeps it and 0037's
263
+ // agent-resume can find the conversation to revive.
264
+ if (pane.agentSessionId) {
265
+ runTmux(["set-option", "-p", "-t", paneId, "@agent_session_id", pane.agentSessionId]);
266
+ }
267
+ if (pane.agent) {
268
+ runTmux(["set-option", "-p", "-t", paneId, "@agent_hint", pane.agent]);
269
+ }
270
+ // Resume the agent conversation if eligible; that takes precedence over
271
+ // the recorded-command replay so a pane is never sent two commands.
272
+ const resumeCmd = paneResumeCommand(pane, { resumeAgents });
273
+ if (resumeCmd) {
274
+ runTmux(["send-keys", "-t", paneId, "-l", "--", resumeCmd]);
275
+ runTmux(["send-keys", "-t", paneId, "Enter"]);
276
+ resumedTitles.push(pane.title || paneId);
277
+ } else if (runCommands && pane.command) {
278
+ runTmux(["send-keys", "-t", paneId, "-l", "--", pane.command]);
279
+ runTmux(["send-keys", "-t", paneId, "Enter"]);
280
+ }
281
+ });
282
+ });
283
+
284
+ // Set the active window to whatever the snapshot had focused.
285
+ const activeIndex = windows.findIndex((w) => w.active);
286
+ if (activeIndex >= 0) {
287
+ runTmux(["select-window", "-t", `${session.name}:${windows[activeIndex]!.index}`]);
288
+ }
289
+ return resumedTitles;
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // io — the command
294
+ // ---------------------------------------------------------------------------
295
+
296
+ /** Registry sessions whose dir has an ide.yml → name -> dir (the launch path). */
297
+ function ideBackedProjects(): Map<string, string> {
298
+ const map = new Map<string, string>();
299
+ try {
300
+ for (const project of listProjects()) {
301
+ if (project.hasIdeYml && project.dir) map.set(project.name, project.dir);
302
+ }
303
+ } catch {
304
+ // no registry → nothing prefers a launch; everything raw-rebuilds
305
+ }
306
+ return map;
307
+ }
308
+
309
+ function liveSessions(): string[] {
310
+ try {
311
+ const raw = tmuxCapture(["list-sessions", "-F", "#{session_name}"]);
312
+ return raw ? raw.split("\n").filter(Boolean) : [];
313
+ } catch {
314
+ // no server / no sessions → nothing is live, rebuild everything
315
+ return [];
316
+ }
317
+ }
318
+
319
+ export interface RestoreOptions {
320
+ json?: boolean;
321
+ dryRun?: boolean;
322
+ runCommands?: boolean;
323
+ /** Relaunch resumable agent panes (`--resume-agents`); ORed with config default. */
324
+ resumeAgents?: boolean;
325
+ }
326
+
327
+ /** One rebuilt session's resumed agent panes (for the report). */
328
+ interface ResumedSession {
329
+ session: string;
330
+ panes: string[];
331
+ }
332
+
333
+ /**
334
+ * Rebuild the fleet from the last snapshot. Missing snapshot → friendly exit 1.
335
+ * `dryRun` prints the plan without touching tmux; `runCommands` opts into
336
+ * replaying recorded pane commands (off by default for safety); `resumeAgents`
337
+ * (flag ORed with the `~/.tmux-ide/config.json` default) revives agent
338
+ * conversations via `claude --resume <id>`.
339
+ */
340
+ export async function restore({
341
+ json = false,
342
+ dryRun = false,
343
+ runCommands = false,
344
+ resumeAgents = false,
345
+ }: RestoreOptions = {}): Promise<void> {
346
+ const snapshot = readSnapshot();
347
+ if (!snapshot) {
348
+ throw new IdeError(
349
+ "no snapshot yet — the updater writes one every ~30s while any session is adopted",
350
+ { code: "NO_SNAPSHOT", exitCode: 1 },
351
+ );
352
+ }
353
+
354
+ // The flag forces resume on; config makes it the default. No negative flag.
355
+ const resume = resumeAgents || readRestorePrefs().resumeAgents;
356
+ const plan = buildRestorePlan(snapshot, liveSessions(), ideBackedProjects());
357
+
358
+ if (dryRun) {
359
+ reportPlan(plan, snapshot, {
360
+ json,
361
+ dryRun: true,
362
+ restored: [],
363
+ launched: [],
364
+ resumed: [],
365
+ resumeAgents: resume,
366
+ });
367
+ return;
368
+ }
369
+
370
+ const restored: string[] = [];
371
+ const launched: string[] = [];
372
+ const resumed: ResumedSession[] = [];
373
+ const recordResumed = (session: string, panes: string[]) => {
374
+ if (panes.length) resumed.push({ session, panes });
375
+ };
376
+ for (const action of plan.actions) {
377
+ if (action.kind === "skip") continue;
378
+ if (action.kind === "launch") {
379
+ const ok = await launchProject(action.dir, json);
380
+ if (ok) launched.push(action.session);
381
+ else {
382
+ // Launch failed — fall back to a raw rebuild from the snapshot so the
383
+ // session still comes back.
384
+ const snap = snapshot.sessions.find((s) => s.name === action.session);
385
+ if (snap) {
386
+ recordResumed(snap.name, rebuildSession(snap, { runCommands, resumeAgents: resume }));
387
+ if (snap.adopted) safeAdopt(snap.name);
388
+ restored.push(action.session);
389
+ }
390
+ }
391
+ continue;
392
+ }
393
+ recordResumed(
394
+ action.session.name,
395
+ rebuildSession(action.session, { runCommands, resumeAgents: resume }),
396
+ );
397
+ if (action.session.adopted) safeAdopt(action.session.name);
398
+ restored.push(action.session.name);
399
+ }
400
+
401
+ reportPlan(plan, snapshot, {
402
+ json,
403
+ dryRun: false,
404
+ restored,
405
+ launched,
406
+ resumed,
407
+ resumeAgents: resume,
408
+ });
409
+ }
410
+
411
+ /** Adopt a restored session into the chrome; never let a chrome failure abort restore. */
412
+ function safeAdopt(session: string): void {
413
+ try {
414
+ adoptSession(session);
415
+ } catch {
416
+ // chrome is optional — the session is already rebuilt
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Relaunch a project from its ide.yml (config is the source of truth). Returns
422
+ * false on any failure so the caller can fall back to a raw rebuild. Launch's
423
+ * own stdout is swallowed in `--json` mode so the report stays parseable.
424
+ */
425
+ async function launchProject(dir: string, json: boolean): Promise<boolean> {
426
+ const restoreLog = console.log;
427
+ if (json) console.log = () => {};
428
+ try {
429
+ const { launch } = await import("./launch.ts");
430
+ await launch(dir, { attach: false });
431
+ return true;
432
+ } catch {
433
+ return false;
434
+ } finally {
435
+ console.log = restoreLog;
436
+ }
437
+ }
438
+
439
+ interface ReportContext {
440
+ json: boolean;
441
+ dryRun: boolean;
442
+ restored: string[];
443
+ launched: string[];
444
+ /** Per-session resumed agent panes (empty on dry-run — see `resumeAgents`). */
445
+ resumed: ResumedSession[];
446
+ /** Whether resume is on (drives the dry-run `(would resume N agents)` note). */
447
+ resumeAgents: boolean;
448
+ }
449
+
450
+ function reportPlan(
451
+ plan: RestorePlan,
452
+ snapshot: FleetSnapshot,
453
+ { json, dryRun, restored, launched, resumed, resumeAgents }: ReportContext,
454
+ ): void {
455
+ const skipped = plan.actions.filter((a) => a.kind === "skip").map((a) => a.session);
456
+ const willLaunch = plan.actions.filter((a) => a.kind === "launch").map((a) => a.session);
457
+ const willRebuild = plan.actions.filter((a) => a.kind === "rebuild").map((a) => a.session.name);
458
+ const resumedPanes = resumed.reduce((n, r) => n + r.panes.length, 0);
459
+
460
+ if (json) {
461
+ console.log(
462
+ JSON.stringify(
463
+ {
464
+ dryRun,
465
+ savedAt: snapshot.savedAt,
466
+ skipped,
467
+ launched: dryRun ? willLaunch : launched,
468
+ restored: dryRun ? willRebuild : restored,
469
+ panes: plan.paneCount,
470
+ resumeAgents,
471
+ resumedPanes,
472
+ resumed,
473
+ },
474
+ null,
475
+ 2,
476
+ ),
477
+ );
478
+ return;
479
+ }
480
+
481
+ if (dryRun) {
482
+ console.log(`Restore plan (snapshot from ${snapshot.savedAt}):`);
483
+ for (const action of plan.actions) {
484
+ if (action.kind === "skip") {
485
+ console.log(` skip ${action.session} (already running)`);
486
+ } else if (action.kind === "launch") {
487
+ console.log(` launch ${action.session} (ide.yml at ${action.dir})`);
488
+ } else {
489
+ const w = action.session.windows.length;
490
+ const p = action.session.windows.reduce((n, win) => n + win.panes.length, 0);
491
+ const wouldResume = countResumableAgents(action.session, resumeAgents);
492
+ const resumeNote = wouldResume
493
+ ? `, would resume ${wouldResume} agent${wouldResume === 1 ? "" : "s"}`
494
+ : "";
495
+ console.log(
496
+ ` rebuild ${action.session.name} (${w} window${w === 1 ? "" : "s"}, ${p} pane${p === 1 ? "" : "s"}${resumeNote})`,
497
+ );
498
+ }
499
+ }
500
+ return;
501
+ }
502
+
503
+ const resumedBySession = new Map(resumed.map((r) => [r.session, r.panes.length]));
504
+ const resumeSuffix = (name: string) => {
505
+ const n = resumedBySession.get(name) ?? 0;
506
+ return n ? ` (resumed ${n} agent${n === 1 ? "" : "s"})` : "";
507
+ };
508
+ const parts: string[] = [];
509
+ if (restored.length)
510
+ parts.push(`rebuilt ${restored.map((s) => `${s}${resumeSuffix(s)}`).join(", ")}`);
511
+ if (launched.length) parts.push(`launched ${launched.join(", ")}`);
512
+ if (skipped.length) parts.push(`skipped ${skipped.join(", ")} (already running)`);
513
+ console.log(parts.length ? `Restored: ${parts.join("; ")}` : "Nothing to restore.");
514
+ }
@@ -0,0 +1,2 @@
1
+ // Schemas extracted to @tmux-ide/contracts (T056). Re-export shim.
2
+ export * from "@tmux-ide/contracts";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Filesystem browsing contracts. Powers the dashboard's directory picker
3
+ * (used in AddProjectDialog and elsewhere). The shape mirrors t3code's
4
+ * `FilesystemBrowseResult` so the consuming pattern (browse on partial
5
+ * path, render entries, click to navigate) ports straight across.
6
+ *
7
+ * The shape is FROZEN — the dashboard imports these via `@tmux-ide/schemas`.
8
+ * Add new fields by appending optional properties; do not rename existing
9
+ * fields without bumping a major.
10
+ */
11
+
12
+ import { z } from "zod";
13
+
14
+ export const FilesystemEntrySchemaZ = z.object({
15
+ /** Basename only (e.g. "src", "package.json"). Never contains a slash. */
16
+ name: z.string(),
17
+ /** Absolute, canonical path to the entry. */
18
+ fullPath: z.string(),
19
+ /** True if the entry is a directory (or a symlink pointing at one). */
20
+ isDir: z.boolean(),
21
+ /** True if the dirent itself is a symlink (regardless of target type). */
22
+ isSymlink: z.boolean(),
23
+ });
24
+ export type FilesystemEntry = z.infer<typeof FilesystemEntrySchemaZ>;
25
+
26
+ export const FilesystemBrowseResultSchemaZ = z.object({
27
+ /** Canonical absolute path of the directory listed. */
28
+ path: z.string(),
29
+ /** Parent directory path, or null if at filesystem / sandbox root. */
30
+ parentPath: z.string().nullable(),
31
+ /** Directories first, then files; alphabetical (case-insensitive) within group. */
32
+ entries: z.array(FilesystemEntrySchemaZ),
33
+ });
34
+ export type FilesystemBrowseResult = z.infer<typeof FilesystemBrowseResultSchemaZ>;
@@ -0,0 +1,2 @@
1
+ // Schemas extracted to @tmux-ide/contracts (T056). Re-export shim.
2
+ export * from "@tmux-ide/contracts";
@@ -0,0 +1,59 @@
1
+ export {
2
+ PaneSchema,
3
+ RowSchema,
4
+ ThemeConfigSchema,
5
+ OrchestratorYamlConfigSchema,
6
+ IdeConfigSchema,
7
+ PaneActionSchema,
8
+ SessionStateSchema,
9
+ } from "./ide-config.ts";
10
+
11
+ export type {
12
+ Pane,
13
+ Row,
14
+ ThemeConfig,
15
+ OrchestratorYamlConfig,
16
+ IdeConfig,
17
+ PaneAction,
18
+ SessionState,
19
+ } from "./ide-config.ts";
20
+
21
+ export { SessionOverviewSchemaZ, PaneInfoSchemaZ } from "./domain.ts";
22
+
23
+ export type { SessionOverview, PaneInfo } from "./domain.ts";
24
+
25
+ export { ClientFrameSchemaZ, ServerFrameSchemaZ, SessionSnapshotSchemaZ } from "./ws-events.ts";
26
+
27
+ export type { ClientFrame, ServerFrame, SessionSnapshot } from "./ws-events.ts";
28
+
29
+ export {
30
+ RegisteredProjectSchemaZ,
31
+ RegisterProjectRequestSchemaZ,
32
+ InitProjectRequestSchemaZ,
33
+ ProjectTemplateSchemaZ,
34
+ } from "./registry.ts";
35
+
36
+ export type {
37
+ RegisteredProject,
38
+ RegisterProjectRequest,
39
+ InitProjectRequest,
40
+ ProjectTemplate,
41
+ } from "./registry.ts";
42
+
43
+ export { FilesystemEntrySchemaZ, FilesystemBrowseResultSchemaZ } from "./filesystem.ts";
44
+
45
+ export type { FilesystemEntry, FilesystemBrowseResult } from "./filesystem.ts";
46
+
47
+ export {
48
+ ProjectInspectDetectedSchemaZ,
49
+ ProjectInspectSchemaZ,
50
+ InspectFilesystemRequestSchemaZ,
51
+ OnboardProjectRequestSchemaZ,
52
+ } from "./inspect.ts";
53
+
54
+ export type {
55
+ ProjectInspectDetected,
56
+ ProjectInspect,
57
+ InspectFilesystemRequest,
58
+ OnboardProjectRequest,
59
+ } from "./inspect.ts";
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Project inspect + onboard contracts. Used by the dashboard's "Add project"
3
+ * dialog to inspect an arbitrary directory (without registering it) and to
4
+ * onboard a directory that has no `ide.yml` yet (compose, write, register).
5
+ *
6
+ * The shape is FROZEN — the dashboard imports these via `@tmux-ide/schemas`.
7
+ */
8
+
9
+ import { z } from "zod";
10
+
11
+ export const ProjectInspectDetectedSchemaZ = z.object({
12
+ /** Detected package manager from lockfile, or `null`. */
13
+ packageManager: z.enum(["pnpm", "npm", "yarn", "bun"]).nullable(),
14
+ /** Detected frameworks (e.g. `["next", "convex"]`). Empty array when none. */
15
+ frameworks: z.array(z.string()),
16
+ /** Suggested dev command (e.g. `pnpm dev`). `null` if no dev script found. */
17
+ devCommand: z.string().nullable(),
18
+ /** Suggested test command (e.g. `pnpm test`). `null` if no test script found. */
19
+ testCommand: z.string().nullable(),
20
+ });
21
+ export type ProjectInspectDetected = z.infer<typeof ProjectInspectDetectedSchemaZ>;
22
+
23
+ export const ProjectInspectSchemaZ = z.object({
24
+ /** Sanitized basename of the directory — safe to use as a tmux session name. */
25
+ name: z.string(),
26
+ /** Absolute, canonical path to the directory. */
27
+ dir: z.string(),
28
+ /** Whether `<dir>/ide.yml` exists. */
29
+ hasIdeYml: z.boolean(),
30
+ /** Git remote origin URL, or `null` if not a git repo / no origin / probe failed. */
31
+ gitOrigin: z.string().nullable(),
32
+ /** Current git branch, or `null` if not a git repo / detached HEAD / probe failed. */
33
+ gitBranch: z.string().nullable(),
34
+ /** Detected stack signals (reuses `tmux-ide detect` logic). */
35
+ detected: ProjectInspectDetectedSchemaZ,
36
+ });
37
+ export type ProjectInspect = z.infer<typeof ProjectInspectSchemaZ>;
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // REST request bodies
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export const InspectFilesystemRequestSchemaZ = z.object({
44
+ dir: z.string().min(1),
45
+ });
46
+ export type InspectFilesystemRequest = z.infer<typeof InspectFilesystemRequestSchemaZ>;
47
+
48
+ export const OnboardProjectRequestSchemaZ = z.object({
49
+ dir: z.string().min(1),
50
+ /** Optional override for the project name — defaults to inspect.name. */
51
+ name: z.string().min(1).optional(),
52
+ /** 1, 2, or 3 — how many Claude panes to scaffold in the top row. */
53
+ agents: z.number().int().min(1).max(3),
54
+ /**
55
+ * Optional per-agent pane titles. When provided, length must equal
56
+ * `agents`; the server uses these as `title:` for the Claude panes
57
+ * instead of the canonical `Lead`/`Teammate N`/`Claude N` defaults.
58
+ */
59
+ agentNames: z.array(z.string().min(1)).optional(),
60
+ /** Dev server command (e.g. `pnpm dev`). Omit / null to skip the dev pane. */
61
+ devCommand: z.string().min(1).nullable().optional(),
62
+ /** Test command (e.g. `pnpm test`). Currently informational; stored for later. */
63
+ testCommand: z.string().min(1).nullable().optional(),
64
+ /** Lint command (e.g. `pnpm lint`). Currently informational; stored for later. */
65
+ lintCommand: z.string().min(1).nullable().optional(),
66
+ });
67
+ export type OnboardProjectRequest = z.infer<typeof OnboardProjectRequestSchemaZ>;