tmux-ide 2.6.0 → 2.6.1

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 (209) hide show
  1. package/bin/cli.js +13 -5
  2. package/bin/cli.ts +1 -1
  3. package/bunfig.toml +4 -0
  4. package/package.json +11 -3
  5. package/packages/contracts/package.json +22 -0
  6. package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
  7. package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
  8. package/packages/contracts/src/actions-contract.ts +310 -0
  9. package/packages/contracts/src/actions-errors.ts +41 -0
  10. package/packages/contracts/src/domain.ts +36 -0
  11. package/packages/contracts/src/ide-config.ts +170 -0
  12. package/packages/contracts/src/index.ts +24 -0
  13. package/packages/contracts/src/lib-internal/auth.ts +13 -0
  14. package/packages/contracts/src/lib-internal/hq.ts +38 -0
  15. package/packages/contracts/src/terminals.ts +116 -0
  16. package/packages/contracts/src/tmux.ts +60 -0
  17. package/packages/contracts/src/workspace.ts +67 -0
  18. package/packages/daemon/src/agent-explain.ts +270 -0
  19. package/packages/daemon/src/attach.ts +20 -0
  20. package/packages/daemon/src/bin.ts +4 -0
  21. package/packages/daemon/src/canonical.ts +7 -0
  22. package/packages/daemon/src/cli.ts +499 -0
  23. package/packages/daemon/src/command-center/actions/contract.ts +2 -0
  24. package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
  25. package/packages/daemon/src/command-center/actions/errors.ts +105 -0
  26. package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
  27. package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
  28. package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
  29. package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
  30. package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
  31. package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
  32. package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
  33. package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
  34. package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
  35. package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
  36. package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
  37. package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
  38. package/packages/daemon/src/command-center/actions/registry.ts +149 -0
  39. package/packages/daemon/src/command-center/discovery.ts +96 -0
  40. package/packages/daemon/src/command-center/index.ts +31 -0
  41. package/packages/daemon/src/command-center/schemas.ts +85 -0
  42. package/packages/daemon/src/command-center/server.ts +1260 -0
  43. package/packages/daemon/src/command-center/ws-events.ts +316 -0
  44. package/packages/daemon/src/config.ts +549 -0
  45. package/packages/daemon/src/detect.ts +248 -0
  46. package/packages/daemon/src/doctor.ts +242 -0
  47. package/packages/daemon/src/embed.ts +5 -0
  48. package/packages/daemon/src/index.ts +12 -0
  49. package/packages/daemon/src/init.ts +211 -0
  50. package/packages/daemon/src/inspect.ts +178 -0
  51. package/packages/daemon/src/js-yaml.d.ts +10 -0
  52. package/packages/daemon/src/launch.ts +349 -0
  53. package/packages/daemon/src/lib/active-projects.ts +49 -0
  54. package/packages/daemon/src/lib/agent-discovery.ts +121 -0
  55. package/packages/daemon/src/lib/app-config.ts +336 -0
  56. package/packages/daemon/src/lib/app-settings.ts +53 -0
  57. package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
  58. package/packages/daemon/src/lib/auth/middleware.ts +56 -0
  59. package/packages/daemon/src/lib/auth/types.ts +2 -0
  60. package/packages/daemon/src/lib/auth-token.ts +5 -0
  61. package/packages/daemon/src/lib/authorship.ts +280 -0
  62. package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
  63. package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
  64. package/packages/daemon/src/lib/daemon-embed.ts +782 -0
  65. package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
  66. package/packages/daemon/src/lib/daemon.ts +79 -0
  67. package/packages/daemon/src/lib/dot-path.ts +17 -0
  68. package/packages/daemon/src/lib/errors.ts +67 -0
  69. package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
  70. package/packages/daemon/src/lib/launch-plan.ts +90 -0
  71. package/packages/daemon/src/lib/log.ts +134 -0
  72. package/packages/daemon/src/lib/output.ts +76 -0
  73. package/packages/daemon/src/lib/project-init-runner.ts +150 -0
  74. package/packages/daemon/src/lib/project-inspect.ts +80 -0
  75. package/packages/daemon/src/lib/project-onboard.ts +149 -0
  76. package/packages/daemon/src/lib/project-probe.ts +92 -0
  77. package/packages/daemon/src/lib/project-registry.ts +296 -0
  78. package/packages/daemon/src/lib/session-monitor.ts +122 -0
  79. package/packages/daemon/src/lib/session-options.ts +100 -0
  80. package/packages/daemon/src/lib/shell.ts +8 -0
  81. package/packages/daemon/src/lib/sizes.ts +36 -0
  82. package/packages/daemon/src/lib/skill-sync.ts +155 -0
  83. package/packages/daemon/src/lib/slugify.ts +10 -0
  84. package/packages/daemon/src/lib/terminals-store.ts +125 -0
  85. package/packages/daemon/src/lib/update-check.ts +298 -0
  86. package/packages/daemon/src/lib/update.ts +158 -0
  87. package/packages/daemon/src/lib/workspace-registry.ts +229 -0
  88. package/packages/daemon/src/lib/worktree.ts +289 -0
  89. package/packages/daemon/src/lib/yaml-io.ts +27 -0
  90. package/packages/daemon/src/ls.ts +40 -0
  91. package/packages/daemon/src/restart.ts +24 -0
  92. package/packages/daemon/src/restore.ts +514 -0
  93. package/packages/daemon/src/schemas/domain.ts +2 -0
  94. package/packages/daemon/src/schemas/filesystem.ts +34 -0
  95. package/packages/daemon/src/schemas/ide-config.ts +2 -0
  96. package/packages/daemon/src/schemas/index.ts +59 -0
  97. package/packages/daemon/src/schemas/inspect.ts +67 -0
  98. package/packages/daemon/src/schemas/registry.ts +55 -0
  99. package/packages/daemon/src/schemas/ws-events.ts +135 -0
  100. package/packages/daemon/src/send.ts +171 -0
  101. package/packages/daemon/src/server/README.md +15 -0
  102. package/packages/daemon/src/server/index.ts +74 -0
  103. package/packages/daemon/src/server/pty-bridge.ts +532 -0
  104. package/packages/daemon/src/server/standalone.ts +18 -0
  105. package/packages/daemon/src/server/ws-route.ts +483 -0
  106. package/packages/daemon/src/status.ts +59 -0
  107. package/packages/daemon/src/stop.ts +28 -0
  108. package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
  109. package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
  110. package/packages/daemon/src/terminal/README.md +92 -0
  111. package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
  112. package/packages/daemon/src/tui/chrome/chip.ts +30 -0
  113. package/packages/daemon/src/tui/chrome/events.ts +119 -0
  114. package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
  115. package/packages/daemon/src/tui/chrome/menu.ts +289 -0
  116. package/packages/daemon/src/tui/chrome/notify.ts +195 -0
  117. package/packages/daemon/src/tui/chrome/panels.ts +111 -0
  118. package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
  119. package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
  120. package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
  121. package/packages/daemon/src/tui/chrome/updater.ts +428 -0
  122. package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
  123. package/packages/daemon/src/tui/compiled.ts +113 -0
  124. package/packages/daemon/src/tui/detect/classify.ts +193 -0
  125. package/packages/daemon/src/tui/detect/manifest-loader.ts +192 -0
  126. package/packages/daemon/src/tui/detect/manifest.ts +184 -0
  127. package/packages/daemon/src/tui/detect/manifests.ts +226 -0
  128. package/packages/daemon/src/tui/detect/process-tree.ts +197 -0
  129. package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
  130. package/packages/daemon/src/tui/integrations/claude.ts +176 -0
  131. package/packages/daemon/src/tui/integrations/offer.ts +145 -0
  132. package/packages/daemon/src/tui/main.ts +70 -0
  133. package/packages/daemon/src/tui/mirror/control-client.ts +143 -0
  134. package/packages/daemon/src/tui/mirror/control.ts +97 -0
  135. package/packages/daemon/src/tui/mirror/pane-mirror.ts +81 -0
  136. package/packages/daemon/src/tui/mirror/viewer.tsx +166 -0
  137. package/packages/daemon/src/tui/team/CONTROL.md +50 -0
  138. package/packages/daemon/src/tui/team/entry.ts +11 -0
  139. package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
  140. package/packages/daemon/src/tui/team/home.ts +170 -0
  141. package/packages/daemon/src/tui/team/index.tsx +1521 -0
  142. package/packages/daemon/src/tui/team/input.ts +34 -0
  143. package/packages/daemon/src/tui/team/keymap.ts +127 -0
  144. package/packages/daemon/src/tui/team/mouse.ts +29 -0
  145. package/packages/daemon/src/tui/team/nav.ts +31 -0
  146. package/packages/daemon/src/tui/team/preview.ts +34 -0
  147. package/packages/daemon/src/tui/team/projects.ts +191 -0
  148. package/packages/daemon/src/tui/team/report.ts +73 -0
  149. package/packages/daemon/src/tui/team/sessions.ts +344 -0
  150. package/packages/daemon/src/tui/team/tree.ts +62 -0
  151. package/packages/daemon/src/types.ts +13 -0
  152. package/packages/daemon/src/ui/index.ts +32 -0
  153. package/packages/daemon/src/ui/terminal/index.ts +9 -0
  154. package/packages/daemon/src/ui/types.ts +91 -0
  155. package/packages/daemon/src/ui/web/base.css +80 -0
  156. package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
  157. package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
  158. package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
  159. package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
  160. package/packages/daemon/src/ui/web/hooks.ts +106 -0
  161. package/packages/daemon/src/ui/web/index.ts +27 -0
  162. package/packages/daemon/src/ui/web/render.ts +77 -0
  163. package/packages/daemon/src/ui/web/utils/color.ts +27 -0
  164. package/packages/daemon/src/validate.ts +217 -0
  165. package/packages/daemon/src/widgets/changes/README.md +3 -0
  166. package/packages/daemon/src/widgets/changes/index.tsx +691 -0
  167. package/packages/daemon/src/widgets/config/README.md +3 -0
  168. package/packages/daemon/src/widgets/config/index.tsx +481 -0
  169. package/packages/daemon/src/widgets/explorer/README.md +3 -0
  170. package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
  171. package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
  172. package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
  173. package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
  174. package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
  175. package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
  176. package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
  177. package/packages/daemon/src/widgets/lib/files.ts +88 -0
  178. package/packages/daemon/src/widgets/lib/git.ts +88 -0
  179. package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
  180. package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
  181. package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
  182. package/packages/daemon/src/widgets/lib/theme.ts +194 -0
  183. package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
  184. package/packages/daemon/src/widgets/preview/README.md +3 -0
  185. package/packages/daemon/src/widgets/preview/index.tsx +416 -0
  186. package/packages/daemon/src/widgets/resolve.ts +121 -0
  187. package/packages/daemon/src/widgets/setup/README.md +3 -0
  188. package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
  189. package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
  190. package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
  191. package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
  192. package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
  193. package/packages/daemon/src/widgets/setup/index.tsx +341 -0
  194. package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
  195. package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
  196. package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
  197. package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
  198. package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
  199. package/packages/tmux-bridge/package.json +22 -0
  200. package/packages/tmux-bridge/src/errors.ts +28 -0
  201. package/packages/tmux-bridge/src/index.ts +31 -0
  202. package/packages/tmux-bridge/src/monitor.ts +77 -0
  203. package/packages/tmux-bridge/src/panes.ts +136 -0
  204. package/packages/tmux-bridge/src/runner.test.ts +501 -0
  205. package/packages/tmux-bridge/src/runner.ts +91 -0
  206. package/packages/tmux-bridge/src/sessions.ts +126 -0
  207. package/packages/tmux-bridge/src/targeting.test.ts +107 -0
  208. package/packages/tmux-bridge/src/targeting.ts +90 -0
  209. package/scripts/postinstall.js +26 -2
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Data layer for the team TUI.
3
+ *
4
+ * Enumerates every live tmux session and rolls each one up to an agent
5
+ * status. Detection is TWO-LAYER: a fresh `@agent_state` pane option —
6
+ * written by a lifecycle-hook integration (`tmux-ide integration install
7
+ * claude`) or any self-reporting agent — is AUTHORITATIVE; only panes
8
+ * without authority fall back to snapshot scraping (pick a manifest by the
9
+ * pane command, capture, classify, fold through the persistent
10
+ * `StatusTracker` so the cross-tick `done` surfaces). Pane statuses roll up
11
+ * to a single session status.
12
+ */
13
+ import { execFileSync } from "node:child_process";
14
+ import {
15
+ classifyInstant,
16
+ parseAuthority,
17
+ type AgentStatus,
18
+ type StatusTracker,
19
+ } from "../detect/classify.ts";
20
+ import type { AgentManifest } from "../detect/manifest.ts";
21
+ import { readProcessTable, resolveAgentCommand } from "../detect/process-tree.ts";
22
+ import { readPaneSnapshot } from "../detect/snapshot.ts";
23
+
24
+ export interface TeamSession {
25
+ name: string;
26
+ attached: boolean;
27
+ windows: number;
28
+ panes: number;
29
+ status: AgentStatus;
30
+ /**
31
+ * Per-window (tmux tab) rollup: one entry per window the session owns, in
32
+ * ascending window-index order. The `windows` count above stays for compat;
33
+ * this is the richer breakdown the switcher/cockpit navigate.
34
+ */
35
+ windowList: TeamWindow[];
36
+ }
37
+
38
+ /**
39
+ * A single tmux window (tab) within a session, with its panes' statuses rolled
40
+ * up to one status (highest severity wins, like a session rollup).
41
+ */
42
+ export interface TeamWindow {
43
+ /** `window_index` — tmux's own index (may be non-contiguous). */
44
+ index: number;
45
+ /** `window_name`. */
46
+ name: string;
47
+ /** Whether this is the session's active (foreground) window. */
48
+ active: boolean;
49
+ /** How many panes the window owns. */
50
+ panes: number;
51
+ /** Rolled-up status of the window's panes. */
52
+ status: AgentStatus;
53
+ }
54
+
55
+ /**
56
+ * A single pane's resolved detail, handed to {@link ListTeamSessionsOpts.onPane}
57
+ * as each pane is classified. `agent` is the resolved agent id, or `null` for a
58
+ * non-agent pane (no manifest, or the `shell` catch-all — those get no chip).
59
+ * `status` is the final status (authority when fresh, else scraped/tracked).
60
+ */
61
+ export interface PaneDetail {
62
+ sessionName: string;
63
+ paneId: string;
64
+ agent: string | null;
65
+ status: AgentStatus;
66
+ }
67
+
68
+ interface PaneRecord {
69
+ /** tmux pane id, e.g. `%5`. */
70
+ id: string;
71
+ /** `pane_pid` — the pane's immediate process, root of its process tree. */
72
+ pid: number;
73
+ /** `pane_current_command`. */
74
+ cmd: string;
75
+ /** `pane_title`. */
76
+ title: string;
77
+ /** Raw `@agent_state` pane option (authority layer), if set. */
78
+ authority: string;
79
+ /** Raw `@agent_hint` pane option — forces a manifest when set. */
80
+ hint: string;
81
+ /** `window_index` — the window (tab) this pane lives in. */
82
+ windowIndex: number;
83
+ /** `window_name`. */
84
+ windowName: string;
85
+ /** `window_active` — whether this pane's window is the session's active one. */
86
+ windowActive: boolean;
87
+ /** `@tmux_ide_sidebar` — set on the app's own nav-column pane. Such panes are
88
+ * chrome, not agents, so they're excluded from the status rollup entirely. */
89
+ sidebar: boolean;
90
+ }
91
+
92
+ /** The pane option that marks the app's sidebar (nav-column) pane. */
93
+ export const SIDEBAR_PANE_OPTION = "@tmux_ide_sidebar";
94
+
95
+ /**
96
+ * PURE — drop the app's own sidebar panes from a session's pane list. The
97
+ * sidebar is chrome (it renders the fleet nav column), so it must never fold
98
+ * into the session's agent-status rollup, pane count, or window breakdown.
99
+ */
100
+ export function excludeSidebarPanes<T extends { sidebar: boolean }>(panes: T[]): T[] {
101
+ return panes.filter((pane) => !pane.sidebar);
102
+ }
103
+
104
+ /** Severity order — highest present status wins in a rollup. */
105
+ const SEVERITY: AgentStatus[] = ["blocked", "working", "done", "idle", "unknown"];
106
+
107
+ /**
108
+ * Whether a session should appear in the switcher. Any `_`-prefixed session is
109
+ * internal plumbing (the `_tmux-ide-chrome` updater, scratch sessions, …) and
110
+ * is filtered out so the cockpit never lists — or navigates into — infrastructure.
111
+ */
112
+ export function isListableSession(name: string): boolean {
113
+ return !name.startsWith("_");
114
+ }
115
+
116
+ function tmux(args: string[]): string {
117
+ try {
118
+ return execFileSync("tmux", args, {
119
+ encoding: "utf8",
120
+ stdio: ["ignore", "pipe", "ignore"],
121
+ }).trim();
122
+ } catch {
123
+ return "";
124
+ }
125
+ }
126
+
127
+ /** Options for {@link listTeamSessions}. */
128
+ export interface ListTeamSessionsOpts {
129
+ /**
130
+ * Name of the currently-attached/viewed session, if any — its panes are
131
+ * marked seen (acknowledging any pending `done`).
132
+ */
133
+ viewed?: string;
134
+ /**
135
+ * Per-pane sink, invoked once per pane with its resolved agent + final
136
+ * status as it's classified. Lets a caller (the chrome updater) recover the
137
+ * per-pane truth the rollup throws away, WITHOUT a second scan. When absent,
138
+ * the extra manifest resolution for authority panes is skipped — existing
139
+ * callers see zero behavior change.
140
+ */
141
+ onPane?: (pane: PaneDetail) => void;
142
+ }
143
+
144
+ /**
145
+ * List every live tmux session with a rolled-up agent status.
146
+ *
147
+ * @param tracker Persistent status tracker threaded across refreshes so the
148
+ * cross-tick `done` state can be inferred.
149
+ * @param opts See {@link ListTeamSessionsOpts}.
150
+ */
151
+ export function listTeamSessions(
152
+ tracker: StatusTracker,
153
+ opts: ListTeamSessionsOpts = {},
154
+ ): TeamSession[] {
155
+ const raw = tmux([
156
+ "list-sessions",
157
+ "-F",
158
+ "#{session_name}\t#{session_attached}\t#{session_windows}",
159
+ ]);
160
+ if (!raw) return [];
161
+
162
+ const panesBySession = collectPanes();
163
+ // One `ps` read per call — a single ~10-20ms syscall shared across every
164
+ // fallback pane, so the manifest lookup can walk each pane's process tree
165
+ // instead of matching on the (usually generic) pane_current_command.
166
+ const processTable = readProcessTable();
167
+
168
+ return raw
169
+ .split("\n")
170
+ .filter(Boolean)
171
+ .filter((line) => isListableSession(line.split("\t")[0] ?? ""))
172
+ .map((line) => {
173
+ const [name = "", attached = "", windows = "0"] = line.split("\t");
174
+ // Sidebar panes are chrome, not agents — drop them before any rollup so
175
+ // they never pollute the session status, pane count, or window list.
176
+ const panes = excludeSidebarPanes(panesBySession.get(name) ?? []);
177
+ const seen = opts.viewed === name;
178
+
179
+ const nowSec = Math.floor(Date.now() / 1000);
180
+ const wantPane = typeof opts.onPane === "function";
181
+ const statuses = panes.map((pane) => {
182
+ // AUTHORITY first: a fresh hook-reported state outranks scraping.
183
+ const authority = parseAuthority(pane.authority, nowSec);
184
+ let status: AgentStatus;
185
+ // The manifest is resolved in the fallback anyway; for authority panes
186
+ // we only resolve it (cheaply — the process table is already loaded, no
187
+ // capture) when a pane sink wants the agent name for a chip.
188
+ let manifest: AgentManifest | undefined;
189
+ if (authority !== null) {
190
+ if (authority === "done" && seen) {
191
+ // Viewing acknowledges a finished agent — persist the ack so the
192
+ // pane doesn't flip back to done on the next tick.
193
+ ackDone(pane.id, nowSec);
194
+ status = "idle";
195
+ } else {
196
+ status = authority;
197
+ }
198
+ if (wantPane) {
199
+ manifest = resolveAgentCommand(pane.cmd, pane.pid, processTable, {
200
+ hint: pane.hint,
201
+ }).manifest;
202
+ }
203
+ } else {
204
+ // FALLBACK: snapshot scraping. Resolve the real agent from the pane's
205
+ // process tree (pane_current_command alone is usually just node/bun/sh).
206
+ // Only capture recognized agent panes; unknown commands stay "unknown"
207
+ // without a capture-pane round-trip.
208
+ manifest = resolveAgentCommand(pane.cmd, pane.pid, processTable, {
209
+ hint: pane.hint,
210
+ }).manifest;
211
+ const instant = manifest
212
+ ? classifyInstant({ ...readPaneSnapshot(pane.id), title: pane.title }, manifest)
213
+ : "unknown";
214
+ status = tracker.update(pane.id, instant, { seen });
215
+ }
216
+ // The `shell` catch-all isn't a real agent — a raw shell pane gets no
217
+ // chip (agent null → empty chip → border falls back to the pane title).
218
+ opts.onPane?.({
219
+ sessionName: name,
220
+ paneId: pane.id,
221
+ agent: manifest && manifest.id !== "shell" ? manifest.id : null,
222
+ status,
223
+ });
224
+ return status;
225
+ });
226
+
227
+ return {
228
+ name,
229
+ attached: attached === "1",
230
+ windows: Number(windows) || 0,
231
+ panes: panes.length,
232
+ status: rollupStatus(statuses),
233
+ // `panes` and `statuses` are parallel (statuses = panes.map(...)), so
234
+ // the pure rollup can group each pane's window with its resolved status.
235
+ windowList: rollupWindows(panes, statuses),
236
+ };
237
+ });
238
+ }
239
+
240
+ /** One `list-panes -a` call, grouped by session — avoids N tmux calls. */
241
+ function collectPanes(): Map<string, PaneRecord[]> {
242
+ const raw = tmux([
243
+ "list-panes",
244
+ "-a",
245
+ "-F",
246
+ // Window fields sit before pane_title so the (tab-safe) title stays the
247
+ // trailing catch-all — window names don't contain tabs in practice.
248
+ `#{session_name}\t#{pane_id}\t#{pane_pid}\t#{pane_current_command}\t#{@agent_state}\t#{@agent_hint}\t#{${SIDEBAR_PANE_OPTION}}\t#{window_index}\t#{window_name}\t#{window_active}\t#{pane_title}`,
249
+ ]);
250
+ const bySession = new Map<string, PaneRecord[]>();
251
+ for (const line of raw.split("\n").filter(Boolean)) {
252
+ const [
253
+ session = "",
254
+ id = "",
255
+ pid = "",
256
+ cmd = "",
257
+ authority = "",
258
+ hint = "",
259
+ sidebar = "",
260
+ windowIndex = "0",
261
+ windowName = "",
262
+ windowActive = "0",
263
+ ...titleParts
264
+ ] = line.split("\t");
265
+ if (!session) continue;
266
+ const list = bySession.get(session) ?? [];
267
+ list.push({
268
+ id,
269
+ pid: Number(pid) || 0,
270
+ cmd,
271
+ authority,
272
+ hint,
273
+ sidebar: sidebar === "1",
274
+ windowIndex: Number(windowIndex) || 0,
275
+ windowName,
276
+ windowActive: windowActive === "1",
277
+ title: titleParts.join("\t"),
278
+ });
279
+ bySession.set(session, list);
280
+ }
281
+ return bySession;
282
+ }
283
+
284
+ /**
285
+ * Acknowledge an authority-reported `done` for a viewed pane by rewriting its
286
+ * option to idle — otherwise the stamped "done" would resurface every tick.
287
+ */
288
+ function ackDone(paneId: string, nowSec: number): void {
289
+ tmux(["set-option", "-p", "-t", paneId, "@agent_state", `idle:${nowSec}`]);
290
+ }
291
+
292
+ /**
293
+ * Roll a session's per-pane statuses up to a single status. The highest
294
+ * present severity wins (blocked > working > done > idle > unknown), so
295
+ * `unknown` only surfaces when nothing else is present. An empty session
296
+ * (no panes) rolls up to `"idle"`.
297
+ */
298
+ export function rollupStatus(statuses: AgentStatus[]): AgentStatus {
299
+ if (statuses.length === 0) return "idle";
300
+ const present = new Set(statuses);
301
+ for (const status of SEVERITY) {
302
+ if (present.has(status)) return status;
303
+ }
304
+ return "unknown";
305
+ }
306
+
307
+ /** The window fields {@link rollupWindows} needs from each pane — PaneRecord fits. */
308
+ export interface WindowPaneInput {
309
+ windowIndex: number;
310
+ windowName: string;
311
+ windowActive: boolean;
312
+ }
313
+
314
+ /**
315
+ * Group a session's panes into per-window rollups — PURE.
316
+ *
317
+ * `panes[i]` and `statuses[i]` are parallel (one status per pane). Panes are
318
+ * bucketed by `windowIndex`; each window's panes roll up to a single status via
319
+ * {@link rollupStatus}, its `name` taken from the first pane seen and `active`
320
+ * true if ANY of its panes report `window_active`. Windows come back in
321
+ * ascending index order. An empty pane list yields an empty window list.
322
+ */
323
+ export function rollupWindows(panes: WindowPaneInput[], statuses: AgentStatus[]): TeamWindow[] {
324
+ const byIndex = new Map<number, { name: string; active: boolean; statuses: AgentStatus[] }>();
325
+ panes.forEach((pane, i) => {
326
+ let entry = byIndex.get(pane.windowIndex);
327
+ if (!entry) {
328
+ entry = { name: pane.windowName, active: false, statuses: [] };
329
+ byIndex.set(pane.windowIndex, entry);
330
+ }
331
+ if (pane.windowActive) entry.active = true;
332
+ const status = statuses[i];
333
+ if (status) entry.statuses.push(status);
334
+ });
335
+ return [...byIndex.entries()]
336
+ .sort(([a], [b]) => a - b)
337
+ .map(([index, entry]) => ({
338
+ index,
339
+ name: entry.name,
340
+ active: entry.active,
341
+ panes: entry.statuses.length,
342
+ status: rollupStatus(entry.statuses),
343
+ }));
344
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The flat row-node union shared by the picker popup and the sidebar column.
3
+ *
4
+ * Both surfaces navigate the same three-tier tree — project → session → window
5
+ * — with a single flat cursor. This module is the PURE builder for that ordered
6
+ * node list so the two callers can't drift: `treeNodes` produces the union for a
7
+ * given cursor/expansion policy, `findCursor` locates a cursor within it.
8
+ *
9
+ * The cursor is the `(pi, si, wi)` triple, where `si:-1` marks a project row and
10
+ * `wi:-1` a session (or project) row — identical to the picker's original
11
+ * inline `pickerNodes`.
12
+ */
13
+
14
+ /** A flat cursor position: project index, session index (-1 = project row),
15
+ * window index (-1 = session/project row). */
16
+ export interface TreeCursor {
17
+ pi: number;
18
+ si: number;
19
+ wi: number;
20
+ }
21
+
22
+ /** The minimal project shape the node builder needs — a structural subset of
23
+ * TeamProject so tests can pass plain fixtures. */
24
+ export interface TreeProjectLike {
25
+ sessions: Array<{ windowList: readonly unknown[] }>;
26
+ }
27
+
28
+ /**
29
+ * PURE — build the ordered flat node union for the tree.
30
+ *
31
+ * For every project a project row is emitted. Its sessions are expanded when the
32
+ * project is the active one, OR always when `expandAllProjects` is set (the
33
+ * sidebar shows the whole fleet; the picker only opens the active project).
34
+ * Window rows are emitted only under the ACTIVE session (`pi===activeProject &&
35
+ * si===activeSession`), so exactly one session's windows are ever expanded.
36
+ */
37
+ export function treeNodes(
38
+ projects: readonly TreeProjectLike[],
39
+ activeProject: number,
40
+ activeSession: number,
41
+ opts: { expandAllProjects?: boolean } = {},
42
+ ): TreeCursor[] {
43
+ const expandAll = opts.expandAllProjects ?? false;
44
+ const nodes: TreeCursor[] = [];
45
+ projects.forEach((proj, pi) => {
46
+ nodes.push({ pi, si: -1, wi: -1 });
47
+ if (!expandAll && pi !== activeProject) return;
48
+ proj.sessions.forEach((sess, si) => {
49
+ nodes.push({ pi, si, wi: -1 });
50
+ if (pi === activeProject && si === activeSession) {
51
+ sess.windowList.forEach((_w, wi) => nodes.push({ pi, si, wi }));
52
+ }
53
+ });
54
+ });
55
+ return nodes;
56
+ }
57
+
58
+ /** PURE — index of `cursor` in `nodes`, or -1 when absent (e.g. after a refresh
59
+ * dropped the row). */
60
+ export function findCursor(nodes: readonly TreeCursor[], cursor: TreeCursor): number {
61
+ return nodes.findIndex((n) => n.pi === cursor.pi && n.si === cursor.si && n.wi === cursor.wi);
62
+ }
@@ -0,0 +1,13 @@
1
+ export type {
2
+ Pane,
3
+ Row,
4
+ ThemeConfig,
5
+ OrchestratorYamlConfig,
6
+ CommandCenterConfig,
7
+ DashboardConfig,
8
+ IdeConfig,
9
+ PaneAction,
10
+ SessionState,
11
+ } from "./schemas/ide-config.ts";
12
+
13
+ export type TmuxCommand = string[];
@@ -0,0 +1,32 @@
1
+ // Environment-detecting UI entry point.
2
+ //
3
+ // Detects whether code is running in a browser (window exists) or terminal
4
+ // (Node/Bun) and re-exports the appropriate backend. Widgets import from
5
+ // this module to work in both environments without code changes.
6
+ //
7
+ // Usage:
8
+ // import { render, useKeyboard, RGBA, TextAttributes } from "../../ui/index.ts";
9
+ //
10
+ // Terminal backend: re-exports @opentui/solid + @opentui/core
11
+ // Web backend: re-exports DOM-based mirror components
12
+
13
+ // Use a compile-time constant so bundlers can tree-shake the unused backend.
14
+ // In browser builds, `typeof window !== "undefined"` is true and the terminal
15
+ // branch is eliminated. In Node/Bun, the web branch is eliminated.
16
+ const IS_BROWSER = typeof window !== "undefined";
17
+
18
+ if (IS_BROWSER) {
19
+ // Re-export web backend at module level for static analysis
20
+ }
21
+
22
+ // We use conditional re-export via a barrel pattern.
23
+ // Bundlers (Bun, webpack, turbopack) resolve this statically.
24
+ // For runtime detection, we export from web by default (browser-first)
25
+ // and terminal widgets import from ./terminal/index.ts directly.
26
+ //
27
+ // The web backend is safe to import in Node because it only touches
28
+ // the DOM inside render() and hooks (which are never called in terminal mode).
29
+ export * from "./web/index.ts";
30
+
31
+ // Also re-export shared types so they're available regardless of backend
32
+ export type { BoxProps, TextProps, ScrollBoxProps, InputProps } from "./types.ts";
@@ -0,0 +1,9 @@
1
+ // Terminal backend — re-exports @opentui/solid and @opentui/core for terminal rendering.
2
+ // Widgets importing from src/ui/ will get these exports when running in Node/Bun.
3
+
4
+ // Render and hooks
5
+ export { render, useKeyboard, useTerminalDimensions } from "@opentui/solid";
6
+
7
+ // Core primitives and types
8
+ export { RGBA, TextAttributes } from "@opentui/core";
9
+ export type { ScrollBoxRenderable, InputRenderable } from "@opentui/core";
@@ -0,0 +1,91 @@
1
+ export interface RGBA {
2
+ r: number;
3
+ g: number;
4
+ b: number;
5
+ a: number;
6
+ }
7
+
8
+ export function rgbaFromInts(r: number, g: number, b: number, a?: number): RGBA {
9
+ return { r, g, b, a: a ?? 255 };
10
+ }
11
+
12
+ export const TextAttributes = {
13
+ NONE: 0,
14
+ BOLD: 1,
15
+ DIM: 2,
16
+ ITALIC: 4,
17
+ UNDERLINE: 8,
18
+ BLINK: 16,
19
+ INVERSE: 32,
20
+ HIDDEN: 64,
21
+ STRIKETHROUGH: 128,
22
+ } as const;
23
+
24
+ export interface BoxProps {
25
+ id?: string;
26
+ flexDirection?: "row" | "column";
27
+ flexGrow?: number;
28
+ flexShrink?: number;
29
+ gap?: number;
30
+ justifyContent?: "flex-start" | "center" | "flex-end" | "space-between";
31
+ alignItems?: "flex-start" | "center" | "flex-end" | "stretch";
32
+ position?: "relative" | "absolute";
33
+ overflow?: "visible" | "hidden";
34
+ width?: number | string;
35
+ height?: number | string;
36
+ maxWidth?: number | string;
37
+ maxHeight?: number | string;
38
+ padding?: number;
39
+ paddingLeft?: number;
40
+ paddingRight?: number;
41
+ paddingTop?: number;
42
+ paddingBottom?: number;
43
+ backgroundColor?: RGBA;
44
+ border?: string[];
45
+ borderColor?: RGBA;
46
+ top?: number;
47
+ left?: number;
48
+ right?: number;
49
+ bottom?: number;
50
+ // Mouse events
51
+ onMouseDown?: (e: unknown) => void;
52
+ onMouseUp?: (e: unknown) => void;
53
+ onMouseMove?: (e: unknown) => void;
54
+ onMouseOver?: (e: unknown) => void;
55
+ // Children
56
+ children?: unknown;
57
+ ref?: (el: unknown) => void;
58
+ }
59
+
60
+ export interface TextProps {
61
+ fg?: RGBA;
62
+ bg?: RGBA;
63
+ attributes?: number;
64
+ wrapMode?: "none" | "word" | "char";
65
+ width?: number | string;
66
+ height?: number | string;
67
+ flexGrow?: number;
68
+ flexShrink?: number;
69
+ // Mouse events
70
+ onMouseUp?: (e: unknown) => void;
71
+ onMouseDown?: (e: unknown) => void;
72
+ children?: unknown;
73
+ }
74
+
75
+ export interface ScrollBoxProps extends BoxProps {
76
+ verticalScrollbarOptions?: unknown;
77
+ stickyScroll?: boolean;
78
+ stickyStart?: "bottom" | "top";
79
+ }
80
+
81
+ export interface InputProps {
82
+ value?: string;
83
+ placeholder?: string;
84
+ onInput?: (value: string) => void;
85
+ focusedBackgroundColor?: RGBA;
86
+ cursorColor?: RGBA;
87
+ focusedTextColor?: RGBA;
88
+ onMouseDown?: (e: unknown) => void;
89
+ ref?: (el: unknown) => void;
90
+ children?: unknown;
91
+ }
@@ -0,0 +1,80 @@
1
+ /* Base styles for the web terminal backend.
2
+ * Import this in your HTML or bundler entry point to get terminal-like appearance.
3
+ * These styles are also injected automatically by render() at runtime.
4
+ */
5
+
6
+ :root {
7
+ --ui-line-height: 1.4em;
8
+ --ui-font-family:
9
+ "IBM Plex Mono", ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, "Liberation Mono",
10
+ monospace;
11
+ --ui-font-size: 13px;
12
+ --ui-bg: rgb(19, 16, 16);
13
+ --ui-fg: rgb(232, 228, 228);
14
+ --ui-scrollbar-track: rgb(25, 25, 35);
15
+ --ui-scrollbar-thumb: rgb(60, 60, 80);
16
+ --ui-scrollbar-thumb-hover: rgb(80, 80, 100);
17
+ }
18
+
19
+ body,
20
+ #root {
21
+ font-family: var(--ui-font-family);
22
+ font-size: var(--ui-font-size);
23
+ line-height: var(--ui-line-height);
24
+ background: var(--ui-bg);
25
+ color: var(--ui-fg);
26
+ margin: 0;
27
+ padding: 0;
28
+ -webkit-font-smoothing: antialiased;
29
+ -moz-osx-font-smoothing: grayscale;
30
+ }
31
+
32
+ * {
33
+ box-sizing: border-box;
34
+ }
35
+
36
+ /* Flex-column by default — matches terminal box model */
37
+ div {
38
+ display: flex;
39
+ flex-direction: column;
40
+ min-width: 0;
41
+ min-height: 0;
42
+ }
43
+
44
+ span {
45
+ font-family: inherit;
46
+ font-size: inherit;
47
+ line-height: inherit;
48
+ }
49
+
50
+ input {
51
+ font-family: inherit;
52
+ font-size: inherit;
53
+ line-height: inherit;
54
+ background: transparent;
55
+ color: inherit;
56
+ border: none;
57
+ outline: none;
58
+ }
59
+
60
+ input::placeholder {
61
+ color: rgba(232, 228, 228, 0.4);
62
+ }
63
+
64
+ /* Scrollbar styling */
65
+ ::-webkit-scrollbar {
66
+ width: 8px;
67
+ }
68
+
69
+ ::-webkit-scrollbar-track {
70
+ background: var(--ui-scrollbar-track);
71
+ }
72
+
73
+ ::-webkit-scrollbar-thumb {
74
+ background: var(--ui-scrollbar-thumb);
75
+ border-radius: 4px;
76
+ }
77
+
78
+ ::-webkit-scrollbar-thumb:hover {
79
+ background: var(--ui-scrollbar-thumb-hover);
80
+ }
@@ -0,0 +1,59 @@
1
+ import type { JSX, ParentProps } from "solid-js";
2
+ import { rgbaToCSS } from "../utils/color.ts";
3
+ import type { BoxProps } from "../../types.ts";
4
+
5
+ export function Box(props: ParentProps<BoxProps>) {
6
+ const style = (): JSX.CSSProperties => ({
7
+ display: "flex",
8
+ "flex-direction": props.flexDirection ?? "column",
9
+ "flex-grow": props.flexGrow,
10
+ "flex-shrink": props.flexShrink,
11
+ gap: props.gap !== undefined ? `${props.gap}ch` : undefined,
12
+ "justify-content": props.justifyContent,
13
+ "align-items": props.alignItems,
14
+ position: props.position ?? "relative",
15
+ overflow: props.overflow,
16
+ width: typeof props.width === "number" ? `${props.width}ch` : props.width,
17
+ height:
18
+ typeof props.height === "number"
19
+ ? `calc(${props.height} * var(--line-height))`
20
+ : props.height,
21
+ "max-width": typeof props.maxWidth === "number" ? `${props.maxWidth}ch` : props.maxWidth,
22
+ "max-height":
23
+ typeof props.maxHeight === "number"
24
+ ? `calc(${props.maxHeight} * var(--line-height))`
25
+ : props.maxHeight,
26
+ padding:
27
+ props.padding !== undefined
28
+ ? `calc(${props.padding} * var(--line-height)) ${props.padding}ch`
29
+ : undefined,
30
+ "padding-left": props.paddingLeft !== undefined ? `${props.paddingLeft}ch` : undefined,
31
+ "padding-right": props.paddingRight !== undefined ? `${props.paddingRight}ch` : undefined,
32
+ "padding-top":
33
+ props.paddingTop !== undefined ? `calc(${props.paddingTop} * var(--line-height))` : undefined,
34
+ "padding-bottom":
35
+ props.paddingBottom !== undefined
36
+ ? `calc(${props.paddingBottom} * var(--line-height))`
37
+ : undefined,
38
+ background: props.backgroundColor ? rgbaToCSS(props.backgroundColor) : undefined,
39
+ "border-color": props.borderColor ? rgbaToCSS(props.borderColor) : undefined,
40
+ top: props.top !== undefined ? `calc(${props.top} * var(--line-height))` : undefined,
41
+ left: props.left !== undefined ? `${props.left}ch` : undefined,
42
+ right: props.right !== undefined ? `${props.right}ch` : undefined,
43
+ bottom: props.bottom !== undefined ? `calc(${props.bottom} * var(--line-height))` : undefined,
44
+ });
45
+
46
+ return (
47
+ <div
48
+ id={props.id}
49
+ style={style()}
50
+ onMouseDown={props.onMouseDown}
51
+ onMouseUp={props.onMouseUp}
52
+ onMouseMove={props.onMouseMove}
53
+ onMouseOver={props.onMouseOver}
54
+ ref={props.ref}
55
+ >
56
+ {props.children}
57
+ </div>
58
+ );
59
+ }