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,481 @@
1
+ import "@opentui/solid/runtime-plugin-support";
2
+ import { parseArgs } from "node:util";
3
+ import { render, useKeyboard, useTerminalDimensions } from "@opentui/solid";
4
+ import { RGBA, TextAttributes } from "@opentui/core";
5
+ import { createSignal, createMemo, Show } from "solid-js";
6
+ import { createTheme, type RGBA as RGBAType, type WidgetTheme } from "../lib/theme.ts";
7
+ import { getAppConfig } from "../../lib/app-config.ts";
8
+ import {
9
+ flattenConfigTree,
10
+ updateConfigAtPath,
11
+ addPane,
12
+ removePane,
13
+ addRow,
14
+ removeRow,
15
+ validateSetupConfig,
16
+ type TreeNode,
17
+ } from "../lib/config-model.ts";
18
+ import { readConfig, writeConfig } from "../../lib/yaml-io.ts";
19
+ import { hasSession } from "@tmux-ide/tmux-bridge";
20
+ import type { IdeConfig } from "../../schemas/ide-config.ts";
21
+ import { ConfigTree } from "../setup/config-tree.tsx";
22
+ import { FieldEditor, type FieldType } from "../setup/field-editor.tsx";
23
+ import { matchGrammar } from "../lib/grammar.ts";
24
+ import { HelpOverlay, type WidgetKey } from "../lib/help-overlay.tsx";
25
+
26
+ /** Settings keys beyond the shared grammar — listed in the `?` overlay. The
27
+ * layout tree adds its own j/k/enter (they follow the grammar). */
28
+ const WIDGET_KEYS: WidgetKey[] = [
29
+ { key: "tab / 1–4", label: "switch tab" },
30
+ { key: "a", label: "add pane (layout)" },
31
+ { key: "d", label: "delete row/pane (layout)" },
32
+ { key: "^S", label: "save" },
33
+ { key: "^R", label: "save & restart" },
34
+ ];
35
+
36
+ const { values } = parseArgs({
37
+ options: {
38
+ session: { type: "string" },
39
+ dir: { type: "string" },
40
+ theme: { type: "string" },
41
+ },
42
+ });
43
+
44
+ const dir = values.dir ?? process.cwd();
45
+ const themeConfig = values.theme ? JSON.parse(values.theme) : undefined;
46
+
47
+ function toRGBA(c: RGBAType): RGBA {
48
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
49
+ }
50
+
51
+ type Tab = "layout" | "team" | "orchestrator" | "theme";
52
+ const TABS: { id: Tab; label: string }[] = [
53
+ { id: "layout", label: "Layout" },
54
+ { id: "team", label: "Team" },
55
+ { id: "orchestrator", label: "Orch." },
56
+ { id: "theme", label: "Theme" },
57
+ ];
58
+
59
+ function inferFieldType(path: string[]): { type: FieldType; enumValues?: string[] } {
60
+ const last = path[path.length - 1] ?? "";
61
+
62
+ // Boolean fields
63
+ if (["focus", "enabled", "auto_dispatch", "widgets"].includes(last)) {
64
+ return { type: "boolean" };
65
+ }
66
+
67
+ // Enum fields
68
+ if (last === "role") return { type: "enum", enumValues: ["lead", "teammate", "planner"] };
69
+ if (last === "type")
70
+ return {
71
+ type: "enum",
72
+ enumValues: ["explorer", "changes", "preview", "tasks", "costs", "config", "mission-control"],
73
+ };
74
+ if (last === "dispatch_mode") return { type: "enum", enumValues: ["tasks", "goals"] };
75
+
76
+ // Size fields
77
+ if (last === "size") return { type: "size" };
78
+
79
+ // Number fields rendered as string (will be coerced)
80
+ return { type: "string" };
81
+ }
82
+
83
+ function coerceValue(raw: unknown, path: string[]): unknown {
84
+ if (typeof raw !== "string") return raw;
85
+ if (raw === "true") return true;
86
+ if (raw === "false") return false;
87
+ const last = path[path.length - 1] ?? "";
88
+ if (["stall_timeout", "poll_interval", "max_concurrent_agents", "priority"].includes(last)) {
89
+ const num = parseInt(raw, 10);
90
+ if (!isNaN(num)) return num;
91
+ }
92
+ return raw;
93
+ }
94
+
95
+ function getValueAtPath(config: IdeConfig, path: string[]): string {
96
+ let current: unknown = config;
97
+ for (const key of path) {
98
+ if (current === null || current === undefined) return "";
99
+ if (typeof current === "object") {
100
+ current = (current as Record<string, unknown>)[key];
101
+ } else {
102
+ return "";
103
+ }
104
+ }
105
+ return current === null || current === undefined ? "" : String(current);
106
+ }
107
+
108
+ render(
109
+ () => {
110
+ const theme = createTheme(themeConfig, getAppConfig().theme);
111
+ const dimensions = useTerminalDimensions();
112
+
113
+ // Load config
114
+ let initialConfig: IdeConfig;
115
+ try {
116
+ const result = readConfig(dir);
117
+ initialConfig = result.config;
118
+ } catch {
119
+ console.error("No ide.yml found. Run 'tmux-ide init' first.");
120
+ process.exit(1);
121
+ }
122
+
123
+ const [config, setConfig] = createSignal<IdeConfig>(initialConfig);
124
+ const [savedConfig, setSavedConfig] = createSignal(JSON.stringify(initialConfig));
125
+ const [activeTab, setActiveTab] = createSignal<Tab>("layout");
126
+ const [editingField, setEditingField] = createSignal<string[] | null>(null);
127
+ const [statusMsg, setStatusMsg] = createSignal<string | null>(null);
128
+ const [helpOpen, setHelpOpen] = createSignal(false);
129
+
130
+ const dirty = createMemo(() => JSON.stringify(config()) !== savedConfig());
131
+ const validation = createMemo(() => validateSetupConfig(config()));
132
+
133
+ function handleSave() {
134
+ writeConfig(dir, config());
135
+ setSavedConfig(JSON.stringify(config()));
136
+ setStatusMsg("Saved");
137
+ setTimeout(() => setStatusMsg(null), 2000);
138
+ }
139
+
140
+ function handleEditField(path: string[]) {
141
+ setEditingField(path);
142
+ }
143
+
144
+ function handleFieldSave(path: string[], value: unknown) {
145
+ const coerced = coerceValue(value, path);
146
+ setConfig(updateConfigAtPath(config(), path, coerced));
147
+ setEditingField(null);
148
+ }
149
+
150
+ function handleAddPane(rowIdx: number) {
151
+ setConfig(addPane(config(), rowIdx));
152
+ }
153
+
154
+ function handleDelete(path: string[]) {
155
+ // Handle row deletion
156
+ if (path.length === 2 && path[0] === "rows" && /^\d+$/.test(path[1]!)) {
157
+ setConfig(removeRow(config(), parseInt(path[1]!, 10)));
158
+ return;
159
+ }
160
+ // Handle pane deletion
161
+ if (path.length >= 4 && path[0] === "rows" && path[2] === "panes") {
162
+ const rowIdx = parseInt(path[1]!, 10);
163
+ const paneIdx = parseInt(path[3]!, 10);
164
+ if (!isNaN(rowIdx) && !isNaN(paneIdx)) {
165
+ setConfig(removePane(config(), rowIdx, paneIdx));
166
+ }
167
+ }
168
+ }
169
+
170
+ function handleConfigChange(newConfig: IdeConfig) {
171
+ setConfig(newConfig);
172
+ }
173
+
174
+ // Tab-level keyboard handling (when not editing a field). The layout tree
175
+ // (ConfigTree) owns j/k/enter itself — those already follow the grammar.
176
+ useKeyboard((evt) => {
177
+ if (editingField() !== null) return; // Let field editor handle keys
178
+
179
+ // Help overlay swallows keys: esc / q / ? close it.
180
+ if (helpOpen()) {
181
+ const g = matchGrammar(evt);
182
+ if (g === "dismiss" || g === "quit" || g === "help") setHelpOpen(false);
183
+ evt.preventDefault();
184
+ return;
185
+ }
186
+
187
+ // The shared grammar runs FIRST for help + quit; the dirty guard still
188
+ // gates a lossy close.
189
+ const grammar = matchGrammar(evt);
190
+ if (grammar === "help") {
191
+ setHelpOpen(true);
192
+ evt.preventDefault();
193
+ return;
194
+ } else if (grammar === "dismiss" || grammar === "quit") {
195
+ // esc/q close the panel popup; the dirty guard blocks a lossy close
196
+ // (the `-E` popup exits when this process does).
197
+ if (dirty()) {
198
+ setStatusMsg("Unsaved changes! Ctrl+S to save, then q to quit.");
199
+ setTimeout(() => setStatusMsg(null), 3000);
200
+ } else {
201
+ process.exit(0);
202
+ }
203
+ evt.preventDefault();
204
+ return;
205
+ }
206
+
207
+ if (evt.name === "tab") {
208
+ const tabs = TABS.map((t) => t.id);
209
+ const idx = tabs.indexOf(activeTab());
210
+ setActiveTab(tabs[(idx + 1) % tabs.length]!);
211
+ evt.preventDefault();
212
+ } else if (evt.ctrl && evt.name === "s") {
213
+ handleSave();
214
+ evt.preventDefault();
215
+ } else if (evt.ctrl && evt.name === "r") {
216
+ handleSave();
217
+ const sessionName = config().name ?? dir.split("/").pop() ?? "ide";
218
+ if (hasSession(sessionName)) {
219
+ const { execFileSync } = require("node:child_process");
220
+ try {
221
+ execFileSync("tmux-ide", ["restart"], { cwd: dir, stdio: "inherit" });
222
+ } catch {
223
+ /* restart may close our pane */
224
+ }
225
+ }
226
+ process.exit(0);
227
+ } else if (evt.name === "1") {
228
+ setActiveTab("layout");
229
+ evt.preventDefault();
230
+ } else if (evt.name === "2") {
231
+ setActiveTab("team");
232
+ evt.preventDefault();
233
+ } else if (evt.name === "3") {
234
+ setActiveTab("orchestrator");
235
+ evt.preventDefault();
236
+ } else if (evt.name === "4") {
237
+ setActiveTab("theme");
238
+ evt.preventDefault();
239
+ }
240
+ });
241
+
242
+ // Team panel: simple form for team config
243
+ function TeamPanel() {
244
+ const cfg = config();
245
+ const teamEnabled = !!cfg.team;
246
+ const teamName = cfg.team?.name ?? "";
247
+
248
+ const panes: { row: number; pane: number; title: string; role: string | undefined }[] = [];
249
+ cfg.rows.forEach((row, ri) => {
250
+ row.panes.forEach((p, pi) => {
251
+ panes.push({ row: ri, pane: pi, title: p.title ?? `Row ${ri} Pane ${pi}`, role: p.role });
252
+ });
253
+ });
254
+
255
+ return (
256
+ <box paddingLeft={1}>
257
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
258
+ Team Configuration
259
+ </text>
260
+ <box paddingTop={1}>
261
+ <text fg={toRGBA(theme.fg)}>
262
+ Team: {teamEnabled ? `enabled (${teamName})` : "disabled"}
263
+ </text>
264
+ </box>
265
+ <box paddingTop={1}>
266
+ <text fg={toRGBA(theme.fgMuted)}>
267
+ {teamEnabled
268
+ ? "Use 'tmux-ide config disable-team' to disable"
269
+ : "Use 'tmux-ide config enable-team --name <N>' to enable"}
270
+ </text>
271
+ </box>
272
+ <box paddingTop={1}>
273
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
274
+ Pane Roles
275
+ </text>
276
+ </box>
277
+ {panes.map((p) => (
278
+ <box paddingLeft={1}>
279
+ <text fg={toRGBA(theme.fg)}>
280
+ {p.title}: {p.role ?? "none"}
281
+ </text>
282
+ </box>
283
+ ))}
284
+ </box>
285
+ );
286
+ }
287
+
288
+ // Orchestrator panel: display orchestrator settings
289
+ function OrchestratorPanel() {
290
+ const cfg = config();
291
+ const orch = cfg.orchestrator;
292
+ const fields: [string, string][] = [
293
+ ["enabled", String(orch?.enabled ?? false)],
294
+ ["auto_dispatch", String(orch?.auto_dispatch ?? true)],
295
+ ["dispatch_mode", orch?.dispatch_mode ?? "tasks"],
296
+ ["poll_interval", String(orch?.poll_interval ?? 5000)],
297
+ ["stall_timeout", String(orch?.stall_timeout ?? 300000)],
298
+ ["max_concurrent_agents", String(orch?.max_concurrent_agents ?? 10)],
299
+ ["widgets", String(orch?.widgets ?? false)],
300
+ ];
301
+
302
+ return (
303
+ <box paddingLeft={1}>
304
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
305
+ Orchestrator Settings
306
+ </text>
307
+ <box paddingTop={1}>
308
+ {fields.map(([key, val]) => (
309
+ <box flexDirection="row" gap={1}>
310
+ <text fg={toRGBA(theme.fgMuted)}>{key!.padEnd(24)}</text>
311
+ <text fg={toRGBA(theme.fg)}>{val}</text>
312
+ </box>
313
+ ))}
314
+ </box>
315
+ <box paddingTop={1}>
316
+ <text fg={toRGBA(theme.fgMuted)}>
317
+ Edit via Layout tab or: tmux-ide config set orchestrator.enabled true
318
+ </text>
319
+ </box>
320
+ </box>
321
+ );
322
+ }
323
+
324
+ // Theme panel: display theme settings
325
+ function ThemePanel() {
326
+ const cfg = config();
327
+ const t = cfg.theme;
328
+ const fields: [string, string][] = [
329
+ ["accent", t?.accent ?? "(default)"],
330
+ ["border", t?.border ?? "(default)"],
331
+ ["bg", t?.bg ?? "(default)"],
332
+ ["fg", t?.fg ?? "(default)"],
333
+ ];
334
+
335
+ return (
336
+ <box paddingLeft={1}>
337
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
338
+ Theme Colors
339
+ </text>
340
+ <box paddingTop={1}>
341
+ {fields.map(([key, val]) => (
342
+ <box flexDirection="row" gap={1}>
343
+ <text fg={toRGBA(theme.fgMuted)}>{key!.padEnd(12)}</text>
344
+ <text fg={toRGBA(theme.fg)}>{val}</text>
345
+ </box>
346
+ ))}
347
+ </box>
348
+ <box paddingTop={1}>
349
+ <text fg={toRGBA(theme.fgMuted)}>
350
+ Edit via Layout tab or: tmux-ide config set theme.accent colour75
351
+ </text>
352
+ </box>
353
+ </box>
354
+ );
355
+ }
356
+
357
+ return (
358
+ <box
359
+ width={dimensions().width}
360
+ height={dimensions().height}
361
+ backgroundColor={toRGBA(theme.bg)}
362
+ flexDirection="column"
363
+ >
364
+ <Show when={helpOpen()}>
365
+ <HelpOverlay theme={theme} title="settings" widgetKeys={WIDGET_KEYS} />
366
+ </Show>
367
+ <Show when={!helpOpen()}>
368
+ {/* Title bar */}
369
+ <box
370
+ flexShrink={0}
371
+ flexDirection="row"
372
+ justifyContent="space-between"
373
+ paddingLeft={1}
374
+ paddingRight={1}
375
+ >
376
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
377
+ Settings
378
+ </text>
379
+ <box flexDirection="row" gap={2}>
380
+ {dirty() ? <text fg={toRGBA(theme.gitModified)}>unsaved</text> : null}
381
+ <text fg={toRGBA(theme.fgMuted)}>Ctrl+S:save</text>
382
+ </box>
383
+ </box>
384
+
385
+ {/* Main content */}
386
+ <box flexGrow={1} flexDirection="row">
387
+ {/* Tab sidebar */}
388
+ <box flexShrink={0} width={10} paddingLeft={1}>
389
+ {TABS.map((tab) => {
390
+ const isActive = () => activeTab() === tab.id;
391
+ return (
392
+ <box
393
+ flexShrink={0}
394
+ backgroundColor={isActive() ? toRGBA(theme.selected) : undefined}
395
+ onMouseUp={() => setActiveTab(tab.id)}
396
+ >
397
+ <text
398
+ fg={toRGBA(isActive() ? theme.accent : theme.fgMuted)}
399
+ attributes={isActive() ? TextAttributes.BOLD : 0}
400
+ >
401
+ {isActive() ? ">" : " "} {tab.label}
402
+ </text>
403
+ </box>
404
+ );
405
+ })}
406
+ </box>
407
+
408
+ {/* Content area */}
409
+ <box flexGrow={1}>
410
+ <Show
411
+ when={editingField() !== null}
412
+ fallback={
413
+ <>
414
+ <Show when={activeTab() === "layout"}>
415
+ <ConfigTree
416
+ config={config()}
417
+ onEditField={handleEditField}
418
+ onAddPane={handleAddPane}
419
+ onDelete={handleDelete}
420
+ onSave={handleSave}
421
+ onConfigChange={handleConfigChange}
422
+ theme={theme}
423
+ />
424
+ </Show>
425
+ <Show when={activeTab() === "team"}>
426
+ <TeamPanel />
427
+ </Show>
428
+ <Show when={activeTab() === "orchestrator"}>
429
+ <OrchestratorPanel />
430
+ </Show>
431
+ <Show when={activeTab() === "theme"}>
432
+ <ThemePanel />
433
+ </Show>
434
+ </>
435
+ }
436
+ >
437
+ <FieldEditor
438
+ path={editingField()!}
439
+ value={getValueAtPath(config(), editingField()!)}
440
+ fieldType={inferFieldType(editingField()!).type}
441
+ enumValues={inferFieldType(editingField()!).enumValues}
442
+ onSave={handleFieldSave}
443
+ onCancel={() => setEditingField(null)}
444
+ theme={theme}
445
+ />
446
+ </Show>
447
+ </box>
448
+ </box>
449
+
450
+ {/* Status bar */}
451
+ <box flexShrink={0} paddingLeft={1} paddingRight={1}>
452
+ <box flexShrink={0} height={1}>
453
+ <text fg={toRGBA(theme.border)} wrapMode="none">
454
+ {"─".repeat(Math.max(1, dimensions().width - 2))}
455
+ </text>
456
+ </box>
457
+ <box flexDirection="row" gap={2}>
458
+ {validation().valid ? (
459
+ <text fg={toRGBA(theme.gitAdded)}>Valid</text>
460
+ ) : (
461
+ <text fg={toRGBA(theme.gitDeleted)}>Invalid</text>
462
+ )}
463
+ {statusMsg() ? <text fg={toRGBA(theme.accent)}>{statusMsg()}</text> : null}
464
+ <text fg={toRGBA(theme.fgMuted)}>Tab:switch</text>
465
+ <text fg={toRGBA(theme.fgMuted)}>1-4:tabs</text>
466
+ <text fg={toRGBA(theme.fgMuted)}>Ctrl+R:restart</text>
467
+ <text fg={toRGBA(theme.fgMuted)}>?:help</text>
468
+ <text fg={toRGBA(theme.fgMuted)}>q:quit</text>
469
+ </box>
470
+ </box>
471
+ </Show>
472
+ </box>
473
+ );
474
+ },
475
+ {
476
+ targetFps: 30,
477
+ exitOnCtrlC: true,
478
+ useKittyKeyboard: {},
479
+ autoFocus: false,
480
+ },
481
+ );
@@ -0,0 +1,3 @@
1
+ # `explorer` — daemon TUI widget
2
+
3
+ File tree navigator. See [ARCHITECTURE.md §3](../../../../../ARCHITECTURE.md#§3--package-map) and the [`explorer` row in docs/widget-index.md](../../../../../docs/widget-index.md#daemon-tui-widgets-8).
@@ -0,0 +1,77 @@
1
+ import { For, Show, createMemo } from "solid-js";
2
+ import { RGBA, TextAttributes } from "@opentui/core";
3
+ import { relative, basename, join } from "node:path";
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
+ interface BreadcrumbsProps {
11
+ projectRoot: string;
12
+ currentDir: string;
13
+ branch: string | null;
14
+ theme: WidgetTheme;
15
+ onNavigate: (absolutePath: string) => void;
16
+ }
17
+
18
+ interface BreadcrumbSegment {
19
+ name: string;
20
+ absolutePath: string;
21
+ isLast: boolean;
22
+ }
23
+
24
+ export function Breadcrumbs(props: BreadcrumbsProps) {
25
+ const segments = createMemo(() => {
26
+ const rel = relative(props.projectRoot, props.currentDir);
27
+ const parts = rel ? rel.split("/") : [];
28
+ const result: BreadcrumbSegment[] = [
29
+ {
30
+ name: basename(props.projectRoot),
31
+ absolutePath: props.projectRoot,
32
+ isLast: parts.length === 0,
33
+ },
34
+ ];
35
+ let currentPath = props.projectRoot;
36
+ for (let i = 0; i < parts.length; i++) {
37
+ currentPath = join(currentPath, parts[i]!);
38
+ result.push({
39
+ name: parts[i]!,
40
+ absolutePath: currentPath,
41
+ isLast: i === parts.length - 1,
42
+ });
43
+ }
44
+ return result;
45
+ });
46
+
47
+ return (
48
+ <box flexShrink={0} paddingLeft={1} paddingBottom={1} flexDirection="row" gap={0}>
49
+ <Show when={props.branch}>
50
+ <text fg={toRGBA(props.theme.accent)} attributes={TextAttributes.BOLD}>
51
+ {"⎇ "}
52
+ {props.branch}{" "}
53
+ </text>
54
+ </Show>
55
+ <For each={segments()}>
56
+ {(seg) => (
57
+ <box flexDirection="row">
58
+ <text
59
+ fg={seg.isLast ? toRGBA(props.theme.fg) : toRGBA(props.theme.fgMuted)}
60
+ attributes={seg.isLast ? TextAttributes.BOLD : undefined}
61
+ onMouseUp={() => {
62
+ if (!seg.isLast) {
63
+ props.onNavigate(seg.absolutePath);
64
+ }
65
+ }}
66
+ >
67
+ {seg.name}
68
+ </text>
69
+ <Show when={!seg.isLast}>
70
+ <text fg={toRGBA(props.theme.border)}>{" / "}</text>
71
+ </Show>
72
+ </box>
73
+ )}
74
+ </For>
75
+ </box>
76
+ );
77
+ }
@@ -0,0 +1,20 @@
1
+ import { RGBA } from "@opentui/core";
2
+ import type { WidgetTheme } from "../lib/theme.ts";
3
+
4
+ function toRGBA(c: { r: number; g: number; b: number; a: number }): RGBA {
5
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
6
+ }
7
+
8
+ interface FooterProps {
9
+ theme: WidgetTheme;
10
+ }
11
+
12
+ export function Footer(props: FooterProps) {
13
+ return (
14
+ <box flexShrink={0} paddingLeft={1} paddingTop={1}>
15
+ <text fg={toRGBA(props.theme.fgMuted)} wrapMode="none">
16
+ ↑↓:nav ⏎:enter ⌫:back /:search []:changes c:claude o:edit q:quit
17
+ </text>
18
+ </box>
19
+ );
20
+ }
@@ -0,0 +1,23 @@
1
+ import { RGBA, TextAttributes } from "@opentui/core";
2
+ import type { WidgetTheme } from "../lib/theme.ts";
3
+
4
+ function toRGBA(c: { r: number; g: number; b: number; a: number }): RGBA {
5
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
6
+ }
7
+
8
+ interface HeaderProps {
9
+ branch: string | null;
10
+ fileCount: number;
11
+ theme: WidgetTheme;
12
+ }
13
+
14
+ export function Header(props: HeaderProps) {
15
+ return (
16
+ <box flexShrink={0} flexDirection="row" gap={1} paddingLeft={1} paddingBottom={1}>
17
+ <text fg={toRGBA(props.theme.accent)} attributes={TextAttributes.BOLD}>
18
+ {props.branch ? `⎇ ${props.branch}` : "FILES"}
19
+ </text>
20
+ <text fg={toRGBA(props.theme.fgMuted)}>{props.fileCount}</text>
21
+ </box>
22
+ );
23
+ }