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,691 @@
1
+ import { parseArgs } from "node:util";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } 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 {
8
+ createSignal,
9
+ createMemo,
10
+ createEffect,
11
+ onMount,
12
+ onCleanup,
13
+ batch,
14
+ Show,
15
+ For,
16
+ } from "solid-js";
17
+ import { createTheme } from "../lib/theme.ts";
18
+ import { getAppConfig } from "../../lib/app-config.ts";
19
+ import { getGitBranch } from "../lib/git.ts";
20
+ import { watchDirectory } from "../lib/watcher.ts";
21
+ import { matchGrammar } from "../lib/grammar.ts";
22
+ import { HelpOverlay, type WidgetKey } from "../lib/help-overlay.tsx";
23
+ import { findPaneByTitle, sendCommand, findPaneByPattern } from "../lib/pane-comms.ts";
24
+
25
+ /** Changes keys beyond the shared grammar — listed in the `?` overlay. */
26
+ const WIDGET_KEYS: WidgetKey[] = [
27
+ { key: "s / u", label: "stage / unstage file" },
28
+ { key: "S / U", label: "stage / unstage all" },
29
+ { key: "c", label: "send to claude" },
30
+ { key: "r", label: "refresh" },
31
+ { key: "g / G", label: "top / bottom" },
32
+ ];
33
+
34
+ const { values } = parseArgs({
35
+ options: {
36
+ session: { type: "string" },
37
+ dir: { type: "string" },
38
+ target: { type: "string" },
39
+ theme: { type: "string" },
40
+ },
41
+ });
42
+
43
+ const session = values.session ?? "";
44
+ const dir = values.dir ?? process.cwd();
45
+ const targetTitle = values.target ?? null;
46
+ const themeConfig = values.theme ? JSON.parse(values.theme) : undefined;
47
+
48
+ // --- Git helpers ---
49
+
50
+ function execGit(args: string[]): string {
51
+ try {
52
+ return execFileSync(
53
+ "git",
54
+ ["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", ...args],
55
+ { cwd: dir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
56
+ );
57
+ } catch {
58
+ return "";
59
+ }
60
+ }
61
+
62
+ interface ChangedFile {
63
+ path: string;
64
+ status: "M" | "A" | "D" | "?";
65
+ staged: boolean;
66
+ additions: number;
67
+ deletions: number;
68
+ }
69
+
70
+ interface ChangesState {
71
+ staged: ChangedFile[];
72
+ unstaged: ChangedFile[];
73
+ untracked: ChangedFile[];
74
+ }
75
+
76
+ function getChanges(): ChangesState {
77
+ const staged: ChangedFile[] = [];
78
+ const unstaged: ChangedFile[] = [];
79
+ const untracked: ChangedFile[] = [];
80
+
81
+ // Staged deleted files
82
+ const stagedDeleted = new Set(
83
+ execGit(["diff", "--cached", "--name-only", "--diff-filter=D"]).split("\n").filter(Boolean),
84
+ );
85
+
86
+ // Staged changes
87
+ for (const line of execGit(["diff", "--cached", "--numstat"]).split("\n").filter(Boolean)) {
88
+ const parts = line.split("\t");
89
+ if (parts.length < 3 || parts[0] === "-") continue;
90
+ const filepath = parts.slice(2).join("\t");
91
+ staged.push({
92
+ path: filepath,
93
+ status: stagedDeleted.has(filepath) ? "D" : "M",
94
+ staged: true,
95
+ additions: parseInt(parts[0]!, 10) || 0,
96
+ deletions: parseInt(parts[1]!, 10) || 0,
97
+ });
98
+ }
99
+ // Staged deleted not in numstat
100
+ for (const filepath of stagedDeleted) {
101
+ if (!staged.some((f) => f.path === filepath)) {
102
+ staged.push({ path: filepath, status: "D", staged: true, additions: 0, deletions: 0 });
103
+ }
104
+ }
105
+
106
+ // Unstaged deleted files
107
+ const unstagedDeleted = new Set(
108
+ execGit(["diff", "--name-only", "--diff-filter=D"]).split("\n").filter(Boolean),
109
+ );
110
+
111
+ // Unstaged changes
112
+ for (const line of execGit(["diff", "--numstat"]).split("\n").filter(Boolean)) {
113
+ const parts = line.split("\t");
114
+ if (parts.length < 3 || parts[0] === "-") continue;
115
+ const filepath = parts.slice(2).join("\t");
116
+ unstaged.push({
117
+ path: filepath,
118
+ status: unstagedDeleted.has(filepath) ? "D" : "M",
119
+ staged: false,
120
+ additions: parseInt(parts[0]!, 10) || 0,
121
+ deletions: parseInt(parts[1]!, 10) || 0,
122
+ });
123
+ }
124
+ for (const filepath of unstagedDeleted) {
125
+ if (!unstaged.some((f) => f.path === filepath)) {
126
+ unstaged.push({ path: filepath, status: "D", staged: false, additions: 0, deletions: 0 });
127
+ }
128
+ }
129
+
130
+ // Untracked files
131
+ for (const filepath of execGit(["ls-files", "--others", "--exclude-standard"])
132
+ .split("\n")
133
+ .filter(Boolean)) {
134
+ let additions = 0;
135
+ try {
136
+ additions = readFileSync(join(dir, filepath), "utf-8").split("\n").length;
137
+ } catch {
138
+ /* ignore */
139
+ }
140
+ untracked.push({ path: filepath, status: "?", staged: false, additions, deletions: 0 });
141
+ }
142
+
143
+ return { staged, unstaged, untracked };
144
+ }
145
+
146
+ function getChangeDiff(file: ChangedFile): string {
147
+ if (file.status === "?") {
148
+ // Untracked: show all content as additions
149
+ try {
150
+ const content = readFileSync(join(dir, file.path), "utf-8");
151
+ return content
152
+ .split("\n")
153
+ .map((l) => `+${l}`)
154
+ .join("\n");
155
+ } catch {
156
+ return "";
157
+ }
158
+ }
159
+ const args = file.staged ? ["diff", "--cached", "--", file.path] : ["diff", "--", file.path];
160
+ return execGit(args);
161
+ }
162
+
163
+ function stageFile(path: string): void {
164
+ execGit(["add", "--", path]);
165
+ }
166
+
167
+ function unstageFile(path: string): void {
168
+ execGit(["reset", "HEAD", "--", path]);
169
+ }
170
+
171
+ function stageAll(): void {
172
+ execGit(["add", "-A"]);
173
+ }
174
+
175
+ function unstageAll(): void {
176
+ execGit(["reset", "HEAD"]);
177
+ }
178
+
179
+ // --- Rendering ---
180
+
181
+ function toRGBA(c: { r: number; g: number; b: number; a: number }): RGBA {
182
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
183
+ }
184
+
185
+ const TRANSPARENT = RGBA.fromInts(0, 0, 0, 0);
186
+
187
+ type ListItem =
188
+ | { kind: "header"; label: string }
189
+ | { kind: "file"; file: ChangedFile; globalIndex: number };
190
+
191
+ /** Narrow every group of a {@link ChangesState} to files whose path matches the
192
+ * (case-insensitive) query. An empty query returns the state unchanged. */
193
+ function filterChanges(changes: ChangesState, query: string): ChangesState {
194
+ const q = query.trim().toLowerCase();
195
+ if (!q) return changes;
196
+ const match = (f: ChangedFile) => f.path.toLowerCase().includes(q);
197
+ return {
198
+ staged: changes.staged.filter(match),
199
+ unstaged: changes.unstaged.filter(match),
200
+ untracked: changes.untracked.filter(match),
201
+ };
202
+ }
203
+
204
+ function buildListItems(changes: ChangesState): { items: ListItem[]; fileCount: number } {
205
+ const items: ListItem[] = [];
206
+ let globalIndex = 0;
207
+
208
+ if (changes.staged.length > 0) {
209
+ items.push({ kind: "header", label: `Staged (${changes.staged.length})` });
210
+ for (const file of changes.staged) {
211
+ items.push({ kind: "file", file, globalIndex });
212
+ globalIndex++;
213
+ }
214
+ }
215
+ if (changes.unstaged.length > 0) {
216
+ items.push({ kind: "header", label: `Unstaged (${changes.unstaged.length})` });
217
+ for (const file of changes.unstaged) {
218
+ items.push({ kind: "file", file, globalIndex });
219
+ globalIndex++;
220
+ }
221
+ }
222
+ if (changes.untracked.length > 0) {
223
+ items.push({ kind: "header", label: `Untracked (${changes.untracked.length})` });
224
+ for (const file of changes.untracked) {
225
+ items.push({ kind: "file", file, globalIndex });
226
+ globalIndex++;
227
+ }
228
+ }
229
+ return { items, fileCount: globalIndex };
230
+ }
231
+
232
+ render(
233
+ () => {
234
+ const theme = createTheme(themeConfig, getAppConfig().theme);
235
+ const dimensions = useTerminalDimensions();
236
+
237
+ const [changes, setChanges] = createSignal<ChangesState>(getChanges());
238
+ const [branch, setBranch] = createSignal(getGitBranch(dir));
239
+ const [selected, setSelected] = createSignal(0);
240
+ const [diffContent, setDiffContent] = createSignal<string | null>(null);
241
+ const [inputMode, setInputMode] = createSignal<"keyboard" | "mouse">("keyboard");
242
+ const [filterMode, setFilterMode] = createSignal(false);
243
+ const [filterQuery, setFilterQuery] = createSignal("");
244
+ const [helpOpen, setHelpOpen] = createSignal(false);
245
+
246
+ // The visible file set narrows to the `/` filter query while filtering; the
247
+ // list, diff selection, and staging all read through it.
248
+ const visibleChanges = createMemo(() =>
249
+ filterMode() ? filterChanges(changes(), filterQuery()) : changes(),
250
+ );
251
+
252
+ const allFiles = createMemo(() => [
253
+ ...visibleChanges().staged,
254
+ ...visibleChanges().unstaged,
255
+ ...visibleChanges().untracked,
256
+ ]);
257
+
258
+ const listData = createMemo(() => buildListItems(visibleChanges()));
259
+
260
+ const totalAdditions = createMemo(() => allFiles().reduce((s, f) => s + f.additions, 0));
261
+ const totalDeletions = createMemo(() => allFiles().reduce((s, f) => s + f.deletions, 0));
262
+
263
+ // Load diff when selection changes
264
+ createEffect(() => {
265
+ const file = allFiles()[selected()];
266
+ if (file) {
267
+ try {
268
+ setDiffContent(getChangeDiff(file));
269
+ } catch {
270
+ setDiffContent(null);
271
+ }
272
+ } else {
273
+ setDiffContent(null);
274
+ }
275
+ });
276
+
277
+ // File watcher
278
+ let stopWatch: (() => Promise<void>) | null = null;
279
+ onMount(async () => {
280
+ try {
281
+ stopWatch = await watchDirectory(
282
+ dir,
283
+ () => {
284
+ batch(() => {
285
+ setChanges(getChanges());
286
+ setBranch(getGitBranch(dir));
287
+ });
288
+ },
289
+ { debounceMs: 500 },
290
+ );
291
+ } catch {
292
+ /* watcher unavailable */
293
+ }
294
+ });
295
+ onCleanup(async () => {
296
+ await stopWatch?.();
297
+ });
298
+
299
+ function resolveTargetPane(): string | null {
300
+ if (!session) return null;
301
+ if (targetTitle) return findPaneByTitle(session, targetTitle);
302
+ return findPaneByPattern(session, "claude");
303
+ }
304
+
305
+ // Keyboard
306
+ useKeyboard((evt) => {
307
+ setInputMode("keyboard");
308
+
309
+ // Help overlay swallows keys: esc / q / ? close it.
310
+ if (helpOpen()) {
311
+ const g = matchGrammar(evt);
312
+ if (g === "dismiss" || g === "quit" || g === "help") setHelpOpen(false);
313
+ evt.preventDefault();
314
+ return;
315
+ }
316
+
317
+ // The `/` filter narrows the file list. Per the grammar's escape
318
+ // precedence esc closes the FILTER first; arrows navigate, typing narrows.
319
+ if (filterMode()) {
320
+ if (evt.name === "escape" || evt.name === "return") {
321
+ setFilterMode(false);
322
+ setFilterQuery("");
323
+ setSelected(0);
324
+ evt.preventDefault();
325
+ return;
326
+ }
327
+ if (evt.name === "up") {
328
+ setSelected((i) => Math.max(0, i - 1));
329
+ evt.preventDefault();
330
+ return;
331
+ }
332
+ if (evt.name === "down") {
333
+ setSelected((i) => Math.min(allFiles().length - 1, i + 1));
334
+ evt.preventDefault();
335
+ return;
336
+ }
337
+ if (evt.name === "backspace") {
338
+ setFilterQuery((q) => q.slice(0, -1));
339
+ setSelected(0);
340
+ evt.preventDefault();
341
+ return;
342
+ }
343
+ if (evt.name.length === 1 && !evt.ctrl && !evt.meta) {
344
+ setFilterQuery((q) => q + evt.name);
345
+ setSelected(0);
346
+ evt.preventDefault();
347
+ return;
348
+ }
349
+ return;
350
+ }
351
+
352
+ const files = allFiles();
353
+
354
+ // The shared grammar runs FIRST; git/staging keys fall through below.
355
+ const grammar = matchGrammar(evt);
356
+ if (grammar === "navUp") {
357
+ setSelected((i) => Math.max(0, i - 1));
358
+ evt.preventDefault();
359
+ return;
360
+ } else if (grammar === "navDown") {
361
+ setSelected((i) => Math.min(files.length - 1, i + 1));
362
+ evt.preventDefault();
363
+ return;
364
+ } else if (grammar === "filter") {
365
+ setFilterMode(true);
366
+ setFilterQuery("");
367
+ setSelected(0);
368
+ evt.preventDefault();
369
+ return;
370
+ } else if (grammar === "help") {
371
+ setHelpOpen(true);
372
+ evt.preventDefault();
373
+ return;
374
+ } else if (grammar === "dismiss" || grammar === "quit") {
375
+ // Nothing is layered here, so esc/q close the panel popup.
376
+ process.exit(0);
377
+ }
378
+
379
+ if (evt.name === "s" && !evt.shift) {
380
+ const file = files[selected()];
381
+ if (file && !file.staged) {
382
+ stageFile(file.path);
383
+ setChanges(getChanges());
384
+ }
385
+ evt.preventDefault();
386
+ } else if (evt.name === "u" && !evt.shift) {
387
+ const file = files[selected()];
388
+ if (file && file.staged) {
389
+ unstageFile(file.path);
390
+ setChanges(getChanges());
391
+ }
392
+ evt.preventDefault();
393
+ } else if (evt.shift && evt.name === "s") {
394
+ stageAll();
395
+ setChanges(getChanges());
396
+ evt.preventDefault();
397
+ } else if (evt.shift && evt.name === "u") {
398
+ unstageAll();
399
+ setChanges(getChanges());
400
+ evt.preventDefault();
401
+ } else if (evt.name === "c") {
402
+ const file = files[selected()];
403
+ if (file) {
404
+ const targetId = resolveTargetPane();
405
+ if (targetId) sendCommand(session, targetId, `read ${file.path}`);
406
+ }
407
+ evt.preventDefault();
408
+ } else if (evt.name === "r") {
409
+ batch(() => {
410
+ setChanges(getChanges());
411
+ setBranch(getGitBranch(dir));
412
+ });
413
+ evt.preventDefault();
414
+ } else if (evt.name === "g" && !evt.shift) {
415
+ setSelected(0);
416
+ evt.preventDefault();
417
+ } else if (evt.shift && evt.name === "g") {
418
+ setSelected(files.length - 1);
419
+ evt.preventDefault();
420
+ }
421
+ });
422
+
423
+ const listHeight = createMemo(() => Math.max(5, Math.floor(dimensions().height * 0.4)));
424
+
425
+ const diffLines = createMemo(() => {
426
+ const d = diffContent();
427
+ if (!d) return [];
428
+ return d.split("\n");
429
+ });
430
+
431
+ return (
432
+ <box
433
+ width={dimensions().width}
434
+ height={dimensions().height}
435
+ backgroundColor={toRGBA(theme.bg)}
436
+ >
437
+ <Show when={helpOpen()}>
438
+ <HelpOverlay theme={theme} title="changes" widgetKeys={WIDGET_KEYS} />
439
+ </Show>
440
+ <Show when={!helpOpen()}>
441
+ {/* Header */}
442
+ <box flexShrink={0} paddingLeft={1} paddingBottom={1} flexDirection="row" gap={2}>
443
+ <Show when={branch()}>
444
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
445
+ {"⎇ "}
446
+ {branch()}
447
+ </text>
448
+ </Show>
449
+ <text fg={toRGBA(theme.fg)} attributes={TextAttributes.BOLD}>
450
+ Changes
451
+ </text>
452
+ <Show when={totalAdditions() > 0}>
453
+ <text fg={toRGBA(theme.gitAdded)}>+{totalAdditions()}</text>
454
+ </Show>
455
+ <Show when={totalDeletions() > 0}>
456
+ <text fg={toRGBA(theme.gitDeleted)}>-{totalDeletions()}</text>
457
+ </Show>
458
+ <text fg={toRGBA(theme.fgMuted)}>{allFiles().length} files</text>
459
+ </box>
460
+
461
+ {/* Filter line */}
462
+ <Show when={filterMode()}>
463
+ <box flexShrink={0} paddingLeft={1} flexDirection="row" gap={1}>
464
+ <text fg={toRGBA(theme.accent)}>/</text>
465
+ <text fg={toRGBA(theme.fg)}>{filterQuery()}</text>
466
+ <text fg={toRGBA(theme.fgMuted)}>_</text>
467
+ </box>
468
+ </Show>
469
+
470
+ {/* File list */}
471
+ <scrollbox maxHeight={listHeight()} flexShrink={0}>
472
+ <Show
473
+ when={allFiles().length > 0}
474
+ fallback={
475
+ <box paddingLeft={2} paddingTop={1}>
476
+ <text fg={toRGBA(theme.fgMuted)}>no changes — working tree clean ✓</text>
477
+ </box>
478
+ }
479
+ >
480
+ <For each={listData().items}>
481
+ {(item) => {
482
+ if (item.kind === "header") {
483
+ return (
484
+ <box paddingTop={1}>
485
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
486
+ {" "}
487
+ {item.label}
488
+ </text>
489
+ </box>
490
+ );
491
+ }
492
+ const isSelected = createMemo(() => item.globalIndex === selected());
493
+ const statusColor = () => {
494
+ if (item.file.staged) return theme.gitAdded;
495
+ switch (item.file.status) {
496
+ case "M":
497
+ return theme.gitModified;
498
+ case "D":
499
+ return theme.gitDeleted;
500
+ case "?":
501
+ return theme.gitUntracked;
502
+ default:
503
+ return theme.fgMuted;
504
+ }
505
+ };
506
+ return (
507
+ <box
508
+ flexDirection="row"
509
+ paddingLeft={1}
510
+ paddingRight={1}
511
+ backgroundColor={isSelected() ? toRGBA(theme.selected) : TRANSPARENT}
512
+ onMouseMove={() => setInputMode("mouse")}
513
+ onMouseDown={() => setSelected(item.globalIndex)}
514
+ onMouseOver={() => {
515
+ if (inputMode() === "mouse") setSelected(item.globalIndex);
516
+ }}
517
+ >
518
+ <text fg={toRGBA(statusColor())} wrapMode="none" flexShrink={0}>
519
+ {" "}
520
+ {item.file.status}{" "}
521
+ </text>
522
+ <text
523
+ fg={toRGBA(isSelected() ? theme.selectedText : theme.fg)}
524
+ wrapMode="none"
525
+ flexGrow={1}
526
+ >
527
+ {item.file.path}
528
+ </text>
529
+ <Show when={item.file.additions > 0}>
530
+ <text fg={toRGBA(theme.gitAdded)} flexShrink={0} wrapMode="none">
531
+ {" +"}
532
+ {item.file.additions}
533
+ </text>
534
+ </Show>
535
+ <Show when={item.file.deletions > 0}>
536
+ <text fg={toRGBA(theme.gitDeleted)} flexShrink={0} wrapMode="none">
537
+ {" -"}
538
+ {item.file.deletions}
539
+ </text>
540
+ </Show>
541
+ <Show when={isSelected()}>
542
+ <text
543
+ fg={toRGBA(theme.fgMuted)}
544
+ flexShrink={0}
545
+ wrapMode="none"
546
+ onMouseUp={() => {
547
+ if (item.file.staged) {
548
+ unstageFile(item.file.path);
549
+ } else {
550
+ stageFile(item.file.path);
551
+ }
552
+ setChanges(getChanges());
553
+ }}
554
+ >
555
+ {item.file.staged ? " u:unstage" : " s:stage"}
556
+ </text>
557
+ </Show>
558
+ </box>
559
+ );
560
+ }}
561
+ </For>
562
+ </Show>
563
+ </scrollbox>
564
+
565
+ {/* Separator */}
566
+ <box flexShrink={0} height={1}>
567
+ <text fg={toRGBA(theme.border)} wrapMode="none">
568
+ {"─".repeat(dimensions().width)}
569
+ </text>
570
+ </box>
571
+
572
+ {/* Diff preview */}
573
+ <Show
574
+ when={diffContent()}
575
+ fallback={
576
+ <box flexGrow={1} paddingLeft={2} paddingTop={1}>
577
+ <text fg={toRGBA(theme.fgMuted)}>Select a file to preview changes</text>
578
+ </box>
579
+ }
580
+ >
581
+ <scrollbox flexGrow={1}>
582
+ <For each={diffLines()}>
583
+ {(line) => {
584
+ const fg = line.startsWith("+")
585
+ ? theme.diffAdded
586
+ : line.startsWith("-")
587
+ ? theme.diffRemoved
588
+ : line.startsWith("@@")
589
+ ? theme.diffHunk
590
+ : theme.diffContext;
591
+ const bg = line.startsWith("+")
592
+ ? theme.diffAddedBg
593
+ : line.startsWith("-")
594
+ ? theme.diffRemovedBg
595
+ : theme.diffContextBg;
596
+ return (
597
+ <box backgroundColor={toRGBA(bg)}>
598
+ <text fg={toRGBA(fg)} wrapMode="none">
599
+ {line || " "}
600
+ </text>
601
+ </box>
602
+ );
603
+ }}
604
+ </For>
605
+ </scrollbox>
606
+ </Show>
607
+
608
+ {/* Footer */}
609
+ <box flexShrink={0} paddingLeft={1} paddingTop={1} flexDirection="row" gap={1}>
610
+ <text fg={toRGBA(theme.fgMuted)} wrapMode="none">
611
+ ↑↓:nav
612
+ </text>
613
+ <text
614
+ fg={toRGBA(theme.fgMuted)}
615
+ wrapMode="none"
616
+ onMouseUp={() => {
617
+ const file = allFiles()[selected()];
618
+ if (file && !file.staged) {
619
+ stageFile(file.path);
620
+ setChanges(getChanges());
621
+ }
622
+ }}
623
+ >
624
+ s:stage
625
+ </text>
626
+ <text
627
+ fg={toRGBA(theme.fgMuted)}
628
+ wrapMode="none"
629
+ onMouseUp={() => {
630
+ const file = allFiles()[selected()];
631
+ if (file && file.staged) {
632
+ unstageFile(file.path);
633
+ setChanges(getChanges());
634
+ }
635
+ }}
636
+ >
637
+ u:unstage
638
+ </text>
639
+ <text
640
+ fg={toRGBA(theme.fgMuted)}
641
+ wrapMode="none"
642
+ onMouseUp={() => {
643
+ stageAll();
644
+ setChanges(getChanges());
645
+ }}
646
+ >
647
+ S:all
648
+ </text>
649
+ <text
650
+ fg={toRGBA(theme.fgMuted)}
651
+ wrapMode="none"
652
+ onMouseUp={() => {
653
+ unstageAll();
654
+ setChanges(getChanges());
655
+ }}
656
+ >
657
+ U:all
658
+ </text>
659
+ <text
660
+ fg={toRGBA(theme.fgMuted)}
661
+ wrapMode="none"
662
+ onMouseUp={() => {
663
+ batch(() => {
664
+ setChanges(getChanges());
665
+ setBranch(getGitBranch(dir));
666
+ });
667
+ }}
668
+ >
669
+ r:refresh
670
+ </text>
671
+ <text fg={toRGBA(theme.fgMuted)} wrapMode="none">
672
+ /:filter
673
+ </text>
674
+ <text fg={toRGBA(theme.fgMuted)} wrapMode="none">
675
+ ?:help
676
+ </text>
677
+ <text fg={toRGBA(theme.fgMuted)} wrapMode="none">
678
+ q:quit
679
+ </text>
680
+ </box>
681
+ </Show>
682
+ </box>
683
+ );
684
+ },
685
+ {
686
+ targetFps: 30,
687
+ exitOnCtrlC: true,
688
+ useKittyKeyboard: {},
689
+ autoFocus: false,
690
+ },
691
+ );
@@ -0,0 +1,3 @@
1
+ # `config` — daemon TUI widget
2
+
3
+ Interactive `ide.yml` editor (config tree TUI). See [ARCHITECTURE.md §3](../../../../../ARCHITECTURE.md#§3--package-map) and the [`config` row in docs/widget-index.md](../../../../../docs/widget-index.md#daemon-tui-widgets-8).