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,209 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import type { PaneInfo } from "@tmux-ide/contracts";
3
+
4
+ export type { PaneInfo };
5
+
6
+ type TmuxExecutor = (cmd: string, args: string[], options?: object) => string;
7
+
8
+ let _executor: TmuxExecutor = (cmd, args, options) =>
9
+ execFileSync(cmd, args, { encoding: "utf-8", ...options }).toString();
10
+
11
+ export function _setExecutor(fn: TmuxExecutor): () => void {
12
+ const prev = _executor;
13
+ _executor = fn;
14
+ return () => {
15
+ _executor = prev;
16
+ };
17
+ }
18
+
19
+ function tmux(...args: string[]): string {
20
+ try {
21
+ return _executor("tmux", args, {
22
+ stdio: ["pipe", "pipe", "pipe"],
23
+ }).trim();
24
+ } catch (error) {
25
+ const stderr = (error as { stderr?: Buffer | string })?.stderr?.toString() ?? "";
26
+ if (stderr.includes("no server running") || stderr.includes("can't find session")) {
27
+ return "";
28
+ }
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ export function listSessionPanes(session: string): PaneInfo[] {
34
+ const format = [
35
+ "#{pane_id}",
36
+ "#{pane_index}",
37
+ "#{pane_title}",
38
+ "#{pane_current_command}",
39
+ "#{pane_width}",
40
+ "#{pane_height}",
41
+ "#{pane_active}",
42
+ "#{@ide_role}",
43
+ "#{@ide_name}",
44
+ "#{@ide_type}",
45
+ ].join("\t");
46
+
47
+ const output = tmux("list-panes", "-t", session, "-F", format);
48
+ if (!output) return [];
49
+
50
+ return output
51
+ .split("\n")
52
+ .filter(Boolean)
53
+ .map((line) => {
54
+ const [id, index, title, cmd, width, height, active, role, name, type] = line.split("\t");
55
+ return {
56
+ id: id!,
57
+ index: parseInt(index!, 10),
58
+ title: title!,
59
+ currentCommand: cmd!,
60
+ width: parseInt(width!, 10),
61
+ height: parseInt(height!, 10),
62
+ active: active === "1",
63
+ role: (role || null) as PaneInfo["role"],
64
+ name: name || null,
65
+ type: type || null,
66
+ };
67
+ });
68
+ }
69
+
70
+ export function findPaneByTitle(session: string, title: string): string | null {
71
+ const panes = listSessionPanes(session);
72
+ const match = panes.find((p) => p.title === title);
73
+ return match?.id ?? null;
74
+ }
75
+
76
+ export function findPaneByPattern(session: string, pattern: string): string | null {
77
+ const panes = listSessionPanes(session);
78
+ const lower = pattern.toLowerCase();
79
+ const match = panes.find((p) => p.title.toLowerCase().includes(lower));
80
+ return match?.id ?? null;
81
+ }
82
+
83
+ export function findAdjacentPane(session: string, currentPaneId: string): string | null {
84
+ const panes = listSessionPanes(session);
85
+ const currentIdx = panes.findIndex((p) => p.id === currentPaneId);
86
+ if (currentIdx === -1 || panes.length < 2) return null;
87
+ const nextIdx = (currentIdx + 1) % panes.length;
88
+ return panes[nextIdx]!.id;
89
+ }
90
+
91
+ const BUSY_COMMANDS = new Set([
92
+ "claude",
93
+ "codex",
94
+ "vim",
95
+ "nvim",
96
+ "vi",
97
+ "nano",
98
+ "emacs",
99
+ "less",
100
+ "more",
101
+ "top",
102
+ "htop",
103
+ "man",
104
+ ]);
105
+
106
+ const SHELL_COMMANDS = new Set(["zsh", "bash", "sh", "fish"]);
107
+
108
+ export type PaneBusyStatus = "idle" | "busy" | "agent";
109
+
110
+ export function isPaneBusy(session: string, paneId: string): boolean {
111
+ const panes = listSessionPanes(session);
112
+ const pane = panes.find((p) => p.id === paneId);
113
+ if (!pane) return true;
114
+ const cmd = pane.currentCommand.toLowerCase();
115
+ if (SHELL_COMMANDS.has(cmd)) return false;
116
+ if (BUSY_COMMANDS.has(cmd)) return true;
117
+ return true; // unknown command — assume busy
118
+ }
119
+
120
+ export function getPaneBusyStatus(session: string, paneId: string): PaneBusyStatus {
121
+ const panes = listSessionPanes(session);
122
+ const pane = panes.find((p) => p.id === paneId);
123
+ if (!pane) return "busy";
124
+ const cmd = pane.currentCommand.toLowerCase();
125
+ if (cmd.startsWith("claude") || cmd.startsWith("codex")) return "agent";
126
+ if (SHELL_COMMANDS.has(cmd)) return "idle";
127
+ return "busy";
128
+ }
129
+
130
+ export function sendText(session: string, paneId: string, text: string): void {
131
+ tmux("send-keys", "-t", paneId, "-l", "--", text);
132
+ }
133
+
134
+ // tmux pane ids (e.g. %10) are GLOBAL — they don't take a session prefix.
135
+ // `tmux send-keys -t <session>:<%paneId>` makes tmux interpret the
136
+ // session prefix and then look up `%paneId` as a window inside that
137
+ // session, which fails ("can't find window: %10"). Pass the bare pane id.
138
+ export function sendLiteralToPane(_session: string, paneId: string, text: string): void {
139
+ tmux("send-keys", "-t", paneId, "-l", "--", text);
140
+ }
141
+
142
+ export function sendEnterToPane(_session: string, paneId: string): void {
143
+ tmux("send-keys", "-t", paneId, "Enter");
144
+ }
145
+
146
+ function sleepMs(ms: number): void {
147
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
148
+ }
149
+
150
+ export function sendCommand(session: string, paneId: string, command: string): boolean {
151
+ const status = getPaneBusyStatus(session, paneId);
152
+ try {
153
+ tmux("send-keys", "-t", paneId, "-l", "--", command);
154
+ } catch {
155
+ return false;
156
+ }
157
+ if (status === "agent") {
158
+ // Claude Code shows a [Pasted text] preview for long input (roughly >200 chars).
159
+ // Short commands land directly in the prompt — just need a brief delay for the
160
+ // TUI to register the input. Long commands trigger the paste preview which needs
161
+ // two Enters: one to confirm the preview, one to submit the prompt.
162
+ if (command.length < 200) {
163
+ sleepMs(150);
164
+ } else {
165
+ sleepMs(5000);
166
+ tmux("send-keys", "-t", paneId, "Enter");
167
+ sleepMs(2000);
168
+ }
169
+ tmux("send-keys", "-t", paneId, "Enter");
170
+ return true;
171
+ }
172
+ tmux("send-keys", "-t", paneId, "Enter");
173
+ return true;
174
+ }
175
+
176
+ export function openFileInEditor(session: string, paneId: string, filePath: string): void {
177
+ const editor = process.env.EDITOR ?? "vim";
178
+ sendCommand(session, paneId, `${editor} ${filePath}`);
179
+ }
180
+
181
+ export interface TargetOptions {
182
+ title?: string;
183
+ titlePattern?: string;
184
+ paneId?: string;
185
+ selfPaneId?: string;
186
+ }
187
+
188
+ export function resolveTarget(session: string, opts: TargetOptions): string | null {
189
+ if (opts.paneId) return opts.paneId;
190
+ if (opts.title) return findPaneByTitle(session, opts.title);
191
+ if (opts.titlePattern) return findPaneByPattern(session, opts.titlePattern);
192
+ if (opts.selfPaneId) return findAdjacentPane(session, opts.selfPaneId);
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * Capture the last line of a tmux pane. Uses the mockable executor so tests
198
+ * can intercept the call instead of hitting a real tmux server.
199
+ */
200
+ export function captureLastLine(paneId: string): string {
201
+ try {
202
+ return _executor("tmux", ["capture-pane", "-t", paneId, "-p", "-S", "-1"], {
203
+ encoding: "utf-8",
204
+ stdio: ["ignore", "pipe", "ignore"],
205
+ }).trim();
206
+ } catch {
207
+ return "";
208
+ }
209
+ }
@@ -0,0 +1,194 @@
1
+ import { DEFAULT_THEME, type AppTheme } from "../../lib/app-config.ts";
2
+
3
+ export type RGBA = { r: number; g: number; b: number; a: number };
4
+
5
+ export interface WidgetTheme {
6
+ bg: RGBA;
7
+ fg: RGBA;
8
+ fgMuted: RGBA;
9
+ accent: RGBA;
10
+ border: RGBA;
11
+ /** Per-agent-status colors, shared with the tmux chrome via the app theme. */
12
+ statusBlocked: RGBA;
13
+ statusWorking: RGBA;
14
+ statusDone: RGBA;
15
+ statusIdle: RGBA;
16
+ statusUnknown: RGBA;
17
+ selected: RGBA;
18
+ selectedText: RGBA;
19
+ dirName: RGBA;
20
+ rowAlt: RGBA;
21
+ indentGuide: RGBA;
22
+ ignored: RGBA;
23
+ gitModified: RGBA;
24
+ gitAdded: RGBA;
25
+ gitDeleted: RGBA;
26
+ gitUntracked: RGBA;
27
+ diffAdded: RGBA;
28
+ diffAddedBg: RGBA;
29
+ diffRemoved: RGBA;
30
+ diffRemovedBg: RGBA;
31
+ diffContext: RGBA;
32
+ diffContextBg: RGBA;
33
+ diffHunk: RGBA;
34
+ diffLineNumber: RGBA;
35
+ }
36
+
37
+ function rgba(r: number, g: number, b: number, a = 255) {
38
+ return { r, g, b, a };
39
+ }
40
+
41
+ const DEFAULTS: WidgetTheme = {
42
+ bg: rgba(30, 30, 40),
43
+ fg: rgba(200, 200, 210),
44
+ fgMuted: rgba(120, 120, 140),
45
+ accent: rgba(130, 170, 255),
46
+ border: rgba(60, 60, 80),
47
+ statusBlocked: rgba(255, 95, 95),
48
+ statusWorking: rgba(255, 215, 95),
49
+ statusDone: rgba(135, 175, 255),
50
+ statusIdle: rgba(135, 215, 135),
51
+ statusUnknown: rgba(128, 128, 128),
52
+ selected: rgba(50, 50, 70),
53
+ selectedText: rgba(255, 255, 255),
54
+ dirName: rgba(100, 160, 255),
55
+ rowAlt: rgba(255, 255, 255, 5),
56
+ indentGuide: rgba(60, 60, 80),
57
+ ignored: rgba(80, 80, 100),
58
+ gitModified: rgba(230, 180, 80),
59
+ gitAdded: rgba(80, 200, 120),
60
+ gitDeleted: rgba(230, 80, 80),
61
+ gitUntracked: rgba(120, 120, 140),
62
+ diffAdded: rgba(80, 200, 120),
63
+ diffAddedBg: rgba(20, 60, 30),
64
+ diffRemoved: rgba(230, 80, 80),
65
+ diffRemovedBg: rgba(60, 20, 20),
66
+ diffContext: rgba(160, 160, 170),
67
+ diffContextBg: rgba(30, 30, 40),
68
+ diffHunk: rgba(100, 180, 220),
69
+ diffLineNumber: rgba(80, 80, 100),
70
+ };
71
+
72
+ /**
73
+ * Parse a tmux/CSS color string into RGBA.
74
+ * Supports: colourN / colorN (0-255), #rgb, #rrggbb.
75
+ */
76
+ function parseColor(color: string): RGBA | null {
77
+ // tmux colour0–colour255
78
+ const tmuxMatch = color.match(/^colou?r(\d+)$/);
79
+ if (tmuxMatch) {
80
+ const rawIndex = tmuxMatch[1];
81
+ if (!rawIndex) return null;
82
+ const n = parseInt(rawIndex, 10);
83
+ if (n < 0 || n > 255) return null;
84
+
85
+ // Standard colors 0–15 (approximate to common terminal defaults)
86
+ const STANDARD: [number, number, number][] = [
87
+ [0, 0, 0],
88
+ [128, 0, 0],
89
+ [0, 128, 0],
90
+ [128, 128, 0],
91
+ [0, 0, 128],
92
+ [128, 0, 128],
93
+ [0, 128, 128],
94
+ [192, 192, 192],
95
+ [128, 128, 128],
96
+ [255, 0, 0],
97
+ [0, 255, 0],
98
+ [255, 255, 0],
99
+ [0, 0, 255],
100
+ [255, 0, 255],
101
+ [0, 255, 255],
102
+ [255, 255, 255],
103
+ ];
104
+ if (n < 16) {
105
+ const standard = STANDARD[n];
106
+ if (!standard) return null;
107
+ return rgba(standard[0], standard[1], standard[2]);
108
+ }
109
+
110
+ // 6×6×6 color cube (16–231)
111
+ if (n < 232) {
112
+ const idx = n - 16;
113
+ const LEVELS: number[] = [0, 95, 135, 175, 215, 255];
114
+ const r = LEVELS[Math.floor(idx / 36)];
115
+ const g = LEVELS[Math.floor((idx % 36) / 6)];
116
+ const b = LEVELS[idx % 6];
117
+ if (r == null || g == null || b == null) return null;
118
+ return rgba(r, g, b);
119
+ }
120
+
121
+ // Grayscale ramp (232–255)
122
+ const level = 8 + 10 * (n - 232);
123
+ return rgba(level, level, level);
124
+ }
125
+
126
+ // Hex: #rgb or #rrggbb
127
+ const hexMatch = color.match(/^#([0-9a-fA-F]{3,6})$/);
128
+ if (hexMatch) {
129
+ let hex = hexMatch[1];
130
+ if (!hex) return null;
131
+ if (hex.length === 3) {
132
+ const [r, g, b] = hex.split("");
133
+ if (!r || !g || !b) return null;
134
+ hex = r + r + g + g + b + b;
135
+ }
136
+ if (hex.length !== 6) return null;
137
+ return rgba(
138
+ parseInt(hex.slice(0, 2), 16),
139
+ parseInt(hex.slice(2, 4), 16),
140
+ parseInt(hex.slice(4, 6), 16),
141
+ );
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Map the shared app-theme tokens ({@link AppTheme}) onto a {@link WidgetTheme}
149
+ * base: accent, muted→fgMuted, fg, and the per-status colors. This is the layer
150
+ * that makes the OpenTUI widgets share the tmux chrome's palette. Unmappable
151
+ * tokens (e.g. a hex the parser rejects) keep their default.
152
+ */
153
+ function applyAppTheme(base: WidgetTheme, appTheme: AppTheme): void {
154
+ const assign = (prop: keyof WidgetTheme, token: string) => {
155
+ const parsed = parseColor(token);
156
+ if (parsed) base[prop] = parsed;
157
+ };
158
+ assign("accent", appTheme.accent);
159
+ assign("fgMuted", appTheme.muted);
160
+ assign("fg", appTheme.fg);
161
+ assign("statusBlocked", appTheme.status.blocked);
162
+ assign("statusWorking", appTheme.status.working);
163
+ assign("statusDone", appTheme.status.done);
164
+ assign("statusIdle", appTheme.status.idle);
165
+ assign("statusUnknown", appTheme.status.unknown);
166
+ }
167
+
168
+ /**
169
+ * Build a widget theme. The shared app-theme tokens (`appTheme`, defaulting to
170
+ * {@link DEFAULT_THEME}) form the base palette so widgets and chrome match; the
171
+ * per-project ide.yml `theme` (`config`: accent/border/bg/fg) overrides on top.
172
+ */
173
+ export function createTheme(
174
+ config?: Record<string, string>,
175
+ appTheme: AppTheme = DEFAULT_THEME,
176
+ ): WidgetTheme {
177
+ const theme = { ...DEFAULTS };
178
+ applyAppTheme(theme, appTheme);
179
+ if (!config) return theme;
180
+ const MAP: Record<string, keyof WidgetTheme> = {
181
+ accent: "accent",
182
+ border: "border",
183
+ bg: "bg",
184
+ fg: "fg",
185
+ };
186
+ for (const [key, prop] of Object.entries(MAP)) {
187
+ const val = config[key];
188
+ if (val) {
189
+ const parsed = parseColor(val);
190
+ if (parsed) theme[prop] = parsed;
191
+ }
192
+ }
193
+ return theme;
194
+ }
@@ -0,0 +1,132 @@
1
+ import { watch as fsWatch, type FSWatcher } from "node:fs";
2
+ import { join, sep } from "node:path";
3
+
4
+ export interface WatchEvent {
5
+ type: "create" | "update" | "delete";
6
+ path: string;
7
+ }
8
+
9
+ /**
10
+ * File watching that survives BOTH runtimes. In a dev checkout / node install
11
+ * we use `@parcel/watcher` (fast, native, recursive with real ignore globs).
12
+ * In the compiled `tmux-ide-tui` binary that native `.node` addon can't be
13
+ * embedded by `bun build --compile`, so importing it throws — we catch that
14
+ * once and fall back to node's builtin `fs.watch`. The fallback is coarser
15
+ * (segment-level ignores, synthesized "update" events) but keeps the explorer
16
+ * and changes surfaces live instead of crashing on boot.
17
+ */
18
+
19
+ interface ParcelSubscription {
20
+ unsubscribe(): Promise<void>;
21
+ }
22
+ interface ParcelWatcher {
23
+ subscribe(
24
+ dir: string,
25
+ cb: (err: Error | null, events: { type: string; path: string }[]) => void,
26
+ opts?: { ignore?: string[] },
27
+ ): Promise<ParcelSubscription>;
28
+ }
29
+
30
+ // undefined = not attempted yet, null = confirmed unavailable (compiled binary).
31
+ let parcel: ParcelWatcher | null | undefined;
32
+
33
+ async function loadParcel(): Promise<ParcelWatcher | null> {
34
+ if (parcel !== undefined) return parcel;
35
+ try {
36
+ const mod = (await import("@parcel/watcher")) as unknown as ParcelWatcher;
37
+ if (typeof mod.subscribe !== "function") throw new Error("no subscribe");
38
+ parcel = mod;
39
+ } catch {
40
+ parcel = null;
41
+ }
42
+ return parcel;
43
+ }
44
+
45
+ function fsWatchDirectory(
46
+ dir: string,
47
+ onChange: (events: WatchEvent[]) => void,
48
+ ignore: string[],
49
+ debounceMs: number,
50
+ ): () => Promise<void> {
51
+ const ignoreSet = new Set(ignore);
52
+ let timeout: ReturnType<typeof setTimeout> | null = null;
53
+ let handle: FSWatcher | null = null;
54
+ try {
55
+ handle = fsWatch(dir, { recursive: true }, (_event, filename) => {
56
+ if (!filename) return;
57
+ const rel = filename.toString();
58
+ if (rel.split(sep).some((part) => ignoreSet.has(part))) return;
59
+ if (timeout) clearTimeout(timeout);
60
+ timeout = setTimeout(() => onChange([{ type: "update", path: join(dir, rel) }]), debounceMs);
61
+ });
62
+ } catch {
63
+ // Directory may be unreadable/vanished — degrade to a no-op watcher.
64
+ }
65
+ return async () => {
66
+ if (timeout) clearTimeout(timeout);
67
+ handle?.close();
68
+ };
69
+ }
70
+
71
+ export async function watchDirectory(
72
+ dir: string,
73
+ onChange: (events: WatchEvent[]) => void,
74
+ options?: { debounceMs?: number; ignore?: string[] },
75
+ ): Promise<() => Promise<void>> {
76
+ const debounceMs = options?.debounceMs ?? 300;
77
+ const ignore = options?.ignore ?? ["node_modules", ".git", "dist", "build", ".next"];
78
+
79
+ const native = await loadParcel();
80
+ if (!native) return fsWatchDirectory(dir, onChange, ignore, debounceMs);
81
+
82
+ let timeout: ReturnType<typeof setTimeout> | null = null;
83
+ const subscription = await native.subscribe(
84
+ dir,
85
+ (err, events) => {
86
+ if (err) return;
87
+ if (timeout) clearTimeout(timeout);
88
+ timeout = setTimeout(() => onChange(events as unknown as WatchEvent[]), debounceMs);
89
+ },
90
+ { ignore },
91
+ );
92
+
93
+ return async () => {
94
+ if (timeout) clearTimeout(timeout);
95
+ await subscription.unsubscribe();
96
+ };
97
+ }
98
+
99
+ export async function watchGitHead(
100
+ dir: string,
101
+ onBranchChange: () => void,
102
+ ): Promise<(() => Promise<void>) | null> {
103
+ const gitDir = join(dir, ".git");
104
+
105
+ const native = await loadParcel();
106
+ if (!native) {
107
+ try {
108
+ const handle = fsWatch(gitDir, (_event, filename) => {
109
+ if (filename && filename.toString().endsWith("HEAD")) onBranchChange();
110
+ });
111
+ return async () => handle.close();
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ try {
118
+ const subscription = await native.subscribe(
119
+ gitDir,
120
+ (err, events) => {
121
+ if (err) return;
122
+ if (events.some((e) => e.path.endsWith("HEAD"))) {
123
+ onBranchChange();
124
+ }
125
+ },
126
+ { ignore: ["objects", "pack", "refs", "logs"] },
127
+ );
128
+ return () => subscription.unsubscribe();
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
@@ -0,0 +1,3 @@
1
+ # `preview` — daemon TUI widget
2
+
3
+ File content preview. See [ARCHITECTURE.md §3](../../../../../ARCHITECTURE.md#§3--package-map) and the [`preview` row in docs/widget-index.md](../../../../../docs/widget-index.md#daemon-tui-widgets-8).