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,416 @@
1
+ import { parseArgs } from "node:util";
2
+ import { readFileSync, existsSync, statSync } from "node:fs";
3
+ import { extname, basename, resolve } from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { render, useKeyboard, useTerminalDimensions } from "@opentui/solid";
6
+ import { RGBA, TextAttributes } from "@opentui/core";
7
+ import { createSignal, createMemo, onCleanup, Show, For } from "solid-js";
8
+ import { createTheme, type WidgetTheme } from "../lib/theme.ts";
9
+ import { getAppConfig } from "../../lib/app-config.ts";
10
+ import { getFileDiff } from "../lib/git.ts";
11
+
12
+ const { values } = parseArgs({
13
+ options: {
14
+ session: { type: "string" },
15
+ dir: { type: "string" },
16
+ theme: { type: "string" },
17
+ },
18
+ });
19
+
20
+ const session = values.session ?? "";
21
+ const dir = values.dir ?? process.cwd();
22
+ const themeConfig = values.theme ? JSON.parse(values.theme) : undefined;
23
+
24
+ const MAX_PREVIEW_LINES = 500;
25
+ const BINARY_EXTENSIONS = new Set([
26
+ ".png",
27
+ ".jpg",
28
+ ".jpeg",
29
+ ".gif",
30
+ ".bmp",
31
+ ".ico",
32
+ ".webp",
33
+ ".woff",
34
+ ".woff2",
35
+ ".ttf",
36
+ ".otf",
37
+ ".eot",
38
+ ".mp3",
39
+ ".mp4",
40
+ ".wav",
41
+ ".ogg",
42
+ ".webm",
43
+ ".zip",
44
+ ".tar",
45
+ ".gz",
46
+ ".bz2",
47
+ ".7z",
48
+ ".rar",
49
+ ".pdf",
50
+ ".exe",
51
+ ".dll",
52
+ ".so",
53
+ ".dylib",
54
+ ".node",
55
+ ".wasm",
56
+ ".tgz",
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
+ function getPreviewFile(): string | null {
64
+ if (!session) return null;
65
+ try {
66
+ return (
67
+ execFileSync("tmux", ["show-option", "-t", session, "-v", "@preview_file"], {
68
+ encoding: "utf-8",
69
+ stdio: ["ignore", "pipe", "ignore"],
70
+ }).trim() || null
71
+ );
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ function formatSize(bytes: number): string {
78
+ if (bytes < 1024) return `${bytes}B`;
79
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
80
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
81
+ }
82
+
83
+ const KEYWORD_RE =
84
+ /^(import|export|from|const|let|var|function|return|if|else|for|while|class|interface|type|async|await|try|catch|throw|new|switch|case|default|break|continue|enum|extends|implements|public|private|protected|static|readonly|abstract|override|declare|module|namespace|def|fn|pub|mut|use|mod|struct|impl|trait|match|loop|where|yield)\b/;
85
+
86
+ function getLineColor(
87
+ line: string,
88
+ theme: WidgetTheme,
89
+ ): { r: number; g: number; b: number; a: number } {
90
+ const trimmed = line.trim();
91
+ if (!trimmed) return theme.fg;
92
+
93
+ // Comments
94
+ if (
95
+ trimmed.startsWith("//") ||
96
+ trimmed.startsWith("#") ||
97
+ trimmed.startsWith("--") ||
98
+ trimmed.startsWith("/*") ||
99
+ trimmed.startsWith("*")
100
+ ) {
101
+ return theme.fgMuted;
102
+ }
103
+
104
+ // Strings
105
+ if (trimmed.startsWith('"') || trimmed.startsWith("'") || trimmed.startsWith("`")) {
106
+ return theme.gitAdded;
107
+ }
108
+
109
+ // Keywords
110
+ if (KEYWORD_RE.test(trimmed)) {
111
+ return theme.accent;
112
+ }
113
+
114
+ return theme.fg;
115
+ }
116
+
117
+ interface FileData {
118
+ content: string;
119
+ totalLines: number;
120
+ binary: boolean;
121
+ size: string;
122
+ }
123
+
124
+ // Parse diff to get line-level change info for gutter markers
125
+ function parseDiffLineMap(diff: string): Map<number, "added" | "modified"> {
126
+ const map = new Map<number, "added" | "modified">();
127
+ if (!diff) return map;
128
+ const lines = diff.split("\n");
129
+ for (const line of lines) {
130
+ // Parse hunk headers: @@ -oldStart,oldCount +newStart,newCount @@
131
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
132
+ if (hunkMatch) continue;
133
+ }
134
+ // Simpler approach: track added lines from the diff
135
+ let newLineNum = 0;
136
+ for (const line of lines) {
137
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/);
138
+ if (hunkMatch) {
139
+ newLineNum = parseInt(hunkMatch[1]!, 10);
140
+ continue;
141
+ }
142
+ if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("diff")) continue;
143
+ if (line.startsWith("+")) {
144
+ map.set(newLineNum, "added");
145
+ newLineNum++;
146
+ } else if (line.startsWith("-")) {
147
+ // Deleted line — mark the current position as modified
148
+ if (!map.has(newLineNum)) map.set(newLineNum, "modified");
149
+ } else {
150
+ newLineNum++;
151
+ }
152
+ }
153
+ return map;
154
+ }
155
+
156
+ function loadFile(filePath: string): FileData | null {
157
+ const fullPath = filePath.startsWith("/") ? filePath : resolve(dir, filePath);
158
+ if (!existsSync(fullPath)) return null;
159
+
160
+ let size: string;
161
+ try {
162
+ size = formatSize(statSync(fullPath).size);
163
+ } catch {
164
+ size = "";
165
+ }
166
+
167
+ if (BINARY_EXTENSIONS.has(extname(fullPath).toLowerCase())) {
168
+ return { content: "(binary file)", totalLines: 1, binary: true, size };
169
+ }
170
+ try {
171
+ const raw = readFileSync(fullPath, "utf-8");
172
+ if (raw.includes("\0")) {
173
+ return { content: "(binary file)", totalLines: 1, binary: true, size };
174
+ }
175
+ const allLines = raw.split("\n");
176
+ return {
177
+ content: allLines.slice(0, MAX_PREVIEW_LINES).join("\n"),
178
+ totalLines: allLines.length,
179
+ binary: false,
180
+ size,
181
+ };
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ render(
188
+ () => {
189
+ const theme = createTheme(themeConfig, getAppConfig().theme);
190
+ const dimensions = useTerminalDimensions();
191
+ const [filePath, setFilePath] = createSignal<string | null>(null);
192
+ const [fileContent, setFileContent] = createSignal<string | null>(null);
193
+ const [totalLines, setTotalLines] = createSignal(0);
194
+ const [fileSize, setFileSize] = createSignal("");
195
+ const [isBinary, setIsBinary] = createSignal(false);
196
+ const [fileDiff, setFileDiff] = createSignal<string | null>(null);
197
+ const [viewMode, setViewMode] = createSignal<"content" | "diff">("content");
198
+
199
+ // Poll tmux session variable for file path changes
200
+ const interval = setInterval(() => {
201
+ const newPath = getPreviewFile();
202
+ if (newPath !== filePath()) {
203
+ setFilePath(newPath);
204
+ setViewMode("content");
205
+ if (newPath) {
206
+ const result = loadFile(newPath);
207
+ if (result) {
208
+ setFileContent(result.content);
209
+ setTotalLines(result.totalLines);
210
+ setFileSize(result.size);
211
+ setIsBinary(result.binary);
212
+ } else {
213
+ setFileContent(null);
214
+ setTotalLines(0);
215
+ setFileSize("");
216
+ setIsBinary(false);
217
+ }
218
+ // Check for git diff
219
+ try {
220
+ const diff = getFileDiff(dir, newPath, false);
221
+ setFileDiff(diff || null);
222
+ } catch {
223
+ setFileDiff(null);
224
+ }
225
+ } else {
226
+ setFileContent(null);
227
+ setTotalLines(0);
228
+ setFileSize("");
229
+ setIsBinary(false);
230
+ setFileDiff(null);
231
+ }
232
+ }
233
+ }, 200);
234
+
235
+ onCleanup(() => clearInterval(interval));
236
+
237
+ const lines = createMemo(() => {
238
+ const content = fileContent();
239
+ if (!content) return [];
240
+ return content.split("\n");
241
+ });
242
+
243
+ const fileExt = createMemo(() => {
244
+ const fp = filePath();
245
+ return fp ? extname(fp).toLowerCase() : "";
246
+ });
247
+
248
+ const lineNumWidth = createMemo(() => Math.max(3, String(totalLines()).length));
249
+
250
+ const diffLineMap = createMemo(() => parseDiffLineMap(fileDiff() ?? ""));
251
+
252
+ useKeyboard((evt) => {
253
+ if (evt.name === "d") {
254
+ if (fileDiff()) setViewMode((m) => (m === "content" ? "diff" : "content"));
255
+ evt.preventDefault();
256
+ } else if (evt.name === "r" && filePath()) {
257
+ const result = loadFile(filePath()!);
258
+ if (result) {
259
+ setFileContent(result.content);
260
+ setTotalLines(result.totalLines);
261
+ setFileSize(result.size);
262
+ setIsBinary(result.binary);
263
+ }
264
+ try {
265
+ const diff = getFileDiff(dir, filePath()!, false);
266
+ setFileDiff(diff || null);
267
+ } catch {
268
+ setFileDiff(null);
269
+ }
270
+ evt.preventDefault();
271
+ } else if (evt.name === "q") {
272
+ process.exit(0);
273
+ }
274
+ });
275
+
276
+ return (
277
+ <box
278
+ width={dimensions().width}
279
+ height={dimensions().height}
280
+ backgroundColor={toRGBA(theme.bg)}
281
+ >
282
+ {/* Header */}
283
+ <Show
284
+ when={filePath()}
285
+ fallback={
286
+ <box flexGrow={1} paddingLeft={2} paddingTop={2}>
287
+ <text fg={toRGBA(theme.fgMuted)}>Select a file in the explorer</text>
288
+ <text fg={toRGBA(theme.border)} paddingTop={1}>
289
+ Navigate with ↑↓ keys
290
+ </text>
291
+ <text fg={toRGBA(theme.border)}>Preview updates automatically</text>
292
+ </box>
293
+ }
294
+ >
295
+ <box flexShrink={0} paddingLeft={1} flexDirection="row" gap={2}>
296
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
297
+ {basename(filePath()!)}
298
+ </text>
299
+ <Show when={!isBinary()}>
300
+ <text fg={toRGBA(theme.fgMuted)}>{totalLines()} lines</text>
301
+ </Show>
302
+ <text fg={toRGBA(theme.fgMuted)}>{fileSize()}</text>
303
+ <Show when={fileDiff()}>
304
+ {(() => {
305
+ const lines = fileDiff()!.split("\n");
306
+ const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
307
+ const removed = lines.filter(
308
+ (l) => l.startsWith("-") && !l.startsWith("---"),
309
+ ).length;
310
+ return (
311
+ <box flexDirection="row" gap={1}>
312
+ <Show when={added > 0}>
313
+ <text fg={toRGBA(theme.gitAdded)}>+{added}</text>
314
+ </Show>
315
+ <Show when={removed > 0}>
316
+ <text fg={toRGBA(theme.gitDeleted)}>-{removed}</text>
317
+ </Show>
318
+ <text fg={toRGBA(viewMode() === "diff" ? theme.gitModified : theme.fgMuted)}>
319
+ {viewMode() === "diff" ? "[diff]" : "[d:diff]"}
320
+ </text>
321
+ </box>
322
+ );
323
+ })()}
324
+ </Show>
325
+ </box>
326
+
327
+ {/* Separator */}
328
+ <box flexShrink={0} height={1}>
329
+ <text fg={toRGBA(theme.border)} wrapMode="none">
330
+ {"─".repeat(dimensions().width)}
331
+ </text>
332
+ </box>
333
+
334
+ {/* Diff view */}
335
+ <Show when={viewMode() === "diff" && fileDiff()}>
336
+ <scrollbox flexGrow={1}>
337
+ <For each={fileDiff()!.split("\n")}>
338
+ {(line) => {
339
+ const color = line.startsWith("+")
340
+ ? theme.diffAdded
341
+ : line.startsWith("-")
342
+ ? theme.diffRemoved
343
+ : line.startsWith("@@")
344
+ ? theme.diffHunk
345
+ : theme.diffContext;
346
+ const bg = line.startsWith("+")
347
+ ? theme.diffAddedBg
348
+ : line.startsWith("-")
349
+ ? theme.diffRemovedBg
350
+ : theme.diffContextBg;
351
+ return (
352
+ <box backgroundColor={toRGBA(bg)}>
353
+ <text fg={toRGBA(color)} wrapMode="none">
354
+ {line || " "}
355
+ </text>
356
+ </box>
357
+ );
358
+ }}
359
+ </For>
360
+ </scrollbox>
361
+ </Show>
362
+
363
+ {/* Content view */}
364
+ <Show when={viewMode() === "content" && fileContent()}>
365
+ <scrollbox flexGrow={1}>
366
+ <For each={lines()}>
367
+ {(line, lineNum) => {
368
+ const color = isBinary() ? theme.fgMuted : getLineColor(line, theme);
369
+ const lineNumber = lineNum() + 1;
370
+ const changeType = diffLineMap().get(lineNumber);
371
+ const gutterColor =
372
+ changeType === "added"
373
+ ? theme.gitAdded
374
+ : changeType === "modified"
375
+ ? theme.gitModified
376
+ : null;
377
+ const gutterChar = gutterColor ? "│" : " ";
378
+ return (
379
+ <box flexDirection="row">
380
+ <Show when={!isBinary()}>
381
+ <text
382
+ fg={toRGBA(gutterColor ?? theme.diffLineNumber)}
383
+ flexShrink={0}
384
+ wrapMode="none"
385
+ >
386
+ {gutterChar}
387
+ {String(lineNumber).padStart(lineNumWidth())}{" "}
388
+ </text>
389
+ </Show>
390
+ <text fg={toRGBA(color)} wrapMode="none">
391
+ {line || " "}
392
+ </text>
393
+ </box>
394
+ );
395
+ }}
396
+ </For>
397
+ </scrollbox>
398
+ </Show>
399
+
400
+ {/* Footer */}
401
+ <box flexShrink={0} paddingLeft={1}>
402
+ <text fg={toRGBA(theme.fgMuted)} wrapMode="none">
403
+ d:diff view r:refresh q:quit
404
+ </text>
405
+ </box>
406
+ </Show>
407
+ </box>
408
+ );
409
+ },
410
+ {
411
+ targetFps: 30,
412
+ exitOnCtrlC: true,
413
+ useKittyKeyboard: {},
414
+ autoFocus: false,
415
+ },
416
+ );
@@ -0,0 +1,121 @@
1
+ import { resolve, dirname } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { ThemeConfig } from "../types.ts";
5
+ import { shellEscape } from "../lib/shell.ts";
6
+ import { resolveTuiLaunch, findCompiledTui, isBunAvailable } from "../tui/compiled.ts";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+
10
+ interface WidgetOptions {
11
+ session: string;
12
+ dir: string;
13
+ target: string | null;
14
+ theme: ThemeConfig | null;
15
+ }
16
+
17
+ // The .tsx extension is used at runtime; Bun handles JSX via preload plugin.
18
+ // Exactly the widgets that exist — a stale entry here becomes a dead
19
+ // `Module not found` pane at launch time.
20
+ const WIDGET_ENTRY_POINTS: Record<string, string> = {
21
+ explorer: "explorer/index.tsx",
22
+ changes: "changes/index.tsx",
23
+ preview: "preview/index.tsx",
24
+ setup: "setup/index.tsx",
25
+ config: "config/index.tsx",
26
+ sidebar: "sidebar/index.tsx",
27
+ };
28
+
29
+ /**
30
+ * Resolve a widget entry to an absolute path that works from BOTH runtimes:
31
+ * unbundled (this file lives in packages/daemon/src/widgets — entries are
32
+ * siblings) and the esbuild bundle (import.meta.url collapses to bin/cli.js,
33
+ * so entries live at ../packages/daemon/src/widgets from there). Probing with
34
+ * existsSync keeps one code path honest instead of guessing by environment.
35
+ */
36
+ function widgetEntryPath(entry: string): string {
37
+ const sibling = resolve(__dirname, entry);
38
+ if (existsSync(sibling)) return sibling;
39
+ return resolve(__dirname, "../packages/daemon/src/widgets", entry);
40
+ }
41
+
42
+ /**
43
+ * The tmux-ide repo root — where bunfig.toml (the JSX preload) lives. Widgets
44
+ * must be SPAWNED from here regardless of which project they render (the
45
+ * project dir travels via --dir); cd-ing to the project instead crashes bun
46
+ * with "Cannot find module react/jsx-dev-runtime" outside this repo.
47
+ */
48
+ const REPO_ROOT = existsSync(resolve(__dirname, "explorer/index.tsx"))
49
+ ? resolve(__dirname, "../../../..") // unbundled: src/widgets → repo root
50
+ : resolve(__dirname, ".."); // bundled: bin → repo root
51
+
52
+ function widgetArgs(opts: WidgetOptions): string[] {
53
+ const args = [`--session=${opts.session}`, `--dir=${opts.dir}`];
54
+ if (opts.target) args.push(`--target=${opts.target}`);
55
+ if (opts.theme) args.push(`--theme=${JSON.stringify(opts.theme)}`);
56
+ return args;
57
+ }
58
+
59
+ export function resolveWidgetCommand(type: string, opts: WidgetOptions): string {
60
+ const entry = WIDGET_ENTRY_POINTS[type];
61
+ if (!entry) throw new Error(`Unknown widget type: ${type}`);
62
+
63
+ const scriptPath = widgetEntryPath(entry);
64
+ const launch = resolveTuiLaunch({
65
+ surface: type,
66
+ scriptPath,
67
+ args: widgetArgs(opts),
68
+ checkoutExists: existsSync(scriptPath),
69
+ bunAvailable: isBunAvailable(),
70
+ compiledBinary: findCompiledTui(),
71
+ });
72
+
73
+ if (launch.mode === "unavailable") {
74
+ throw new Error(`Cannot launch ${type} widget: ${launch.reasons.join("; ")}`);
75
+ }
76
+
77
+ const escapedArgs = launch.argv.map(shellEscape).join(" ");
78
+ if (launch.mode === "bun") {
79
+ // cd to the tmux-ide REPO root (not the project) so bunfig.toml's JSX
80
+ // preload is found; the project dir rides in via --dir.
81
+ return `cd ${shellEscape(REPO_ROOT)} && bun ${escapedArgs}`;
82
+ }
83
+ // Compiled binary: no bunfig to find (JSX + native dylib are baked in). Run
84
+ // from the pane's dir so a stray repo-root bunfig can't hijack the preload.
85
+ return `cd ${shellEscape(opts.dir)} && ${shellEscape(launch.bin)} ${escapedArgs}`;
86
+ }
87
+
88
+ export interface WidgetSpawnSpec {
89
+ cwd: string;
90
+ cmd: string[];
91
+ }
92
+
93
+ /**
94
+ * Structured form of `resolveWidgetCommand` for direct PTY spawning. Callers
95
+ * that own the cwd separately (e.g. the /ws/pty bridge accepting cwd in its
96
+ * init frame) avoid the shell hop and get exec-style argv.
97
+ */
98
+ export function resolveWidgetSpawn(type: string, opts: WidgetOptions): WidgetSpawnSpec {
99
+ const entry = WIDGET_ENTRY_POINTS[type];
100
+ if (!entry) throw new Error(`Unknown widget type: ${type}`);
101
+
102
+ const scriptPath = widgetEntryPath(entry);
103
+ const launch = resolveTuiLaunch({
104
+ surface: type,
105
+ scriptPath,
106
+ args: widgetArgs(opts),
107
+ checkoutExists: existsSync(scriptPath),
108
+ bunAvailable: isBunAvailable(),
109
+ compiledBinary: findCompiledTui(),
110
+ });
111
+
112
+ if (launch.mode === "unavailable") {
113
+ throw new Error(`Cannot launch ${type} widget: ${launch.reasons.join("; ")}`);
114
+ }
115
+ // Bun spawns from the repo root (bunfig preload); the binary spawns from the
116
+ // pane's dir (self-contained, avoids a stray bunfig preload).
117
+ const cwd = launch.mode === "bun" ? REPO_ROOT : opts.dir;
118
+ return { cwd, cmd: [launch.bin, ...launch.argv] };
119
+ }
120
+
121
+ export const WIDGET_TYPES = Object.keys(WIDGET_ENTRY_POINTS);
@@ -0,0 +1,3 @@
1
+ # `setup` — daemon TUI widget
2
+
3
+ Project setup wizard. See [ARCHITECTURE.md §3](../../../../../ARCHITECTURE.md#§3--package-map) and the [`setup` row in docs/widget-index.md](../../../../../docs/widget-index.md#daemon-tui-widgets-8).
@@ -0,0 +1,112 @@
1
+ import { createSignal, onMount } from "solid-js";
2
+ import { RGBA, TextAttributes, type InputRenderable } from "@opentui/core";
3
+ import { useKeyboard } from "@opentui/solid";
4
+ import type { IdeConfig } from "../../schemas/ide-config.ts";
5
+ import type { WidgetTheme, RGBA as RGBAType } from "../lib/theme.ts";
6
+
7
+ function toRGBA(c: RGBAType): RGBA {
8
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
9
+ }
10
+
11
+ /** Extract titles of claude panes from an IdeConfig. */
12
+ function extractClaudePaneTitles(config: IdeConfig): string[] {
13
+ const titles: string[] = [];
14
+ for (const row of config.rows) {
15
+ for (const pane of row.panes) {
16
+ if (pane.command === "claude") {
17
+ titles.push(pane.title ?? "Claude");
18
+ }
19
+ }
20
+ }
21
+ return titles;
22
+ }
23
+
24
+ interface AgentNamingProps {
25
+ config: IdeConfig;
26
+ onContinue: (names: string[]) => void;
27
+ theme: WidgetTheme;
28
+ }
29
+
30
+ export function AgentNaming(props: AgentNamingProps) {
31
+ const defaults = extractClaudePaneTitles(props.config);
32
+ const theme = props.theme;
33
+
34
+ // Create a signal per pane name
35
+ const nameSignals = defaults.map((d) => createSignal(d));
36
+ const [activeField, setActiveField] = createSignal(0);
37
+
38
+ const inputRefs: (InputRenderable | undefined)[] = new Array(defaults.length).fill(undefined);
39
+
40
+ onMount(() => {
41
+ setTimeout(() => inputRefs[0]?.focus(), 50);
42
+ });
43
+
44
+ useKeyboard((evt) => {
45
+ if (evt.name === "tab") {
46
+ const next = (activeField() + 1) % nameSignals.length;
47
+ setActiveField(next);
48
+ setTimeout(() => inputRefs[next]?.focus(), 10);
49
+ evt.preventDefault();
50
+ } else if (evt.name === "return") {
51
+ const names = nameSignals.map(([getter]) => getter());
52
+ props.onContinue(names);
53
+ evt.preventDefault();
54
+ }
55
+ });
56
+
57
+ return (
58
+ <box paddingLeft={1} paddingRight={1}>
59
+ {/* Header */}
60
+ <box flexShrink={0} paddingBottom={1}>
61
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
62
+ Name Your Agents
63
+ </text>
64
+ <text fg={toRGBA(theme.fgMuted)}>Customize the names for each Claude pane.</text>
65
+ </box>
66
+
67
+ {/* Input fields */}
68
+ {nameSignals.map(([getter, setter], index) => {
69
+ const isActive = () => activeField() === index;
70
+ return (
71
+ <box
72
+ flexShrink={0}
73
+ paddingBottom={1}
74
+ onMouseDown={() => {
75
+ setActiveField(index);
76
+ setTimeout(() => inputRefs[index]?.focus(), 10);
77
+ }}
78
+ >
79
+ <text fg={toRGBA(isActive() ? theme.accent : theme.fgMuted)}>Pane {index + 1}</text>
80
+ <input
81
+ value={getter()}
82
+ placeholder={defaults[index] ?? "Agent"}
83
+ onInput={(v: string) => setter(v)}
84
+ focusedBackgroundColor={toRGBA(theme.selected)}
85
+ cursorColor={toRGBA(theme.accent)}
86
+ focusedTextColor={toRGBA(theme.fg)}
87
+ ref={(r: InputRenderable) => {
88
+ inputRefs[index] = r;
89
+ }}
90
+ />
91
+ </box>
92
+ );
93
+ })}
94
+
95
+ {/* Spacer */}
96
+ <box flexGrow={1} />
97
+
98
+ {/* Footer */}
99
+ <box flexShrink={0}>
100
+ <box flexShrink={0} height={1}>
101
+ <text fg={toRGBA(theme.border)} wrapMode="none">
102
+ {"─".repeat(40)}
103
+ </text>
104
+ </box>
105
+ <box flexDirection="row" gap={2}>
106
+ <text fg={toRGBA(theme.fgMuted)}>Tab:next field</text>
107
+ <text fg={toRGBA(theme.fgMuted)}>Enter:continue</text>
108
+ </box>
109
+ </box>
110
+ </box>
111
+ );
112
+ }