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,165 @@
1
+ import { createMemo, createEffect, For, Show } from "solid-js";
2
+ import { RGBA, type ScrollBoxRenderable } from "@opentui/core";
3
+ import type { TreeNode } from "./tree-model.ts";
4
+ import type { WidgetTheme } from "../lib/theme.ts";
5
+
6
+ function toRGBA(c: { r: number; g: number; b: number; a: number }): RGBA {
7
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
8
+ }
9
+
10
+ const TRANSPARENT = RGBA.fromInts(0, 0, 0, 0);
11
+
12
+ function getStatusLabel(status: string): string {
13
+ switch (status) {
14
+ case "M":
15
+ return " M";
16
+ case "A":
17
+ return " A";
18
+ case "D":
19
+ return " D";
20
+ case "?":
21
+ return " ?";
22
+ default:
23
+ return "";
24
+ }
25
+ }
26
+
27
+ function getStatusColor(
28
+ status: string,
29
+ theme: WidgetTheme,
30
+ ): { r: number; g: number; b: number; a: number } {
31
+ switch (status) {
32
+ case "M":
33
+ return theme.gitModified;
34
+ case "A":
35
+ return theme.gitAdded;
36
+ case "D":
37
+ return theme.gitDeleted;
38
+ case "?":
39
+ return theme.gitUntracked;
40
+ default:
41
+ return theme.fgMuted;
42
+ }
43
+ }
44
+
45
+ function getNameColor(
46
+ name: string,
47
+ isDir: boolean,
48
+ isSelected: boolean,
49
+ isIgnored: boolean,
50
+ theme: WidgetTheme,
51
+ ): { r: number; g: number; b: number; a: number } {
52
+ if (isSelected) return theme.selectedText;
53
+ if (isIgnored) return theme.ignored;
54
+ if (isDir) return theme.dirName;
55
+ if (name.endsWith(".lock") || name.startsWith(".") || name === "LICENSE") {
56
+ return theme.fgMuted;
57
+ }
58
+ return theme.fg;
59
+ }
60
+
61
+ interface FileTreeProps {
62
+ nodes: TreeNode[];
63
+ selected: number;
64
+ theme: WidgetTheme;
65
+ inputMode: "keyboard" | "mouse";
66
+ onSelect: (index: number) => void;
67
+ onActivate: (node: TreeNode) => void;
68
+ onInputModeChange: (mode: "keyboard" | "mouse") => void;
69
+ }
70
+
71
+ export function FileTree(props: FileTreeProps) {
72
+ let scroll: ScrollBoxRenderable | undefined;
73
+
74
+ createEffect(() => {
75
+ const idx = props.selected;
76
+ if (!scroll) return;
77
+ const children = scroll.getChildren();
78
+ const target = children[idx];
79
+ if (!target) return;
80
+ const y = target.y - scroll.y;
81
+ if (y >= scroll.height) scroll.scrollBy(y - scroll.height + 1);
82
+ if (y < 0) scroll.scrollBy(y);
83
+ });
84
+
85
+ return (
86
+ <scrollbox
87
+ ref={(r: ScrollBoxRenderable) => (scroll = r)}
88
+ flexGrow={1}
89
+ verticalScrollbarOptions={{
90
+ trackOptions: {
91
+ backgroundColor: toRGBA(props.theme.bg),
92
+ foregroundColor: toRGBA(props.theme.accent),
93
+ },
94
+ }}
95
+ >
96
+ <Show
97
+ when={props.nodes.length > 0}
98
+ fallback={
99
+ <box paddingLeft={2} paddingTop={1}>
100
+ <text fg={toRGBA(props.theme.fgMuted)}>empty directory</text>
101
+ </box>
102
+ }
103
+ >
104
+ <For each={props.nodes}>
105
+ {(node, index) => {
106
+ const isSelected = createMemo(() => index() === props.selected);
107
+ const icon = node.entry.isDir ? "> " : " ";
108
+ const nameColor = () =>
109
+ getNameColor(
110
+ node.entry.name,
111
+ node.entry.isDir,
112
+ isSelected(),
113
+ node.entry.ignored,
114
+ props.theme,
115
+ );
116
+ const rowBg = () =>
117
+ isSelected()
118
+ ? toRGBA(props.theme.selected)
119
+ : index() % 2 === 1
120
+ ? toRGBA(props.theme.rowAlt)
121
+ : TRANSPARENT;
122
+
123
+ return (
124
+ <box
125
+ id={String(index())}
126
+ backgroundColor={rowBg()}
127
+ flexDirection="row"
128
+ paddingLeft={1}
129
+ paddingRight={1}
130
+ onMouseMove={() => {
131
+ props.onInputModeChange("mouse");
132
+ props.onSelect(index());
133
+ }}
134
+ onMouseDown={() => props.onSelect(index())}
135
+ onMouseUp={() => {
136
+ props.onActivate(node);
137
+ }}
138
+ >
139
+ <text fg={toRGBA(nameColor())} wrapMode="none" flexGrow={1}>
140
+ {icon}
141
+ {node.entry.name}
142
+ {node.entry.isDir ? "/" : ""}
143
+ </text>
144
+ <Show when={node.gitStatus}>
145
+ <text
146
+ fg={toRGBA(getStatusColor(node.gitStatus!, props.theme))}
147
+ flexShrink={0}
148
+ wrapMode="none"
149
+ >
150
+ {getStatusLabel(node.gitStatus!)}
151
+ </text>
152
+ </Show>
153
+ <Show when={isSelected() && !node.entry.isDir}>
154
+ <text fg={toRGBA(props.theme.fgMuted)} flexShrink={0} wrapMode="none">
155
+ {" c: send to claude code"}
156
+ </text>
157
+ </Show>
158
+ </box>
159
+ );
160
+ }}
161
+ </For>
162
+ </Show>
163
+ </scrollbox>
164
+ );
165
+ }
@@ -0,0 +1,116 @@
1
+ import { IdeConfigSchema, type IdeConfig, type Row } from "../../schemas/ide-config.ts";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Config Tree
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export interface TreeNode {
8
+ path: string[];
9
+ label: string;
10
+ value: string | null;
11
+ depth: number;
12
+ expandable: boolean;
13
+ }
14
+
15
+ function flattenValue(obj: unknown, path: string[], depth: number, nodes: TreeNode[]): void {
16
+ if (Array.isArray(obj)) {
17
+ const label = path[path.length - 1] ?? "";
18
+ nodes.push({ path: [...path], label, value: null, depth, expandable: true });
19
+ for (let i = 0; i < obj.length; i++) {
20
+ flattenValue(obj[i], [...path, String(i)], depth + 1, nodes);
21
+ }
22
+ } else if (obj !== null && typeof obj === "object") {
23
+ const label = path[path.length - 1] ?? "";
24
+ nodes.push({ path: [...path], label, value: null, depth, expandable: true });
25
+ for (const [key, val] of Object.entries(obj as Record<string, unknown>)) {
26
+ flattenValue(val, [...path, key], depth + 1, nodes);
27
+ }
28
+ } else {
29
+ const label = path[path.length - 1] ?? "";
30
+ nodes.push({
31
+ path: [...path],
32
+ label,
33
+ value: obj === undefined || obj === null ? null : String(obj),
34
+ depth,
35
+ expandable: false,
36
+ });
37
+ }
38
+ }
39
+
40
+ export function flattenConfigTree(config: IdeConfig): TreeNode[] {
41
+ const nodes: TreeNode[] = [];
42
+ for (const [key, val] of Object.entries(config)) {
43
+ if (val === undefined) continue;
44
+ flattenValue(val, [key], 0, nodes);
45
+ }
46
+ return nodes;
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Config Mutations
51
+ // ---------------------------------------------------------------------------
52
+
53
+ export function deepClone<T>(obj: T): T {
54
+ return JSON.parse(JSON.stringify(obj)) as T;
55
+ }
56
+
57
+ export function updateConfigAtPath(config: IdeConfig, path: string[], value: unknown): IdeConfig {
58
+ const cloned = deepClone(config);
59
+ let current: Record<string, unknown> = cloned as unknown as Record<string, unknown>;
60
+ for (let i = 0; i < path.length - 1; i++) {
61
+ const key = path[i]!;
62
+ current = current[key] as Record<string, unknown>;
63
+ }
64
+ const lastKey = path[path.length - 1]!;
65
+ current[lastKey] = value;
66
+ return cloned;
67
+ }
68
+
69
+ export function addPane(config: IdeConfig, rowIdx: number): IdeConfig {
70
+ const cloned = deepClone(config);
71
+ const row = cloned.rows[rowIdx];
72
+ if (!row) return cloned;
73
+ row.panes.push({ title: "New Pane" });
74
+ return cloned;
75
+ }
76
+
77
+ export function removePane(config: IdeConfig, rowIdx: number, paneIdx: number): IdeConfig {
78
+ const cloned = deepClone(config);
79
+ const row = cloned.rows[rowIdx];
80
+ if (!row) return cloned;
81
+ if (row.panes.length <= 1) return cloned;
82
+ row.panes.splice(paneIdx, 1);
83
+ return cloned;
84
+ }
85
+
86
+ export function addRow(config: IdeConfig, size?: string): IdeConfig {
87
+ const cloned = deepClone(config);
88
+ const row: Row = { panes: [{ title: "Shell" }] };
89
+ if (size) row.size = size;
90
+ cloned.rows.push(row);
91
+ return cloned;
92
+ }
93
+
94
+ export function removeRow(config: IdeConfig, rowIdx: number): IdeConfig {
95
+ const cloned = deepClone(config);
96
+ if (cloned.rows.length <= 1) return cloned;
97
+ cloned.rows.splice(rowIdx, 1);
98
+ return cloned;
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Validation
103
+ // ---------------------------------------------------------------------------
104
+
105
+ export function validateSetupConfig(
106
+ config: unknown,
107
+ ): { valid: true; config: IdeConfig } | { valid: false; errors: string[] } {
108
+ const result = IdeConfigSchema.safeParse(config);
109
+ if (result.success) {
110
+ return { valid: true, config: result.data };
111
+ }
112
+ return {
113
+ valid: false,
114
+ errors: result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`),
115
+ };
116
+ }
@@ -0,0 +1,88 @@
1
+ import { readdirSync, readFileSync, existsSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import ignore, { type Ignore } from "ignore";
4
+
5
+ const ALWAYS_IGNORE = new Set([
6
+ "node_modules",
7
+ ".git",
8
+ ".svn",
9
+ ".hg",
10
+ "dist",
11
+ "build",
12
+ "out",
13
+ ".next",
14
+ ".turbo",
15
+ ".cache",
16
+ "__pycache__",
17
+ "coverage",
18
+ ".nyc_output",
19
+ "target",
20
+ "vendor",
21
+ "bower_components",
22
+ ]);
23
+
24
+ export { type Ignore };
25
+
26
+ export interface FileEntry {
27
+ name: string;
28
+ path: string; // relative to root
29
+ absolutePath: string;
30
+ isDir: boolean;
31
+ ignored: boolean; // gitignored
32
+ }
33
+
34
+ export function createIgnoreFilter(rootDir: string): Ignore {
35
+ const ig = ignore();
36
+ const gitignorePath = join(rootDir, ".gitignore");
37
+ if (existsSync(gitignorePath)) {
38
+ ig.add(readFileSync(gitignorePath, "utf-8"));
39
+ }
40
+ return ig;
41
+ }
42
+
43
+ export function readDirectory(
44
+ dir: string,
45
+ rootDir: string,
46
+ ig: Ignore,
47
+ showHidden: boolean,
48
+ showIgnored: boolean = false,
49
+ ): FileEntry[] {
50
+ let entries;
51
+ try {
52
+ entries = readdirSync(dir, { withFileTypes: true });
53
+ } catch {
54
+ return [];
55
+ }
56
+ return entries
57
+ .filter((e) => {
58
+ if (ALWAYS_IGNORE.has(e.name)) return false;
59
+ if (!showHidden && e.name.startsWith(".")) return false;
60
+ if (showIgnored) return true;
61
+ const rel = relative(rootDir, join(dir, e.name));
62
+ try {
63
+ return !ig.ignores(e.isDirectory() ? rel + "/" : rel);
64
+ } catch {
65
+ return true;
66
+ }
67
+ })
68
+ .map((e) => {
69
+ const rel = relative(rootDir, join(dir, e.name));
70
+ let isIgnored = false;
71
+ try {
72
+ isIgnored = ig.ignores(e.isDirectory() ? rel + "/" : rel);
73
+ } catch {
74
+ // ignore malformed paths for ignore rules
75
+ }
76
+ return {
77
+ name: e.name,
78
+ path: rel,
79
+ absolutePath: join(dir, e.name),
80
+ isDir: e.isDirectory(),
81
+ ignored: isIgnored,
82
+ };
83
+ })
84
+ .sort((a, b) => {
85
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
86
+ return a.name.localeCompare(b.name);
87
+ });
88
+ }
@@ -0,0 +1,88 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ export interface GitFileStatus {
4
+ path: string;
5
+ status: "M" | "A" | "D" | "R" | "?";
6
+ additions: number;
7
+ deletions: number;
8
+ }
9
+
10
+ function execGit(dir: string, args: string[]): string {
11
+ try {
12
+ return execFileSync(
13
+ "git",
14
+ ["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", ...args],
15
+ { cwd: dir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
16
+ );
17
+ } catch {
18
+ return "";
19
+ }
20
+ }
21
+
22
+ export function getGitBranch(dir: string): string | null {
23
+ const result = execGit(dir, ["rev-parse", "--abbrev-ref", "HEAD"]).trim();
24
+ return result || null;
25
+ }
26
+
27
+ export function getGitFileStatuses(dir: string): GitFileStatus[] {
28
+ const results: GitFileStatus[] = [];
29
+
30
+ // 1. Identify deleted files first so we can tag them correctly in numstat
31
+ const deleted = execGit(dir, ["diff", "--name-only", "--diff-filter=D", "HEAD"]);
32
+ const deletedPaths = new Set(deleted.split("\n").filter(Boolean));
33
+
34
+ // 2. Modified/deleted files with line counts
35
+ const numstat = execGit(dir, ["diff", "--numstat", "HEAD"]);
36
+ for (const line of numstat.split("\n").filter(Boolean)) {
37
+ const parts = line.split("\t");
38
+ if (parts.length < 3) continue;
39
+ const [added, removed, ...pathParts] = parts;
40
+ const filepath = pathParts.join("\t");
41
+ if (added === "-") continue; // binary file
42
+ results.push({
43
+ path: filepath,
44
+ status: deletedPaths.has(filepath) ? "D" : "M",
45
+ additions: parseInt(added!, 10) || 0,
46
+ deletions: parseInt(removed!, 10) || 0,
47
+ });
48
+ }
49
+
50
+ // 3. Deleted files not in numstat (shouldn't happen, but be safe)
51
+ const numstatPaths = new Set(results.map((r) => r.path));
52
+ for (const filepath of deletedPaths) {
53
+ if (!numstatPaths.has(filepath)) {
54
+ results.push({ path: filepath, status: "D", additions: 0, deletions: 0 });
55
+ }
56
+ }
57
+
58
+ // 4. Untracked files
59
+ const untracked = execGit(dir, ["ls-files", "--others", "--exclude-standard"]);
60
+ for (const filepath of untracked.split("\n").filter(Boolean)) {
61
+ results.push({ path: filepath, status: "?", additions: 0, deletions: 0 });
62
+ }
63
+
64
+ return results;
65
+ }
66
+
67
+ export function getGitStatusMap(dir: string): Map<string, string> {
68
+ const map = new Map<string, string>();
69
+ for (const file of getGitFileStatuses(dir)) {
70
+ map.set(file.path, file.status);
71
+ // Propagate to parent directories
72
+ let parent = file.path;
73
+ while (parent.includes("/")) {
74
+ parent = parent.substring(0, parent.lastIndexOf("/"));
75
+ if (!map.has(parent)) map.set(parent, file.status);
76
+ }
77
+ }
78
+ return map;
79
+ }
80
+
81
+ export function getFileDiff(dir: string, path: string, staged: boolean): string {
82
+ const args = staged ? ["diff", "--cached", "--", path] : ["diff", "--", path];
83
+ return execGit(dir, args);
84
+ }
85
+
86
+ export function isGitRepo(dir: string): boolean {
87
+ return execGit(dir, ["rev-parse", "--is-inside-work-tree"]).trim() === "true";
88
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The ONE interaction grammar shared by every TUI surface — the sidebar,
3
+ * explorer, changes, config, and the team cockpit/picker.
4
+ *
5
+ * Cohesion is muscle memory: `j`/`k` (+ arrows) move, `enter` activates, `/`
6
+ * filters, `esc` closes the topmost thing (filter → detail → widget), `q`
7
+ * quits, `?` toggles help. These keys mean the SAME thing everywhere, so a
8
+ * widget never invents its own drift for a universal verb.
9
+ *
10
+ * Widgets call {@link matchGrammar} FIRST in their key handler and act on the
11
+ * returned {@link GrammarAction}, falling through to their own widget-specific
12
+ * keys only when it returns `null`. The escape precedence is factored into the
13
+ * pure {@link dismiss} state-machine so "esc closes the filter before it quits"
14
+ * is defined (and tested) in one place.
15
+ *
16
+ * This module is the SINGLE SOURCE for the grammar: the per-widget help
17
+ * overlays and the cheat sheet both render their "in panels & sidebar" section
18
+ * from {@link GRAMMAR_HELP}, so documentation can never drift from behaviour.
19
+ *
20
+ * Pure — no io, no solid, no opentui — so it unit-tests as a plain table.
21
+ */
22
+
23
+ /** The universal verbs every surface understands. */
24
+ export type GrammarAction =
25
+ | "navDown"
26
+ | "navUp"
27
+ | "activate"
28
+ | "filter"
29
+ | "help"
30
+ | "dismiss"
31
+ | "quit";
32
+
33
+ /** The subset of an @opentui key event the grammar needs. */
34
+ export interface GrammarKeyEvent {
35
+ name: string;
36
+ ctrl?: boolean;
37
+ alt?: boolean;
38
+ meta?: boolean;
39
+ shift?: boolean;
40
+ }
41
+
42
+ /**
43
+ * Deterministic iteration order for {@link matchGrammar}. The grammar keys are
44
+ * mutually exclusive (no key appears in two actions), so order only fixes the
45
+ * lookup for readers/tests — it never changes a result.
46
+ */
47
+ export const GRAMMAR_ACTION_ORDER: GrammarAction[] = [
48
+ "navDown",
49
+ "navUp",
50
+ "activate",
51
+ "filter",
52
+ "help",
53
+ "dismiss",
54
+ "quit",
55
+ ];
56
+
57
+ /**
58
+ * The fixed key → action bindings. Unlike the team app's configurable
59
+ * `DEFAULT_KEYMAP`, the GRAMMAR itself is not user-configurable — its whole
60
+ * value is that it is the same everywhere. The team keymap's universal actions
61
+ * (up/down/enter/filter/help/quit) agree with these by construction.
62
+ */
63
+ export const GRAMMAR_KEYS: Record<GrammarAction, string[]> = {
64
+ navDown: ["j", "down"],
65
+ navUp: ["k", "up"],
66
+ activate: ["return"],
67
+ filter: ["/"],
68
+ help: ["?"],
69
+ dismiss: ["escape"],
70
+ quit: ["q"],
71
+ };
72
+
73
+ /**
74
+ * Human-facing rows for the help overlays and the cheat sheet — rendered
75
+ * straight from this constant so the docs are sourced from the grammar itself.
76
+ */
77
+ export const GRAMMAR_HELP: ReadonlyArray<{ keys: string; label: string }> = [
78
+ { keys: "j / ↓", label: "move down" },
79
+ { keys: "k / ↑", label: "move up" },
80
+ { keys: "enter", label: "activate / open" },
81
+ { keys: "/", label: "filter list" },
82
+ { keys: "esc", label: "close filter → detail → widget" },
83
+ { keys: "q", label: "quit" },
84
+ { keys: "?", label: "toggle this help" },
85
+ ];
86
+
87
+ /**
88
+ * Map a key event to its {@link GrammarAction}, or `null` when the key is not
89
+ * part of the grammar (so the caller falls through to its widget-specific
90
+ * keys).
91
+ *
92
+ * `ctrl`/`alt`/`meta` combos are NEVER grammar — those namespaces belong to the
93
+ * widgets (`ctrl+s` save, `ctrl+c` quit, the `M-…` dock popups). `shift` is
94
+ * allowed through because `?` arrives as a shifted `/` on most layouts; no
95
+ * widget binds a `shift+<grammar key>` combo, so letting it pass is safe.
96
+ */
97
+ export function matchGrammar(evt: GrammarKeyEvent): GrammarAction | null {
98
+ if (evt.ctrl || evt.alt || evt.meta) return null;
99
+ for (const action of GRAMMAR_ACTION_ORDER) {
100
+ if (GRAMMAR_KEYS[action].includes(evt.name)) return action;
101
+ }
102
+ return null;
103
+ }
104
+
105
+ /** Which layer an `esc` (or `q`) should close, given what is currently open. */
106
+ export type DismissTarget = "filter" | "detail" | "widget";
107
+
108
+ /** The open-overlay layers a surface can stack, topmost-wins. */
109
+ export interface OverlayState {
110
+ /** A `/` filter prompt is open (typing narrows a list). */
111
+ filterOpen?: boolean;
112
+ /** A transient detail view is open (help overlay, field editor, preview). */
113
+ detailOpen?: boolean;
114
+ }
115
+
116
+ /**
117
+ * The escape precedence, as a pure function: esc closes the FILTER first, then
118
+ * an open DETAIL, and only when nothing is layered does it fall through to the
119
+ * WIDGET itself (quit / close the popup). Widgets call this to decide what a
120
+ * bare `esc` (grammar `dismiss`) should do without re-deriving the order.
121
+ */
122
+ export function dismiss(state: OverlayState): DismissTarget {
123
+ if (state.filterOpen) return "filter";
124
+ if (state.detailOpen) return "detail";
125
+ return "widget";
126
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The shared `?` help overlay — one box, every surface.
3
+ *
4
+ * Renders the UNIVERSAL interaction grammar (from {@link GRAMMAR_HELP}, the same
5
+ * constant the cheat sheet reads) above the caller's OWN widget-specific keys.
6
+ * Because the grammar rows come from `grammar.ts`, no surface can document
7
+ * `j`/`k`/`enter`/`/`/`esc`/`q`/`?` differently from how it behaves.
8
+ *
9
+ * Each surface keeps a `helpOpen` signal, routes `?` (grammar `help`) to toggle
10
+ * it and `esc`/`q` to close it, and renders `<HelpOverlay …>` in place of its
11
+ * body while open. Kept theme-driven and io-free so it drops into any widget.
12
+ */
13
+ import { For, Show } from "solid-js";
14
+ import { RGBA, TextAttributes } from "@opentui/core";
15
+ import type { WidgetTheme } from "./theme.ts";
16
+ import { GRAMMAR_HELP } from "./grammar.ts";
17
+
18
+ function toRGBA(c: { r: number; g: number; b: number; a: number }): RGBA {
19
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
20
+ }
21
+
22
+ /** A widget-specific key row: the rendered key(s) and what they do. */
23
+ export interface WidgetKey {
24
+ key: string;
25
+ label: string;
26
+ }
27
+
28
+ export interface HelpOverlayProps {
29
+ theme: WidgetTheme;
30
+ /** Surface name shown in the header (e.g. "explorer", "sidebar"). */
31
+ title: string;
32
+ /** The surface's own keys, listed under the shared grammar. */
33
+ widgetKeys: WidgetKey[];
34
+ }
35
+
36
+ /**
37
+ * A centered help card: a "grammar" section (shared verbs) then a section of
38
+ * the surface's own keys. Replaces the surface body while `?` is held open;
39
+ * `esc`/`q`/`?` close it.
40
+ */
41
+ export function HelpOverlay(props: HelpOverlayProps) {
42
+ const keyCol = () => {
43
+ const widths = [
44
+ ...GRAMMAR_HELP.map((r) => r.keys.length),
45
+ ...props.widgetKeys.map((r) => r.key.length),
46
+ ];
47
+ return Math.max(6, ...widths);
48
+ };
49
+ return (
50
+ <box flexDirection="column" flexGrow={1} alignItems="center" paddingTop={2}>
51
+ <box
52
+ flexDirection="column"
53
+ border
54
+ borderColor={toRGBA(props.theme.accent)}
55
+ backgroundColor={toRGBA(props.theme.selected)}
56
+ paddingLeft={2}
57
+ paddingRight={2}
58
+ paddingTop={1}
59
+ paddingBottom={1}
60
+ >
61
+ <text fg={toRGBA(props.theme.accent)} attributes={TextAttributes.BOLD}>
62
+ {props.title} — help
63
+ </text>
64
+
65
+ <box paddingTop={1}>
66
+ <text fg={toRGBA(props.theme.fgMuted)} attributes={TextAttributes.BOLD}>
67
+ grammar
68
+ </text>
69
+ </box>
70
+ <For each={GRAMMAR_HELP}>
71
+ {(row) => (
72
+ <box flexDirection="row" gap={1}>
73
+ <text fg={toRGBA(props.theme.accent)}>{row.keys.padEnd(keyCol())}</text>
74
+ <text fg={toRGBA(props.theme.fg)}>{row.label}</text>
75
+ </box>
76
+ )}
77
+ </For>
78
+
79
+ <Show when={props.widgetKeys.length > 0}>
80
+ <box paddingTop={1}>
81
+ <text fg={toRGBA(props.theme.fgMuted)} attributes={TextAttributes.BOLD}>
82
+ {props.title}
83
+ </text>
84
+ </box>
85
+ <For each={props.widgetKeys}>
86
+ {(row) => (
87
+ <box flexDirection="row" gap={1}>
88
+ <text fg={toRGBA(props.theme.accent)}>{row.key.padEnd(keyCol())}</text>
89
+ <text fg={toRGBA(props.theme.fg)}>{row.label}</text>
90
+ </box>
91
+ )}
92
+ </For>
93
+ </Show>
94
+
95
+ <box paddingTop={1}>
96
+ <text fg={toRGBA(props.theme.fgMuted)}>esc / q / ? to close</text>
97
+ </box>
98
+ </box>
99
+ </box>
100
+ );
101
+ }