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,91 @@
1
+ import { execFileSync, spawn, type ExecFileSyncOptions } from "node:child_process";
2
+ import { TmuxError } from "./errors.ts";
3
+
4
+ const DEBUG = process.env.TMUX_IDE_DEBUG === "1";
5
+
6
+ const SESSION_NOT_FOUND_PATTERNS = ["can't find session", "can't find window", "unknown target"];
7
+
8
+ const TMUX_UNAVAILABLE_PATTERNS = [
9
+ "failed to connect to server",
10
+ "no server running",
11
+ "error connecting to",
12
+ "connection refused",
13
+ ];
14
+
15
+ declare global {
16
+ // Toggled by the CLI's --verbose flag.
17
+ var __tmuxIdeVerbose: boolean | undefined;
18
+ }
19
+
20
+ type Executor = typeof execFileSync;
21
+ type Spawner = typeof spawn;
22
+
23
+ let _executor: Executor = execFileSync;
24
+ let _spawner: Spawner = spawn;
25
+
26
+ /** @internal Replace the executor for testing. Returns a restore function. */
27
+ export function _setExecutor(fn: Executor): () => void {
28
+ const prev = _executor;
29
+ _executor = fn;
30
+ return () => {
31
+ _executor = prev;
32
+ };
33
+ }
34
+
35
+ /** @internal Replace the spawner for testing. Returns a restore function. */
36
+ export function _setSpawner(fn: Spawner): () => void {
37
+ const prev = _spawner;
38
+ _spawner = fn;
39
+ return () => {
40
+ _spawner = prev;
41
+ };
42
+ }
43
+
44
+ /** @internal Access the spawner (used by monitor.ts to keep one mock surface). */
45
+ export function _getSpawner(): Spawner {
46
+ return _spawner;
47
+ }
48
+
49
+ export function runTmux(args: string[], options: ExecFileSyncOptions = {}): string | Buffer {
50
+ if (DEBUG || globalThis.__tmuxIdeVerbose) {
51
+ console.error(` [tmux] ${args.join(" ")}`);
52
+ }
53
+
54
+ const execOptions: ExecFileSyncOptions = {
55
+ stdio: ["ignore", "pipe", "pipe"],
56
+ ...options,
57
+ };
58
+
59
+ try {
60
+ return _executor("tmux", args, execOptions);
61
+ } catch (error) {
62
+ throw classifyTmuxError(error);
63
+ }
64
+ }
65
+
66
+ function classifyTmuxError(error: unknown): TmuxError {
67
+ const detail = getErrorDetail(error).toLowerCase();
68
+
69
+ if (SESSION_NOT_FOUND_PATTERNS.some((pattern) => detail.includes(pattern))) {
70
+ return new TmuxError("tmux session was not found", "SESSION_NOT_FOUND", {
71
+ cause: error as Error,
72
+ });
73
+ }
74
+
75
+ if (TMUX_UNAVAILABLE_PATTERNS.some((pattern) => detail.includes(pattern))) {
76
+ return new TmuxError("tmux is unavailable or its socket is inaccessible", "TMUX_UNAVAILABLE", {
77
+ cause: error as Error,
78
+ });
79
+ }
80
+
81
+ return new TmuxError("tmux command failed", "TMUX_ERROR", {
82
+ cause: error as Error,
83
+ });
84
+ }
85
+
86
+ function getErrorDetail(error: unknown): string {
87
+ const stderr = (error as { stderr?: string | Buffer })?.stderr;
88
+ if (typeof stderr === "string" && stderr.length > 0) return stderr;
89
+ if (Buffer.isBuffer(stderr) && stderr.length > 0) return stderr.toString("utf-8");
90
+ return (error as Error)?.message ?? "";
91
+ }
@@ -0,0 +1,126 @@
1
+ import type { SessionState } from "@tmux-ide/contracts";
2
+ import { TmuxError } from "./errors.ts";
3
+ import { runTmux } from "./runner.ts";
4
+
5
+ export function getSessionState(session: string): SessionState {
6
+ try {
7
+ runTmux(["has-session", "-t", session]);
8
+ return { running: true, reason: null };
9
+ } catch (error) {
10
+ if (error instanceof TmuxError) {
11
+ if (error.code === "SESSION_NOT_FOUND") {
12
+ return { running: false, reason: "SESSION_NOT_FOUND" };
13
+ }
14
+ if (error.code === "TMUX_UNAVAILABLE") {
15
+ return { running: false, reason: "TMUX_UNAVAILABLE" };
16
+ }
17
+ }
18
+ throw error;
19
+ }
20
+ }
21
+
22
+ export function attachSession(session: string): void {
23
+ runTmux(["attach", "-t", session], { stdio: "inherit" });
24
+ }
25
+
26
+ export function hasSession(session: string): boolean {
27
+ try {
28
+ runTmux(["has-session", "-t", session]);
29
+ return true;
30
+ } catch (error) {
31
+ if (
32
+ error instanceof TmuxError &&
33
+ (error.code === "SESSION_NOT_FOUND" || error.code === "TMUX_UNAVAILABLE")
34
+ ) {
35
+ return false;
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Resolve the cwd of a tmux session by reading its first pane's
43
+ * `pane_current_path`. Used as a fallback when looking up a project that
44
+ * isn't in the registry but does have a live tmux session — e.g. when
45
+ * the user opens the dashboard against tmux sessions they spawned by
46
+ * other means. Returns null if the session has no panes or tmux can't
47
+ * resolve the path.
48
+ */
49
+ export function getSessionCwd(session: string): string | null {
50
+ try {
51
+ const raw = runTmux(["display-message", "-p", "-t", session, "#{pane_current_path}"], {
52
+ encoding: "utf-8",
53
+ }) as string;
54
+ const trimmed = raw.trim();
55
+ return trimmed.length > 0 ? trimmed : null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ export function killSession(session: string): { stopped: boolean; reason: string | null } {
62
+ try {
63
+ runTmux(["kill-session", "-t", session]);
64
+ return { stopped: true, reason: null };
65
+ } catch (error) {
66
+ if (error instanceof TmuxError) {
67
+ if (error.code === "SESSION_NOT_FOUND") {
68
+ return { stopped: false, reason: "SESSION_NOT_FOUND" };
69
+ }
70
+ if (error.code === "TMUX_UNAVAILABLE") {
71
+ return { stopped: false, reason: "TMUX_UNAVAILABLE" };
72
+ }
73
+ }
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ export function createDetachedSession(
79
+ session: string,
80
+ cwd: string,
81
+ { cols, lines }: { cols?: number; lines?: number } = {},
82
+ ): string {
83
+ return (
84
+ runTmux(
85
+ [
86
+ "new-session",
87
+ "-d",
88
+ "-P",
89
+ "-F",
90
+ "#{pane_id}",
91
+ "-s",
92
+ session,
93
+ "-c",
94
+ cwd,
95
+ "-x",
96
+ String(cols ?? 200),
97
+ "-y",
98
+ String(lines ?? 50),
99
+ ],
100
+ { encoding: "utf-8" },
101
+ ) as string
102
+ ).trim();
103
+ }
104
+
105
+ export function setSessionEnvironment(session: string, key: string, value: string | number): void {
106
+ runTmux(["set-environment", "-t", session, key, String(value)]);
107
+ }
108
+
109
+ export function getSessionVariable(session: string, name: string): string | null {
110
+ try {
111
+ const raw = runTmux(["show-option", "-qvt", session, name], {
112
+ encoding: "utf-8",
113
+ }) as string;
114
+ return raw.trim() || null;
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ export function setSessionVariable(session: string, name: string, value: string): void {
121
+ runTmux(["set-option", "-t", session, name, value]);
122
+ }
123
+
124
+ export function runSessionCommand(args: string[]): void {
125
+ runTmux(args, { stdio: "inherit" });
126
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { resolveTarget, type TmuxPaneTarget } from "./targeting.ts";
3
+ import type { TmuxPaneInfo } from "./panes.ts";
4
+
5
+ const PANES: TmuxPaneInfo[] = [
6
+ { index: 0, title: "Lead", width: 80, height: 24, active: true },
7
+ { index: 1, title: "Reviewer", width: 80, height: 24, active: false },
8
+ { index: 2, title: "Reviewer", width: 80, height: 24, active: false },
9
+ { index: 3, title: "Shell", width: 80, height: 24, active: false },
10
+ ];
11
+
12
+ describe("resolveTarget — byId", () => {
13
+ it("accepts an opaque %N tmux id and passes it through", () => {
14
+ const result = resolveTarget(PANES, { kind: "byId", id: "%42" }, "sess");
15
+ expect(result.target).toBe("%42");
16
+ });
17
+
18
+ it("treats bare-numeric id as a pane index", () => {
19
+ const result = resolveTarget(PANES, { kind: "byId", id: "1" }, "sess");
20
+ expect(result.target).toBe("sess.1");
21
+ expect(result.pane.title).toBe("Reviewer");
22
+ });
23
+
24
+ it("throws for non-numeric, non-%N ids", () => {
25
+ expect(() => resolveTarget(PANES, { kind: "byId", id: "not-a-number" }, "sess")).toThrow(
26
+ /Invalid pane id/,
27
+ );
28
+ });
29
+
30
+ it("throws when bare-numeric id doesn't match any pane", () => {
31
+ expect(() => resolveTarget(PANES, { kind: "byId", id: "99" }, "sess")).toThrow(/not found/i);
32
+ });
33
+ });
34
+
35
+ describe("resolveTarget — byIndex", () => {
36
+ it("resolves by exact index match", () => {
37
+ const result = resolveTarget(PANES, { kind: "byIndex", index: 3 }, "sess");
38
+ expect(result.target).toBe("sess.3");
39
+ expect(result.pane.title).toBe("Shell");
40
+ });
41
+
42
+ it("throws when index is missing", () => {
43
+ expect(() => resolveTarget(PANES, { kind: "byIndex", index: 99 }, "sess")).toThrow(
44
+ /not found/i,
45
+ );
46
+ });
47
+
48
+ it("omits the session prefix when no session is supplied", () => {
49
+ const result = resolveTarget(PANES, { kind: "byIndex", index: 0 });
50
+ expect(result.target).toBe("0");
51
+ });
52
+ });
53
+
54
+ describe("resolveTarget — byTitle", () => {
55
+ it("resolves a unique title", () => {
56
+ const result = resolveTarget(PANES, { kind: "byTitle", title: "Lead" }, "sess");
57
+ expect(result.target).toBe("sess.0");
58
+ expect(result.pane.index).toBe(0);
59
+ });
60
+
61
+ it("throws on ambiguous titles", () => {
62
+ expect(() => resolveTarget(PANES, { kind: "byTitle", title: "Reviewer" }, "sess")).toThrow(
63
+ /ambiguous/i,
64
+ );
65
+ });
66
+
67
+ it("throws when no pane matches", () => {
68
+ expect(() => resolveTarget(PANES, { kind: "byTitle", title: "Nonexistent" }, "sess")).toThrow(
69
+ /not found/i,
70
+ );
71
+ });
72
+ });
73
+
74
+ describe("resolveTarget — byRole", () => {
75
+ it("throws because byRole must be resolved by the daemon, not the bridge", () => {
76
+ expect(() => resolveTarget(PANES, { kind: "byRole", role: "lead" }, "sess")).toThrow(
77
+ /byRole.*daemon/i,
78
+ );
79
+ });
80
+ });
81
+
82
+ describe("resolveTarget — addressing-mode disambiguation", () => {
83
+ it("byIndex 1 and byId '1' resolve to the same pane (parity)", () => {
84
+ const a = resolveTarget(PANES, { kind: "byIndex", index: 1 }, "sess");
85
+ const b = resolveTarget(PANES, { kind: "byId", id: "1" }, "sess");
86
+ expect(a.target).toBe(b.target);
87
+ expect(a.pane.index).toBe(b.pane.index);
88
+ });
89
+
90
+ it("a numeric title doesn't collide with a numeric index", () => {
91
+ const panes: TmuxPaneInfo[] = [
92
+ { index: 0, title: "1", width: 80, height: 24, active: true },
93
+ { index: 1, title: "Lead", width: 80, height: 24, active: false },
94
+ ];
95
+ const byTitle = resolveTarget(panes, { kind: "byTitle", title: "1" }, "sess");
96
+ const byIndex = resolveTarget(panes, { kind: "byIndex", index: 1 }, "sess");
97
+ expect(byTitle.pane.index).toBe(0);
98
+ expect(byIndex.pane.index).toBe(1);
99
+ });
100
+
101
+ it("uses the first matching index when multiple panes share a title via byIndex", () => {
102
+ const target: TmuxPaneTarget = { kind: "byIndex", index: 2 };
103
+ const result = resolveTarget(PANES, target, "sess");
104
+ expect(result.pane.index).toBe(2);
105
+ expect(result.pane.title).toBe("Reviewer");
106
+ });
107
+ });
@@ -0,0 +1,90 @@
1
+ import type { TmuxPaneTarget } from "@tmux-ide/contracts";
2
+ import type { TmuxPaneInfo } from "./panes.ts";
3
+
4
+ export type { TmuxPaneTarget } from "@tmux-ide/contracts";
5
+
6
+ export interface ResolvedPane {
7
+ /** tmux target string suitable for passing to `-t` */
8
+ target: string;
9
+ pane: TmuxPaneInfo;
10
+ }
11
+
12
+ /**
13
+ * Resolve a typed `TmuxPaneTarget` against a list of panes (typically
14
+ * obtained from `listPanes(session)`).
15
+ *
16
+ * Disambiguation rules — the order is important; `byId` matches against
17
+ * the listing's tmux-format `pane_index` field for symmetry with how the
18
+ * CLI passes `%N`-style ids and bare integers, but `byIndex` is preferred
19
+ * when the caller actually knows it's an index. `byTitle` resolves to the
20
+ * single matching pane and throws if there are zero or more than one.
21
+ *
22
+ * `byRole` is resolved one layer up (in the daemon, against the ide.yml
23
+ * role map) and translated to one of the other modes before reaching here;
24
+ * if a `byRole` target arrives at the bridge, that's a layering bug and
25
+ * we throw rather than silently no-op.
26
+ */
27
+ export function resolveTarget(
28
+ panes: readonly TmuxPaneInfo[],
29
+ target: TmuxPaneTarget,
30
+ session?: string,
31
+ ): ResolvedPane {
32
+ switch (target.kind) {
33
+ case "byId": {
34
+ // Pane id may be either a tmux %N id (kept as opaque string) or a
35
+ // bare numeric index. We don't have the %N id in TmuxPaneInfo, so
36
+ // accept "%N" as opaque (caller knows the id) and fall through to
37
+ // index-matching for bare numerics.
38
+ if (target.id.startsWith("%")) {
39
+ // Opaque: trust the caller. We can't validate against panes[].
40
+ return {
41
+ target: target.id,
42
+ pane: panes[0] ?? {
43
+ index: -1,
44
+ title: undefined,
45
+ width: 0,
46
+ height: 0,
47
+ active: false,
48
+ },
49
+ };
50
+ }
51
+ const numeric = Number.parseInt(target.id, 10);
52
+ if (!Number.isFinite(numeric)) {
53
+ throw new Error(`Invalid pane id: ${target.id}`);
54
+ }
55
+ const found = panes.find((p) => p.index === numeric);
56
+ if (!found) {
57
+ throw new Error(`Pane not found by id: ${target.id}`);
58
+ }
59
+ return { target: paneTarget(session, found.index), pane: found };
60
+ }
61
+ case "byIndex": {
62
+ const found = panes.find((p) => p.index === target.index);
63
+ if (!found) {
64
+ throw new Error(`Pane not found by index: ${target.index}`);
65
+ }
66
+ return { target: paneTarget(session, found.index), pane: found };
67
+ }
68
+ case "byTitle": {
69
+ const matches = panes.filter((p) => p.title === target.title);
70
+ if (matches.length === 0) {
71
+ throw new Error(`Pane not found by title: ${target.title}`);
72
+ }
73
+ if (matches.length > 1) {
74
+ throw new Error(`Ambiguous pane title "${target.title}" matches ${matches.length} panes`);
75
+ }
76
+ return {
77
+ target: paneTarget(session, matches[0]!.index),
78
+ pane: matches[0]!,
79
+ };
80
+ }
81
+ case "byRole":
82
+ throw new Error(
83
+ `byRole targets must be resolved at the daemon layer before reaching the bridge (got role="${target.role}")`,
84
+ );
85
+ }
86
+ }
87
+
88
+ function paneTarget(session: string | undefined, index: number): string {
89
+ return session ? `${session}.${index}` : String(index);
90
+ }
@@ -16,7 +16,9 @@
16
16
  * node-only for CI) — run `pnpm build:tui` on a machine with bun.
17
17
  *
18
18
  * Cross-compile: pass `--target bun-<os>-<arch>` (e.g. bun-linux-x64) to build
19
- * for another platform. Defaults to the host target.
19
+ * for another platform. Defaults to the host target. Pass `--outfile <path>` to
20
+ * write somewhere other than the default dist path (the release workflow uses
21
+ * this to emit per-platform artifacts side by side).
20
22
  */
21
23
 
22
24
  import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin";
@@ -27,8 +29,7 @@ import { fileURLToPath } from "node:url";
27
29
  const here = dirname(fileURLToPath(import.meta.url));
28
30
  const repoRoot = resolve(here, "..");
29
31
  const entry = resolve(repoRoot, "packages/daemon/src/tui/main.ts");
30
- const outDir = resolve(repoRoot, "packages/daemon/dist/tui");
31
- const outfile = resolve(outDir, "tmux-ide-tui");
32
+ const defaultOutDir = resolve(repoRoot, "packages/daemon/dist/tui");
32
33
 
33
34
  if (!existsSync(entry)) {
34
35
  throw new Error(`[build-tui] entry not found: ${entry}`);
@@ -38,7 +39,13 @@ const targetArg = process.argv.indexOf("--target");
38
39
  const target =
39
40
  targetArg !== -1 ? process.argv[targetArg + 1] : `bun-${process.platform}-${process.arch}`;
40
41
 
41
- mkdirSync(outDir, { recursive: true });
42
+ const outfileArg = process.argv.indexOf("--outfile");
43
+ const outfile =
44
+ outfileArg !== -1
45
+ ? resolve(process.argv[outfileArg + 1])
46
+ : resolve(defaultOutDir, "tmux-ide-tui");
47
+
48
+ mkdirSync(dirname(outfile), { recursive: true });
42
49
 
43
50
  const start = Date.now();
44
51
  const result = await Bun.build({