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,527 @@
1
+ /**
2
+ * The SIDEBAR — the app's persistent nav column.
3
+ *
4
+ * A narrow, always-visible left column that renders the whole fleet as a tree
5
+ * — project → session → window — with live status glyphs, and jumps the viewing
6
+ * client anywhere on enter/click. Where the switcher popup (`../../tui/team`)
7
+ * is a transient overlay that switch-clients and exits, the sidebar is a real
8
+ * tmux pane that lives inside a session and persists; it drives the SAME shared
9
+ * data layer (`listTeamProjects`) and the SAME flat tree-node cursor
10
+ * (`treeNodes`/`findCursor`) so the two never drift.
11
+ *
12
+ * Client resolution: the pane lives IN a session, so a switch must retarget the
13
+ * CLIENT viewing it. We resolve that client from inside the pane via
14
+ * `display-message -p '#{client_name}'` (uses the inherited `$TMUX_PANE`),
15
+ * falling back to the most-recently-active attached client. Unlike the popup we
16
+ * keep `$TMUX` intact — the column is long-lived and re-resolves the client on
17
+ * every switch.
18
+ *
19
+ * Runs under bun (JSX via the @opentui/solid preload); spawned by
20
+ * `tmux-ide sidebar-toggle` / the `sidebar: true` ide.yml sugar.
21
+ */
22
+ import { parseArgs } from "node:util";
23
+ import { execFileSync } from "node:child_process";
24
+ import { render, useKeyboard, useTerminalDimensions } from "@opentui/solid";
25
+ import { RGBA, TextAttributes, type MouseEvent } from "@opentui/core";
26
+ import { createSignal, createEffect, onMount, onCleanup, For, Show } from "solid-js";
27
+ import { runTmux } from "@tmux-ide/tmux-bridge";
28
+ import { createTheme } from "../lib/theme.ts";
29
+ import { getAppConfig } from "../../lib/app-config.ts";
30
+ import { matchGrammar } from "../lib/grammar.ts";
31
+ import { HelpOverlay, type WidgetKey } from "../lib/help-overlay.tsx";
32
+ import { createStatusTracker, type AgentStatus } from "../../tui/detect/classify.ts";
33
+ import { listTeamProjects, type TeamProject } from "../../tui/team/projects.ts";
34
+ import { type TeamSession, type TeamWindow } from "../../tui/team/sessions.ts";
35
+ import { fuzzyFilter } from "../../tui/team/fuzzy.ts";
36
+ import { wrapIndex } from "../../tui/team/nav.ts";
37
+ import { treeNodes, findCursor } from "../../tui/team/tree.ts";
38
+ import { nextInput } from "../../tui/team/input.ts";
39
+ import { isDoubleClick, type ClickRecord } from "../../tui/team/mouse.ts";
40
+
41
+ const { values: argv } = parseArgs({
42
+ options: {
43
+ theme: { type: "string" },
44
+ session: { type: "string" },
45
+ dir: { type: "string" },
46
+ },
47
+ strict: false,
48
+ });
49
+
50
+ function parseThemeArg(raw: string | undefined): Record<string, string> | undefined {
51
+ if (!raw) return undefined;
52
+ try {
53
+ return JSON.parse(raw);
54
+ } catch {
55
+ return undefined;
56
+ }
57
+ }
58
+
59
+ function toRGBA(c: { r: number; g: number; b: number; a: number }): RGBA {
60
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
61
+ }
62
+
63
+ const STATUS_GLYPH: Record<AgentStatus, string> = {
64
+ blocked: "●",
65
+ working: "●",
66
+ done: "●",
67
+ idle: "●",
68
+ unknown: "·",
69
+ };
70
+
71
+ /**
72
+ * Resolve the tmux client viewing this pane. `display-message -p
73
+ * '#{client_name}'` uses the inherited `$TMUX_PANE` to name the client — it
74
+ * returns the right one even with several clients attached (the popup relies on
75
+ * the same trick). Falls back to the most-recently-active attached client when
76
+ * the direct read is empty, and to null when there are no clients at all.
77
+ */
78
+ function resolveClient(): string | null {
79
+ try {
80
+ const name = runTmux(["display-message", "-p", "#{client_name}"]).toString().trim();
81
+ if (name.length > 0) return name;
82
+ } catch {
83
+ // fall through to the activity-sorted fallback
84
+ }
85
+ try {
86
+ const raw = runTmux(["list-clients", "-F", "#{client_activity} #{client_name}"])
87
+ .toString()
88
+ .trim();
89
+ const newest = raw
90
+ .split("\n")
91
+ .filter(Boolean)
92
+ .map((line) => {
93
+ const sp = line.indexOf(" ");
94
+ return { activity: Number(line.slice(0, sp)), name: line.slice(sp + 1) };
95
+ })
96
+ .sort((a, b) => b.activity - a.activity)[0];
97
+ return newest?.name ?? null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /** The session this sidebar's client is viewing: the `--session` arg, else the
104
+ * spawn-time env, else a live `display-message`. Marks the "you are here" row. */
105
+ function resolveCurrentSession(): string {
106
+ const fromArg = typeof argv.session === "string" ? argv.session.trim() : "";
107
+ if (fromArg) return fromArg;
108
+ const fromEnv = process.env.TMUX_IDE_SESSION?.trim();
109
+ if (fromEnv) return fromEnv;
110
+ try {
111
+ return execFileSync("tmux", ["display-message", "-p", "#{session_name}"], {
112
+ encoding: "utf8",
113
+ stdio: ["ignore", "pipe", "ignore"],
114
+ }).trim();
115
+ } catch {
116
+ return "";
117
+ }
118
+ }
119
+
120
+ /** Truncate to `width` visible columns with a trailing ellipsis. Names in a
121
+ * ~30-col column must never wrap the row. */
122
+ function trunc(s: string, width: number): string {
123
+ if (width <= 0) return "";
124
+ if (s.length <= width) return s;
125
+ if (width === 1) return "…";
126
+ return `${s.slice(0, width - 1)}…`;
127
+ }
128
+
129
+ /** Sidebar keys beyond the shared grammar — listed in the `?` overlay. */
130
+ const WIDGET_KEYS: WidgetKey[] = [{ key: "r", label: "refresh fleet" }];
131
+
132
+ render(() => {
133
+ const theme = createTheme(
134
+ parseThemeArg(typeof argv.theme === "string" ? argv.theme : undefined),
135
+ getAppConfig().theme,
136
+ );
137
+ const statusColor: Record<AgentStatus, RGBA> = {
138
+ blocked: toRGBA(theme.statusBlocked),
139
+ working: toRGBA(theme.statusWorking),
140
+ done: toRGBA(theme.statusDone),
141
+ idle: toRGBA(theme.statusIdle),
142
+ unknown: toRGBA(theme.statusUnknown),
143
+ };
144
+
145
+ const tracker = createStatusTracker();
146
+ const currentSession = resolveCurrentSession();
147
+
148
+ const [projects, setProjects] = createSignal<TeamProject[]>(listTeamProjects(tracker));
149
+ // The flat tree cursor: active project / session (-1 = project row) / window
150
+ // (-1 = session or project row). Same shape the picker navigates.
151
+ const [activeProject, setActiveProject] = createSignal(0);
152
+ const [activeSession, setActiveSession] = createSignal(-1);
153
+ const [activeWindow, setActiveWindow] = createSignal(-1);
154
+ const [filterMode, setFilterMode] = createSignal(false);
155
+ const [filterQuery, setFilterQuery] = createSignal("");
156
+ const [helpOpen, setHelpOpen] = createSignal(false);
157
+ const [message, setMessage] = createSignal("");
158
+ const dimensions = useTerminalDimensions();
159
+
160
+ /** Visible projects: fuzzy-filtered while filtering, else all. */
161
+ const visibleProjects = () =>
162
+ filterMode()
163
+ ? fuzzyFilter(filterQuery(), projects(), (p) => p.name).map((m) => m.item)
164
+ : projects();
165
+
166
+ const activeProj = (): TeamProject | undefined => visibleProjects()[activeProject()];
167
+ const activeSess = (): TeamSession | undefined => activeProj()?.sessions[activeSession()];
168
+ const activeWin = (): TeamWindow | undefined => {
169
+ const wi = activeWindow();
170
+ return wi >= 0 ? activeSess()?.windowList[wi] : undefined;
171
+ };
172
+
173
+ /** The tree node union — EVERY project's sessions expanded (the sidebar shows
174
+ * the whole fleet), windows only under the active/cursor session. */
175
+ const nodes = () =>
176
+ treeNodes(visibleProjects(), activeProject(), activeSession(), { expandAllProjects: true });
177
+
178
+ /** Move the flat cursor by `delta`, wrapping across the node union. */
179
+ function move(delta: number) {
180
+ const list = nodes();
181
+ if (list.length === 0) return;
182
+ const cur = findCursor(list, {
183
+ pi: activeProject(),
184
+ si: activeSession(),
185
+ wi: activeWindow(),
186
+ });
187
+ const next = list[wrapIndex(cur >= 0 ? cur : 0, delta, list.length)]!;
188
+ setActiveProject(next.pi);
189
+ setActiveSession(next.si);
190
+ setActiveWindow(next.wi);
191
+ }
192
+
193
+ // Park the cursor on the CURRENT session at startup so "you are here" is
194
+ // selected and its windows auto-expand. Runs once against the initial load.
195
+ onMount(() => {
196
+ const projs = projects();
197
+ for (let pi = 0; pi < projs.length; pi++) {
198
+ const si = projs[pi]!.sessions.findIndex((s) => s.name === currentSession);
199
+ if (si >= 0) {
200
+ setActiveProject(pi);
201
+ setActiveSession(si);
202
+ setActiveWindow(-1);
203
+ break;
204
+ }
205
+ }
206
+ const timer = setInterval(() => setProjects(listTeamProjects(tracker)), 2000);
207
+ onCleanup(() => clearInterval(timer));
208
+ });
209
+
210
+ // Keep the cursor valid as the fleet changes under the 2s refresh: if the
211
+ // selected node vanished, collapse toward the nearest still-present ancestor.
212
+ createEffect(() => {
213
+ const vis = visibleProjects();
214
+ const pi = Math.min(activeProject(), Math.max(0, vis.length - 1));
215
+ if (pi !== activeProject()) setActiveProject(pi);
216
+ const sessCount = vis[pi]?.sessions.length ?? 0;
217
+ if (activeSession() >= sessCount) {
218
+ setActiveSession(sessCount > 0 ? sessCount - 1 : -1);
219
+ setActiveWindow(-1);
220
+ }
221
+ const winCount = vis[pi]?.sessions[activeSession()]?.windowList.length ?? 0;
222
+ if (activeWindow() >= winCount) setActiveWindow(-1);
223
+ });
224
+
225
+ /** Switch the viewing client to a tmux target (`session` or `session:window`).
226
+ * The sidebar persists — no exit, just a status line on failure. */
227
+ function doSwitch(target: string) {
228
+ const client = resolveClient();
229
+ const args = ["switch-client"];
230
+ if (client) args.push("-c", client);
231
+ args.push("-t", target);
232
+ try {
233
+ runTmux(args);
234
+ setMessage("");
235
+ } catch (e) {
236
+ setMessage(String((e as { message?: string })?.message ?? e));
237
+ }
238
+ }
239
+
240
+ /** Enter on the current selection: a window row → `session:index`; a session
241
+ * row → the session; a project row → its first live session (or a note). */
242
+ function activate() {
243
+ const proj = activeProj();
244
+ if (!proj) return;
245
+ const sess = activeSess();
246
+ if (sess) {
247
+ const win = activeWin();
248
+ doSwitch(win ? `${sess.name}:${win.index}` : sess.name);
249
+ return;
250
+ }
251
+ // Project row: jump to its first running session, else say why not.
252
+ if (proj.running && proj.sessions[0]) {
253
+ doSwitch(proj.sessions[0].name);
254
+ } else {
255
+ setMessage(`${proj.name}: stopped`);
256
+ }
257
+ }
258
+
259
+ // Double-click tracking (select on first click, switch on the second).
260
+ let lastClick: ClickRecord | null = null;
261
+ function click(pi: number, si: number, wi: number) {
262
+ const now = Date.now();
263
+ setActiveProject(pi);
264
+ setActiveSession(si);
265
+ setActiveWindow(wi);
266
+ // Composite key so a double-click only counts on the same row.
267
+ const key = pi * 1_000_000 + (si + 1) * 1000 + (wi + 1);
268
+ if (isDoubleClick(lastClick, key, now)) {
269
+ lastClick = null;
270
+ activate();
271
+ return;
272
+ }
273
+ lastClick = { index: key, at: now };
274
+ }
275
+
276
+ function scroll(evt: MouseEvent) {
277
+ const dir = evt.scroll?.direction;
278
+ if (dir === "up") move(-1);
279
+ else if (dir === "down") move(1);
280
+ }
281
+
282
+ useKeyboard((evt) => {
283
+ // Help overlay swallows keys: esc / q / ? close it (grammar dismiss/quit/help).
284
+ if (helpOpen()) {
285
+ const g = matchGrammar(evt);
286
+ if (g === "dismiss" || g === "quit" || g === "help") setHelpOpen(false);
287
+ return;
288
+ }
289
+
290
+ // Filter prompt intercepts keys while open — it narrows the project list.
291
+ // Per the grammar's escape precedence, esc closes the FILTER before it
292
+ // would quit the widget; only ARROWS navigate here so j/k stay typeable.
293
+ if (filterMode()) {
294
+ if (evt.name === "escape") {
295
+ setFilterMode(false);
296
+ setFilterQuery("");
297
+ setActiveProject(0);
298
+ setActiveSession(-1);
299
+ setActiveWindow(-1);
300
+ return;
301
+ }
302
+ if (evt.name === "return") {
303
+ activate();
304
+ setFilterMode(false);
305
+ setFilterQuery("");
306
+ return;
307
+ }
308
+ if (evt.name === "up" || evt.name === "down") {
309
+ move(evt.name === "up" ? -1 : 1);
310
+ return;
311
+ }
312
+ const next = nextInput(filterQuery(), evt);
313
+ if (next !== null) {
314
+ setFilterQuery(next);
315
+ setActiveProject(0);
316
+ setActiveSession(-1);
317
+ setActiveWindow(-1);
318
+ }
319
+ return;
320
+ }
321
+
322
+ if (evt.ctrl && evt.name === "c") process.exit(0);
323
+
324
+ // The shared grammar runs FIRST; `r` (refresh) is the sole widget key.
325
+ const grammar = matchGrammar(evt);
326
+ switch (grammar) {
327
+ case "navUp":
328
+ move(-1);
329
+ return;
330
+ case "navDown":
331
+ move(1);
332
+ return;
333
+ case "activate":
334
+ activate();
335
+ return;
336
+ case "filter":
337
+ setMessage("");
338
+ setFilterQuery("");
339
+ setFilterMode(true);
340
+ setActiveProject(0);
341
+ setActiveSession(-1);
342
+ setActiveWindow(-1);
343
+ return;
344
+ case "help":
345
+ setHelpOpen(true);
346
+ return;
347
+ case "dismiss":
348
+ case "quit":
349
+ // Nothing is layered here, so esc/q close the sidebar pane.
350
+ process.exit(0);
351
+ return;
352
+ default:
353
+ break;
354
+ }
355
+
356
+ if (evt.name === "r") setProjects(listTeamProjects(tracker));
357
+ });
358
+
359
+ /** Column text width available for a row's name after its glyph + indent. */
360
+ const nameWidth = (indent: number) => Math.max(3, dimensions().width - indent - 3);
361
+
362
+ return (
363
+ <box flexDirection="column" flexGrow={1} backgroundColor={toRGBA(theme.bg)}>
364
+ <Show when={helpOpen()}>
365
+ <HelpOverlay theme={theme} title="sidebar" widgetKeys={WIDGET_KEYS} />
366
+ </Show>
367
+ <Show when={!helpOpen()}>
368
+ {/* header — one tight line */}
369
+ <box paddingLeft={1} paddingRight={1} flexDirection="row">
370
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
371
+ {trunc("tmux-ide", dimensions().width - 2)}
372
+ </text>
373
+ </box>
374
+
375
+ {/* filter line */}
376
+ <Show when={filterMode()}>
377
+ <box paddingLeft={1} paddingRight={1} flexDirection="row" gap={1}>
378
+ <text fg={toRGBA(theme.accent)}>/</text>
379
+ <text fg={toRGBA(theme.fg)}>{trunc(filterQuery(), dimensions().width - 4)}</text>
380
+ </box>
381
+ </Show>
382
+
383
+ {/* the fleet tree */}
384
+ <box flexDirection="column" flexGrow={1} paddingTop={1} onMouseScroll={scroll}>
385
+ <Show
386
+ when={visibleProjects().length > 0}
387
+ fallback={
388
+ <box paddingLeft={1}>
389
+ <text fg={toRGBA(theme.fgMuted)}>{filterMode() ? "no match" : "no sessions"}</text>
390
+ </box>
391
+ }
392
+ >
393
+ <For each={visibleProjects()}>
394
+ {(project, pi) => {
395
+ const projCursor = () => pi() === activeProject() && activeSession() === -1;
396
+ return (
397
+ <box flexDirection="column">
398
+ {/* project row */}
399
+ <box
400
+ flexDirection="row"
401
+ gap={1}
402
+ paddingLeft={1}
403
+ backgroundColor={projCursor() ? toRGBA(theme.border) : undefined}
404
+ onMouseDown={() => click(pi(), -1, -1)}
405
+ >
406
+ <text fg={projCursor() ? toRGBA(theme.accent) : toRGBA(theme.fgMuted)}>
407
+ {projCursor() ? "▸" : " "}
408
+ </text>
409
+ <text
410
+ fg={project.running ? statusColor[project.status] : toRGBA(theme.fgMuted)}
411
+ >
412
+ {project.running ? STATUS_GLYPH[project.status] : "○"}
413
+ </text>
414
+ <text
415
+ fg={projCursor() ? toRGBA(theme.accent) : toRGBA(theme.fg)}
416
+ attributes={projCursor() ? TextAttributes.BOLD : 0}
417
+ >
418
+ {trunc(project.name, nameWidth(4))}
419
+ </text>
420
+ </box>
421
+
422
+ {/* sessions — always shown under every project */}
423
+ <For each={project.sessions}>
424
+ {(session, si) => {
425
+ const sessCursor = () =>
426
+ pi() === activeProject() &&
427
+ si() === activeSession() &&
428
+ activeWindow() === -1;
429
+ const isCurrent = () => session.name === currentSession;
430
+ const expanded = () => pi() === activeProject() && si() === activeSession();
431
+ return (
432
+ <box flexDirection="column">
433
+ <box
434
+ flexDirection="row"
435
+ gap={1}
436
+ paddingLeft={2}
437
+ backgroundColor={sessCursor() ? toRGBA(theme.border) : undefined}
438
+ onMouseDown={() => click(pi(), si(), -1)}
439
+ >
440
+ <text
441
+ fg={
442
+ isCurrent()
443
+ ? toRGBA(theme.accent)
444
+ : sessCursor()
445
+ ? toRGBA(theme.accent)
446
+ : toRGBA(theme.fgMuted)
447
+ }
448
+ >
449
+ {isCurrent() ? "▸" : sessCursor() ? "›" : " "}
450
+ </text>
451
+ <text fg={statusColor[session.status]}>
452
+ {STATUS_GLYPH[session.status]}
453
+ </text>
454
+ <text
455
+ fg={isCurrent() ? toRGBA(theme.accent) : toRGBA(theme.fg)}
456
+ attributes={isCurrent() ? TextAttributes.BOLD : 0}
457
+ >
458
+ {trunc(session.name, nameWidth(6))}
459
+ </text>
460
+ </box>
461
+ {/* windows — expanded under the active/cursor session */}
462
+ <Show when={expanded() && session.windowList.length > 0}>
463
+ <For each={session.windowList}>
464
+ {(win, wi) => {
465
+ const winCursor = () =>
466
+ pi() === activeProject() &&
467
+ si() === activeSession() &&
468
+ wi() === activeWindow();
469
+ return (
470
+ <box
471
+ flexDirection="row"
472
+ gap={1}
473
+ paddingLeft={4}
474
+ backgroundColor={
475
+ winCursor() ? toRGBA(theme.border) : undefined
476
+ }
477
+ onMouseDown={() => click(pi(), si(), wi())}
478
+ >
479
+ <text
480
+ fg={
481
+ winCursor() ? toRGBA(theme.accent) : toRGBA(theme.fgMuted)
482
+ }
483
+ >
484
+ {winCursor() ? "›" : " "}
485
+ </text>
486
+ <text fg={statusColor[win.status]}>
487
+ {STATUS_GLYPH[win.status]}
488
+ </text>
489
+ <text fg={toRGBA(theme.fg)}>
490
+ {trunc(
491
+ `${win.index}:${win.name}${win.active ? "*" : ""}`,
492
+ nameWidth(8),
493
+ )}
494
+ </text>
495
+ </box>
496
+ );
497
+ }}
498
+ </For>
499
+ </Show>
500
+ </box>
501
+ );
502
+ }}
503
+ </For>
504
+ </box>
505
+ );
506
+ }}
507
+ </For>
508
+ </Show>
509
+ </box>
510
+
511
+ {/* transient status line */}
512
+ <Show when={message().length > 0}>
513
+ <box paddingLeft={1} paddingRight={1}>
514
+ <text fg={toRGBA(theme.fgMuted)}>{trunc(message(), dimensions().width - 2)}</text>
515
+ </box>
516
+ </Show>
517
+
518
+ {/* footer — the verbs that fit a narrow column */}
519
+ <box paddingLeft={1} paddingRight={1} flexDirection="row" gap={1}>
520
+ <text fg={toRGBA(theme.fgMuted)}>
521
+ {trunc("↵ go / find ? help", dimensions().width - 2)}
522
+ </text>
523
+ </box>
524
+ </Show>
525
+ </box>
526
+ );
527
+ });
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@tmux-ide/tmux-bridge",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "lint": "eslint src",
12
+ "typecheck": "tsc --noEmit",
13
+ "test": "bun test src"
14
+ },
15
+ "dependencies": {
16
+ "@tmux-ide/contracts": "workspace:*"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^25.5.0",
20
+ "typescript": "^5.9.3"
21
+ }
22
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Error thrown by the tmux-bridge package. Carries a stable `code` so
3
+ * callers can branch on classes of failure (session missing, tmux unavailable,
4
+ * generic error) without parsing stderr text.
5
+ *
6
+ * Shape mirrors the daemon's IdeError surface (`code`, `exitCode`, `toJSON`)
7
+ * so it can be serialized and printed by the CLI's existing error formatter.
8
+ */
9
+ export class TmuxError extends Error {
10
+ readonly code: string;
11
+ readonly exitCode: number;
12
+
13
+ constructor(message: string, code: string, options: { cause?: unknown; exitCode?: number } = {}) {
14
+ super(message, { cause: options.cause as Error | undefined });
15
+ this.name = "TmuxError";
16
+ this.code = code;
17
+ this.exitCode = options.exitCode ?? 1;
18
+ }
19
+
20
+ toJSON(): { error: string; code: string; cause?: string } {
21
+ const out: { error: string; code: string; cause?: string } = {
22
+ error: this.message,
23
+ code: this.code,
24
+ };
25
+ if (this.cause) out.cause = (this.cause as Error).message;
26
+ return out;
27
+ }
28
+ }
@@ -0,0 +1,31 @@
1
+ export { TmuxError } from "./errors.ts";
2
+ export { runTmux, _setExecutor, _setSpawner, _getSpawner } from "./runner.ts";
3
+ export {
4
+ attachSession,
5
+ createDetachedSession,
6
+ getSessionCwd,
7
+ getSessionState,
8
+ getSessionVariable,
9
+ hasSession,
10
+ killSession,
11
+ runSessionCommand,
12
+ setSessionEnvironment,
13
+ setSessionVariable,
14
+ } from "./sessions.ts";
15
+ export {
16
+ capturePane,
17
+ captureRecent,
18
+ getPaneCurrentCommand,
19
+ listPanes,
20
+ selectPane,
21
+ sendKeys,
22
+ sendLiteral,
23
+ setPaneOption,
24
+ setPaneTitle,
25
+ splitPane,
26
+ type CapturePaneOptions,
27
+ type SendKeysOptions,
28
+ type TmuxPaneInfo,
29
+ } from "./panes.ts";
30
+ export { isProcessAlive, startSessionMonitor, stopSessionMonitor } from "./monitor.ts";
31
+ export { resolveTarget, type ResolvedPane, type TmuxPaneTarget } from "./targeting.ts";
@@ -0,0 +1,77 @@
1
+ import { _getSpawner, runTmux } from "./runner.ts";
2
+
3
+ /** Check if a process is still alive. */
4
+ export function isProcessAlive(pid: number): boolean {
5
+ try {
6
+ process.kill(pid, 0); // signal 0 = check existence
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ export function startSessionMonitor(session: string, monitorScript: string, port?: number): void {
14
+ // If an existing monitor is still alive, kill it for a clean handoff.
15
+ try {
16
+ const existingPid = (
17
+ runTmux(["show-option", "-qvt", session, "@monitor_pid"], {
18
+ encoding: "utf-8",
19
+ }) as string
20
+ ).trim();
21
+ if (existingPid) {
22
+ const pid = parseInt(existingPid, 10);
23
+ if (isProcessAlive(pid)) {
24
+ stopSessionMonitor(session);
25
+ let attempts = 0;
26
+ while (isProcessAlive(pid) && attempts < 10) {
27
+ const { Atomics, SharedArrayBuffer } = globalThis;
28
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
29
+ attempts++;
30
+ }
31
+ }
32
+ }
33
+ } catch {
34
+ // Session variable not readable — continue with fresh start
35
+ }
36
+
37
+ // Spawn the daemon via tsx (runs TypeScript source directly under node).
38
+ // We DELIBERATELY do not use bun: under bun, node-pty's `onData` callback
39
+ // never fires (the PTY spawns and exits but no data flows). T085 burned
40
+ // half a day on this; T087 sealed the rule via PtyAdapter. Use a process
41
+ // group so we can kill the entire tree on stop.
42
+ const child = _getSpawner()("tsx", [monitorScript, session, String(port ?? 0)], {
43
+ detached: true,
44
+ stdio: "ignore",
45
+ cwd: process.cwd(),
46
+ });
47
+ child.unref();
48
+ // Store PID as tmux session variable for later cleanup. This is the actual
49
+ // node process PID (not a shell wrapper).
50
+ runTmux(["set-option", "-t", session, "@monitor_pid", String(child.pid)]);
51
+ }
52
+
53
+ export function stopSessionMonitor(session: string): void {
54
+ try {
55
+ const pid = (
56
+ runTmux(["show-option", "-qvt", session, "@monitor_pid"], {
57
+ encoding: "utf-8",
58
+ }) as string
59
+ ).trim();
60
+ if (pid) {
61
+ const numPid = parseInt(pid, 10);
62
+ // Kill the process group (negative PID) to catch any children
63
+ try {
64
+ process.kill(-numPid, "SIGTERM");
65
+ } catch {
66
+ // Process group kill failed — try direct kill
67
+ try {
68
+ process.kill(numPid, "SIGTERM");
69
+ } catch {
70
+ /* already gone */
71
+ }
72
+ }
73
+ }
74
+ } catch {
75
+ /* session or process already gone */
76
+ }
77
+ }