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,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).