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,136 @@
1
+ import { runTmux } from "./runner.ts";
2
+
3
+ export interface TmuxPaneInfo {
4
+ index: number;
5
+ title: string | undefined;
6
+ width: number;
7
+ height: number;
8
+ active: boolean;
9
+ }
10
+
11
+ export function listPanes(session: string): TmuxPaneInfo[] {
12
+ const raw = (
13
+ runTmux(
14
+ [
15
+ "list-panes",
16
+ "-t",
17
+ session,
18
+ "-F",
19
+ "#{pane_index}|#{pane_title}|#{pane_width}|#{pane_height}|#{pane_active}",
20
+ ],
21
+ { encoding: "utf-8" },
22
+ ) as string
23
+ ).trim();
24
+
25
+ if (!raw) return [];
26
+
27
+ return raw.split("\n").map((line) => {
28
+ const [index, title, width, height, active] = line.split("|");
29
+ return {
30
+ index: Number.parseInt(index!, 10),
31
+ title,
32
+ width: Number.parseInt(width!, 10),
33
+ height: Number.parseInt(height!, 10),
34
+ active: active === "1",
35
+ };
36
+ });
37
+ }
38
+
39
+ export function splitPane(
40
+ targetPane: string,
41
+ direction: string,
42
+ cwd: string,
43
+ percent: number,
44
+ ): string {
45
+ return (
46
+ runTmux(
47
+ [
48
+ "split-window",
49
+ "-P",
50
+ "-F",
51
+ "#{pane_id}",
52
+ "-t",
53
+ targetPane,
54
+ direction === "vertical" ? "-v" : "-h",
55
+ "-c",
56
+ cwd,
57
+ "-p",
58
+ String(percent),
59
+ ],
60
+ { encoding: "utf-8" },
61
+ ) as string
62
+ ).trim();
63
+ }
64
+
65
+ export function sendLiteral(targetPane: string, text: string): void {
66
+ runTmux(["send-keys", "-t", targetPane, "-l", "--", text], { stdio: "inherit" });
67
+ runTmux(["send-keys", "-t", targetPane, "Enter"], { stdio: "inherit" });
68
+ }
69
+
70
+ export interface SendKeysOptions {
71
+ /** Append an Enter keystroke after the literal text. Defaults to true. */
72
+ enter?: boolean;
73
+ }
74
+
75
+ /**
76
+ * Send literal text to a pane. When `enter` is false, the text is delivered
77
+ * without a trailing Enter — useful for staging input for the user.
78
+ */
79
+ export function sendKeys(targetPane: string, text: string, options: SendKeysOptions = {}): void {
80
+ const { enter = true } = options;
81
+ runTmux(["send-keys", "-t", targetPane, "-l", "--", text], { stdio: "inherit" });
82
+ if (enter) {
83
+ runTmux(["send-keys", "-t", targetPane, "Enter"], { stdio: "inherit" });
84
+ }
85
+ }
86
+
87
+ export interface CapturePaneOptions {
88
+ /** Number of lines from the bottom of the buffer to include. */
89
+ lines?: number;
90
+ /** Capture from `scrollback` lines back to the bottom of the buffer. */
91
+ scrollback?: number;
92
+ }
93
+
94
+ /**
95
+ * Capture the visible/scrollback content of a tmux pane.
96
+ *
97
+ * - `lines: N` returns the last N lines (uses `-S -N`).
98
+ * - `scrollback: N` returns N scrollback lines back to the bottom.
99
+ * - With no options, captures only the visible viewport.
100
+ */
101
+ export function capturePane(targetPane: string, options: CapturePaneOptions = {}): string {
102
+ const args = ["capture-pane", "-t", targetPane, "-p", "-J"];
103
+ if (typeof options.scrollback === "number") {
104
+ args.push("-S", `-${options.scrollback}`);
105
+ } else if (typeof options.lines === "number") {
106
+ args.push("-S", `-${options.lines}`);
107
+ }
108
+ return (runTmux(args, { encoding: "utf-8" }) as string).replace(/\n+$/, "");
109
+ }
110
+
111
+ /**
112
+ * Capture the most-recent `lines` of a pane (defaults to 50).
113
+ */
114
+ export function captureRecent(targetPane: string, lines = 50): string {
115
+ return capturePane(targetPane, { lines });
116
+ }
117
+
118
+ export function getPaneCurrentCommand(targetPane: string): string {
119
+ return (
120
+ runTmux(["display-message", "-p", "-t", targetPane, "#{pane_current_command}"], {
121
+ encoding: "utf-8",
122
+ }) as string
123
+ ).trim();
124
+ }
125
+
126
+ export function selectPane(targetPane: string): void {
127
+ runTmux(["select-pane", "-t", targetPane], { stdio: "inherit" });
128
+ }
129
+
130
+ export function setPaneTitle(targetPane: string, title: string): void {
131
+ runTmux(["select-pane", "-t", targetPane, "-T", title], { stdio: "inherit" });
132
+ }
133
+
134
+ export function setPaneOption(targetPane: string, option: string, value: string): void {
135
+ runTmux(["set-option", "-pqt", targetPane, option, value]);
136
+ }
@@ -0,0 +1,501 @@
1
+ import { describe, it, beforeEach, afterEach, mock, expect } from "bun:test";
2
+ import {
3
+ _setExecutor,
4
+ _setSpawner,
5
+ TmuxError,
6
+ getSessionState,
7
+ hasSession,
8
+ killSession,
9
+ listPanes,
10
+ createDetachedSession,
11
+ splitPane,
12
+ sendLiteral,
13
+ getSessionVariable,
14
+ setSessionVariable,
15
+ startSessionMonitor,
16
+ stopSessionMonitor,
17
+ setPaneTitle,
18
+ selectPane,
19
+ getPaneCurrentCommand,
20
+ setSessionEnvironment,
21
+ attachSession,
22
+ runSessionCommand,
23
+ } from "./index.ts";
24
+
25
+ let mockExec;
26
+ let restoreExec;
27
+
28
+ beforeEach(() => {
29
+ mockExec = mock();
30
+ restoreExec = _setExecutor(mockExec);
31
+ });
32
+
33
+ afterEach(() => {
34
+ restoreExec();
35
+ });
36
+
37
+ // --- Helpers ---
38
+
39
+ function makeExecError(stderr) {
40
+ const err = new Error("Command failed");
41
+ err.stderr = stderr;
42
+ return err;
43
+ }
44
+
45
+ // --- Error classification (via public API) ---
46
+
47
+ describe("getSessionState", () => {
48
+ it("returns running: true when has-session succeeds", () => {
49
+ mockExec.mockImplementation(() => "");
50
+ const result = getSessionState("my-session");
51
+ expect(result).toEqual({ running: true, reason: null });
52
+ expect(mockExec.mock.calls[0][1]).toEqual(["has-session", "-t", "my-session"]);
53
+ });
54
+
55
+ it('returns SESSION_NOT_FOUND for "can\'t find session"', () => {
56
+ mockExec.mockImplementation(() => {
57
+ throw makeExecError("can't find session: my-session");
58
+ });
59
+ const result = getSessionState("my-session");
60
+ expect(result).toEqual({ running: false, reason: "SESSION_NOT_FOUND" });
61
+ });
62
+
63
+ it('returns TMUX_UNAVAILABLE for "connection refused"', () => {
64
+ mockExec.mockImplementation(() => {
65
+ throw makeExecError("error connecting to /tmp/tmux-1000/default (connection refused)");
66
+ });
67
+ const result = getSessionState("my-session");
68
+ expect(result).toEqual({ running: false, reason: "TMUX_UNAVAILABLE" });
69
+ });
70
+
71
+ it('returns TMUX_UNAVAILABLE for "no server running"', () => {
72
+ mockExec.mockImplementation(() => {
73
+ throw makeExecError("no server running on /tmp/tmux-1000/default");
74
+ });
75
+ const result = getSessionState("my-session");
76
+ expect(result).toEqual({ running: false, reason: "TMUX_UNAVAILABLE" });
77
+ });
78
+
79
+ it("throws for unknown errors", () => {
80
+ mockExec.mockImplementation(() => {
81
+ throw makeExecError("something totally unexpected");
82
+ });
83
+ expect(() => getSessionState("my-session")).toThrow(TmuxError);
84
+ });
85
+ });
86
+
87
+ describe("hasSession", () => {
88
+ it("returns true when session exists", () => {
89
+ mockExec.mockImplementation(() => "");
90
+ expect(hasSession("proj")).toBe(true);
91
+ });
92
+
93
+ it("returns false for SESSION_NOT_FOUND", () => {
94
+ mockExec.mockImplementation(() => {
95
+ throw makeExecError("can't find session: proj");
96
+ });
97
+ expect(hasSession("proj")).toBe(false);
98
+ });
99
+
100
+ it("returns false for TMUX_UNAVAILABLE", () => {
101
+ mockExec.mockImplementation(() => {
102
+ throw makeExecError("no server running on /tmp/tmux-1000/default");
103
+ });
104
+ expect(hasSession("proj")).toBe(false);
105
+ });
106
+
107
+ it("throws for unknown errors", () => {
108
+ mockExec.mockImplementation(() => {
109
+ throw makeExecError("unexpected failure");
110
+ });
111
+ expect(() => hasSession("proj")).toThrow(TmuxError);
112
+ });
113
+ });
114
+
115
+ describe("killSession", () => {
116
+ it("returns stopped: true when kill succeeds", () => {
117
+ mockExec.mockImplementation(() => "");
118
+ const result = killSession("proj");
119
+ expect(result).toEqual({ stopped: true, reason: null });
120
+ expect(mockExec.mock.calls[0][1]).toEqual(["kill-session", "-t", "proj"]);
121
+ });
122
+
123
+ it("returns stopped: false for missing session", () => {
124
+ mockExec.mockImplementation(() => {
125
+ throw makeExecError("can't find session: proj");
126
+ });
127
+ const result = killSession("proj");
128
+ expect(result).toEqual({ stopped: false, reason: "SESSION_NOT_FOUND" });
129
+ });
130
+
131
+ it("returns stopped: false for unavailable tmux", () => {
132
+ mockExec.mockImplementation(() => {
133
+ throw makeExecError("failed to connect to server");
134
+ });
135
+ const result = killSession("proj");
136
+ expect(result).toEqual({ stopped: false, reason: "TMUX_UNAVAILABLE" });
137
+ });
138
+
139
+ it("throws for unknown errors", () => {
140
+ mockExec.mockImplementation(() => {
141
+ throw makeExecError("disk error");
142
+ });
143
+ expect(() => killSession("proj")).toThrow(TmuxError);
144
+ });
145
+ });
146
+
147
+ // --- listPanes ---
148
+
149
+ describe("listPanes", () => {
150
+ it("parses multi-pane output correctly", () => {
151
+ mockExec.mockImplementation(() => "0|Editor|120|40|1\n1|Shell|80|40|0\n");
152
+ const panes = listPanes("proj");
153
+ expect(panes).toEqual([
154
+ { index: 0, title: "Editor", width: 120, height: 40, active: true },
155
+ { index: 1, title: "Shell", width: 80, height: 40, active: false },
156
+ ]);
157
+ });
158
+
159
+ it("returns empty array for empty output", () => {
160
+ mockExec.mockImplementation(() => " \n");
161
+ const panes = listPanes("proj");
162
+ expect(panes).toEqual([]);
163
+ });
164
+
165
+ it("handles pane titles containing pipe characters", () => {
166
+ // The split("|") will split on the first pipe in the title, causing misparse.
167
+ // This documents the current behavior — titles with pipes are truncated.
168
+ mockExec.mockImplementation(() => "0|A|B|120|40|0\n");
169
+ const panes = listPanes("proj");
170
+ // With pipe in title, index=0, title="A", width=NaN (from "B"), ...
171
+ expect(panes[0].index).toBe(0);
172
+ expect(panes[0].title).toBe("A");
173
+ });
174
+ });
175
+
176
+ // --- createDetachedSession ---
177
+
178
+ describe("createDetachedSession", () => {
179
+ it("returns trimmed pane ID", () => {
180
+ mockExec.mockImplementation(() => " %0\n");
181
+ const id = createDetachedSession("proj", "/workspace");
182
+ expect(id).toBe("%0");
183
+ });
184
+
185
+ it("uses default dimensions when not specified", () => {
186
+ mockExec.mockImplementation(() => "%0\n");
187
+ createDetachedSession("proj", "/workspace");
188
+ const args = mockExec.mock.calls[0][1];
189
+ expect(args.includes("200")).toBeTruthy(); // default cols
190
+ expect(args.includes("50")).toBeTruthy(); // default lines
191
+ });
192
+
193
+ it("passes custom dimensions", () => {
194
+ mockExec.mockImplementation(() => "%0\n");
195
+ createDetachedSession("proj", "/workspace", { cols: 300, lines: 80 });
196
+ const args = mockExec.mock.calls[0][1];
197
+ expect(args.includes("300")).toBeTruthy();
198
+ expect(args.includes("80")).toBeTruthy();
199
+ });
200
+ });
201
+
202
+ // --- splitPane ---
203
+
204
+ describe("splitPane", () => {
205
+ it("uses -v for vertical direction", () => {
206
+ mockExec.mockImplementation(() => "%1\n");
207
+ splitPane("%0", "vertical", "/workspace", 30);
208
+ const args = mockExec.mock.calls[0][1];
209
+ expect(args.includes("-v")).toBeTruthy();
210
+ expect(!args.includes("-h")).toBeTruthy();
211
+ });
212
+
213
+ it("uses -h for horizontal direction", () => {
214
+ mockExec.mockImplementation(() => "%2\n");
215
+ splitPane("%0", "horizontal", "/workspace", 50);
216
+ const args = mockExec.mock.calls[0][1];
217
+ expect(args.includes("-h")).toBeTruthy();
218
+ expect(!args.includes("-v")).toBeTruthy();
219
+ });
220
+
221
+ it("passes percent correctly", () => {
222
+ mockExec.mockImplementation(() => "%3\n");
223
+ splitPane("%0", "vertical", "/workspace", 42);
224
+ const args = mockExec.mock.calls[0][1];
225
+ const pIdx = args.indexOf("-p");
226
+ expect(args[pIdx + 1]).toBe("42");
227
+ });
228
+
229
+ it("returns trimmed pane ID", () => {
230
+ mockExec.mockImplementation(() => " %5\n");
231
+ const id = splitPane("%0", "vertical", "/workspace", 30);
232
+ expect(id).toBe("%5");
233
+ });
234
+ });
235
+
236
+ // --- sendLiteral ---
237
+
238
+ describe("sendLiteral", () => {
239
+ it("sends text with -l flag then Enter", () => {
240
+ mockExec.mockImplementation(() => "");
241
+ sendLiteral("%0", "echo hello");
242
+ expect(mockExec.mock.calls.length).toBe(2);
243
+ // First call: send-keys with -l and the text
244
+ const firstArgs = mockExec.mock.calls[0][1];
245
+ expect(firstArgs).toEqual(["send-keys", "-t", "%0", "-l", "--", "echo hello"]);
246
+ // Second call: send Enter
247
+ const secondArgs = mockExec.mock.calls[1][1];
248
+ expect(secondArgs).toEqual(["send-keys", "-t", "%0", "Enter"]);
249
+ });
250
+ });
251
+
252
+ // --- getSessionVariable / setSessionVariable ---
253
+
254
+ describe("getSessionVariable", () => {
255
+ it("returns trimmed value when variable exists", () => {
256
+ mockExec.mockImplementation(() => " some-value\n");
257
+ const value = getSessionVariable("proj", "@my_var");
258
+ expect(value).toBe("some-value");
259
+ });
260
+
261
+ it("returns null when variable is empty", () => {
262
+ mockExec.mockImplementation(() => "\n");
263
+ const value = getSessionVariable("proj", "@my_var");
264
+ expect(value).toBe(null);
265
+ });
266
+
267
+ it("returns null when show-option throws", () => {
268
+ mockExec.mockImplementation(() => {
269
+ throw makeExecError("can't find session: proj");
270
+ });
271
+ const value = getSessionVariable("proj", "@my_var");
272
+ expect(value).toBe(null);
273
+ });
274
+ });
275
+
276
+ describe("setSessionVariable", () => {
277
+ it("sets variable with correct args", () => {
278
+ mockExec.mockImplementation(() => "");
279
+ setSessionVariable("proj", "@my_var", "hello");
280
+ expect(mockExec.mock.calls[0][1]).toEqual(["set-option", "-t", "proj", "@my_var", "hello"]);
281
+ });
282
+ });
283
+
284
+ // --- startSessionMonitor / stopSessionMonitor ---
285
+
286
+ describe("startSessionMonitor", () => {
287
+ it("spawns detached process and stores PID", () => {
288
+ const fakeChild = { pid: 12345, unref: mock() };
289
+ const restoreSpawn = _setSpawner(mock(() => fakeChild));
290
+
291
+ mockExec.mockImplementation(() => "");
292
+ startSessionMonitor("proj", "/path/to/monitor.js");
293
+
294
+ // Check spawn was called correctly
295
+ expect(fakeChild.unref.mock.calls.length).toBe(1);
296
+
297
+ // First call checks for existing PID (show-option), then stores new PID (set-option)
298
+ const lastCall = mockExec.mock.calls[mockExec.mock.calls.length - 1];
299
+ const setArgs = lastCall[1];
300
+ expect(setArgs).toEqual(["set-option", "-t", "proj", "@monitor_pid", "12345"]);
301
+
302
+ restoreSpawn();
303
+ });
304
+
305
+ it("spawns the daemon under tsx (never bun — see T085/T087)", () => {
306
+ // Pinned to tsx so node-pty's onData callback fires; bun breaks it.
307
+ const fakeChild = { pid: 99, unref: mock() };
308
+ const mockSpawn = mock(() => fakeChild);
309
+ const restoreSpawn = _setSpawner(mockSpawn);
310
+
311
+ mockExec.mockImplementation(() => "");
312
+ startSessionMonitor("proj", "/path/to/monitor.ts");
313
+
314
+ const spawnCall = mockSpawn.mock.calls[0];
315
+ expect(spawnCall[0]).toBe("tsx");
316
+ const args = spawnCall[1];
317
+ expect(args).toEqual(["/path/to/monitor.ts", "proj", "0"]);
318
+
319
+ restoreSpawn();
320
+ });
321
+ });
322
+
323
+ describe("stopSessionMonitor", () => {
324
+ it("kills process group by stored PID", () => {
325
+ mockExec.mockImplementation(() => " 42\n");
326
+ const origKill = process.kill;
327
+ const killCalls: Array<{ pid: number; signal?: string }> = [];
328
+ process.kill = ((pid: number, signal?: string) => {
329
+ killCalls.push({ pid, signal });
330
+ }) as typeof process.kill;
331
+ try {
332
+ stopSessionMonitor("proj");
333
+ // First tries process group kill (negative PID)
334
+ expect(killCalls[0]?.pid).toBe(-42);
335
+ expect(killCalls[0]?.signal).toBe("SIGTERM");
336
+ } finally {
337
+ process.kill = origKill;
338
+ }
339
+ });
340
+
341
+ it("handles missing PID gracefully", () => {
342
+ mockExec.mockImplementation(() => {
343
+ throw makeExecError("can't find session: proj");
344
+ });
345
+ // Should not throw
346
+ stopSessionMonitor("proj");
347
+ });
348
+
349
+ it("handles empty PID gracefully", () => {
350
+ mockExec.mockImplementation(() => "\n");
351
+ // Should not throw (no pid to kill)
352
+ stopSessionMonitor("proj");
353
+ });
354
+ });
355
+
356
+ // --- Other public functions ---
357
+
358
+ describe("setPaneTitle", () => {
359
+ it("calls select-pane with -T flag", () => {
360
+ mockExec.mockImplementation(() => "");
361
+ setPaneTitle("%0", "My Pane");
362
+ expect(mockExec.mock.calls[0][1]).toEqual(["select-pane", "-t", "%0", "-T", "My Pane"]);
363
+ });
364
+ });
365
+
366
+ describe("selectPane", () => {
367
+ it("calls select-pane with target", () => {
368
+ mockExec.mockImplementation(() => "");
369
+ selectPane("%2");
370
+ expect(mockExec.mock.calls[0][1]).toEqual(["select-pane", "-t", "%2"]);
371
+ });
372
+ });
373
+
374
+ describe("getPaneCurrentCommand", () => {
375
+ it("returns trimmed command name", () => {
376
+ mockExec.mockImplementation(() => " zsh\n");
377
+ expect(getPaneCurrentCommand("%0")).toBe("zsh");
378
+ });
379
+ });
380
+
381
+ describe("setSessionEnvironment", () => {
382
+ it("calls set-environment with correct args", () => {
383
+ mockExec.mockImplementation(() => "");
384
+ setSessionEnvironment("proj", "PORT", 3000);
385
+ expect(mockExec.mock.calls[0][1]).toEqual(["set-environment", "-t", "proj", "PORT", "3000"]);
386
+ });
387
+ });
388
+
389
+ describe("attachSession", () => {
390
+ it("calls attach with correct args and stdio inherit", () => {
391
+ mockExec.mockImplementation(() => "");
392
+ attachSession("proj");
393
+ expect(mockExec.mock.calls[0][1]).toEqual(["attach", "-t", "proj"]);
394
+ expect(mockExec.mock.calls[0][2].stdio).toBe("inherit");
395
+ });
396
+ });
397
+
398
+ describe("runSessionCommand", () => {
399
+ it("passes args through to tmux", () => {
400
+ mockExec.mockImplementation(() => "");
401
+ runSessionCommand(["resize-pane", "-t", "%0", "-x", "100"]);
402
+ expect(mockExec.mock.calls[0][1]).toEqual(["resize-pane", "-t", "%0", "-x", "100"]);
403
+ });
404
+ });
405
+
406
+ // --- Debug logging ---
407
+
408
+ describe("debug logging", () => {
409
+ it("logs to stderr when globalThis.__tmuxIdeVerbose is true", () => {
410
+ const origVerbose = globalThis.__tmuxIdeVerbose;
411
+ const stderrCalls = [];
412
+ const origConsoleError = console.error;
413
+ console.error = (...args) => stderrCalls.push(args.join(" "));
414
+
415
+ globalThis.__tmuxIdeVerbose = true;
416
+ mockExec.mockImplementation(() => "");
417
+
418
+ try {
419
+ hasSession("test-session");
420
+ expect(stderrCalls.some((msg) => msg.includes("[tmux]"))).toBeTruthy();
421
+ expect(stderrCalls.some((msg) => msg.includes("has-session"))).toBeTruthy();
422
+ } finally {
423
+ globalThis.__tmuxIdeVerbose = origVerbose;
424
+ console.error = origConsoleError;
425
+ }
426
+ });
427
+
428
+ it("does not log when neither DEBUG nor verbose is set", () => {
429
+ const origVerbose = globalThis.__tmuxIdeVerbose;
430
+ const stderrCalls = [];
431
+ const origConsoleError = console.error;
432
+ console.error = (...args) => stderrCalls.push(args.join(" "));
433
+
434
+ globalThis.__tmuxIdeVerbose = false;
435
+ mockExec.mockImplementation(() => "");
436
+
437
+ try {
438
+ hasSession("test-session");
439
+ expect(stderrCalls.length).toBe(0);
440
+ } finally {
441
+ globalThis.__tmuxIdeVerbose = origVerbose;
442
+ console.error = origConsoleError;
443
+ }
444
+ });
445
+ });
446
+
447
+ // --- Error classification edge cases ---
448
+
449
+ describe("error classification", () => {
450
+ it('classifies "can\'t find window" as SESSION_NOT_FOUND', () => {
451
+ mockExec.mockImplementation(() => {
452
+ throw makeExecError("can't find window: proj:0");
453
+ });
454
+ const result = getSessionState("proj");
455
+ expect(result).toEqual({ running: false, reason: "SESSION_NOT_FOUND" });
456
+ });
457
+
458
+ it('classifies "unknown target" as SESSION_NOT_FOUND', () => {
459
+ mockExec.mockImplementation(() => {
460
+ throw makeExecError("unknown target: proj");
461
+ });
462
+ const result = getSessionState("proj");
463
+ expect(result).toEqual({ running: false, reason: "SESSION_NOT_FOUND" });
464
+ });
465
+
466
+ it('classifies "failed to connect to server" as TMUX_UNAVAILABLE', () => {
467
+ mockExec.mockImplementation(() => {
468
+ throw makeExecError("failed to connect to server: /tmp/tmux-1000/default");
469
+ });
470
+ const result = getSessionState("proj");
471
+ expect(result).toEqual({ running: false, reason: "TMUX_UNAVAILABLE" });
472
+ });
473
+
474
+ it('classifies "error connecting to" as TMUX_UNAVAILABLE', () => {
475
+ mockExec.mockImplementation(() => {
476
+ throw makeExecError("error connecting to /tmp/tmux-1000/default");
477
+ });
478
+ const result = getSessionState("proj");
479
+ expect(result).toEqual({ running: false, reason: "TMUX_UNAVAILABLE" });
480
+ });
481
+
482
+ it("handles Buffer stderr in error", () => {
483
+ mockExec.mockImplementation(() => {
484
+ const err = new Error("Command failed");
485
+ err.stderr = Buffer.from("can't find session: proj");
486
+ throw err;
487
+ });
488
+ const result = getSessionState("proj");
489
+ expect(result).toEqual({ running: false, reason: "SESSION_NOT_FOUND" });
490
+ });
491
+
492
+ it("falls back to error.message when stderr is empty", () => {
493
+ mockExec.mockImplementation(() => {
494
+ const err = new Error("can't find session: proj");
495
+ err.stderr = "";
496
+ throw err;
497
+ });
498
+ const result = getSessionState("proj");
499
+ expect(result).toEqual({ running: false, reason: "SESSION_NOT_FOUND" });
500
+ });
501
+ });