tmux-ide 1.3.1 → 2.0.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 (236) hide show
  1. package/bin/cli.js +4 -216
  2. package/bin/cli.ts +536 -0
  3. package/package.json +27 -14
  4. package/src/__tests__/support.ts +186 -0
  5. package/src/attach.ts +20 -0
  6. package/src/cli.test.ts +314 -0
  7. package/src/command-center/discovery.test.ts +265 -0
  8. package/src/command-center/discovery.ts +296 -0
  9. package/src/command-center/index.ts +31 -0
  10. package/src/command-center/schemas.ts +57 -0
  11. package/src/command-center/server.test.ts +818 -0
  12. package/src/command-center/server.ts +1426 -0
  13. package/src/config-cli.test.ts +86 -0
  14. package/src/config.test.ts +205 -0
  15. package/src/config.ts +313 -0
  16. package/src/detect.test.ts +142 -0
  17. package/src/detect.ts +248 -0
  18. package/src/dispatch.ts +102 -0
  19. package/src/doctor.test.ts +119 -0
  20. package/src/doctor.ts +167 -0
  21. package/src/init.test.ts +101 -0
  22. package/src/init.ts +144 -0
  23. package/src/inspect.test.ts +67 -0
  24. package/src/inspect.ts +212 -0
  25. package/src/integration.test.ts.skip +276 -0
  26. package/src/js-yaml.d.ts +10 -0
  27. package/src/launch.test.ts +222 -0
  28. package/src/launch.ts +494 -0
  29. package/src/lib/auth/auth-service.test.ts +178 -0
  30. package/src/lib/auth/auth-service.ts +227 -0
  31. package/src/lib/auth/middleware.test.ts +119 -0
  32. package/src/lib/auth/middleware.ts +56 -0
  33. package/src/lib/auth/types.ts +13 -0
  34. package/src/lib/authorship.test.ts +242 -0
  35. package/src/lib/authorship.ts +280 -0
  36. package/src/lib/cast/recorder.test.ts +127 -0
  37. package/src/lib/cast/recorder.ts +148 -0
  38. package/src/lib/daemon-watchdog.ts +102 -0
  39. package/src/lib/daemon.ts +417 -0
  40. package/src/lib/dot-path.test.ts +66 -0
  41. package/src/lib/dot-path.ts +17 -0
  42. package/src/lib/errors.ts +44 -0
  43. package/src/lib/event-log.test.ts +189 -0
  44. package/src/lib/event-log.ts +237 -0
  45. package/src/lib/github-pr.ts +182 -0
  46. package/src/lib/hq/client.test.ts +154 -0
  47. package/src/lib/hq/client.ts +142 -0
  48. package/src/lib/hq/mdns.test.ts +54 -0
  49. package/src/lib/hq/mdns.ts +88 -0
  50. package/src/lib/hq/registry.test.ts +237 -0
  51. package/src/lib/hq/registry.ts +145 -0
  52. package/src/lib/hq/types.ts +44 -0
  53. package/src/lib/ipc/socket-protocol.test.ts +126 -0
  54. package/src/lib/ipc/socket-protocol.ts +324 -0
  55. package/src/lib/launch-plan.test.ts +119 -0
  56. package/src/lib/launch-plan.ts +90 -0
  57. package/src/lib/log.test.ts +39 -0
  58. package/src/lib/log.ts +71 -0
  59. package/src/lib/metrics.test.ts +213 -0
  60. package/src/lib/metrics.ts +345 -0
  61. package/src/lib/orchestrator.test.ts +2358 -0
  62. package/src/lib/orchestrator.ts +1792 -0
  63. package/src/lib/output.ts +76 -0
  64. package/src/lib/plan-store.test.ts +148 -0
  65. package/src/lib/plan-store.ts +131 -0
  66. package/src/lib/research.test.ts +208 -0
  67. package/src/lib/research.ts +475 -0
  68. package/src/lib/round-trip.test.ts +341 -0
  69. package/src/lib/session-monitor.test.ts +152 -0
  70. package/src/lib/session-monitor.ts +282 -0
  71. package/src/lib/session-options.test.ts +146 -0
  72. package/src/lib/session-options.ts +100 -0
  73. package/src/lib/shell.test.ts +20 -0
  74. package/src/lib/shell.ts +8 -0
  75. package/src/lib/sizes.test.ts +73 -0
  76. package/src/lib/sizes.ts +36 -0
  77. package/src/lib/skill-registry.test.ts +134 -0
  78. package/src/lib/skill-registry.ts +86 -0
  79. package/src/lib/slugify.test.ts +35 -0
  80. package/src/lib/slugify.ts +10 -0
  81. package/src/lib/task-store.ts +352 -0
  82. package/src/lib/tmux.test.ts +500 -0
  83. package/src/lib/tmux.ts +372 -0
  84. package/src/lib/token-tracker.test.ts +102 -0
  85. package/src/lib/token-tracker.ts +70 -0
  86. package/src/lib/tunnels/cloudflare.test.ts +73 -0
  87. package/src/lib/tunnels/cloudflare.ts +254 -0
  88. package/src/lib/tunnels/manager.test.ts +63 -0
  89. package/src/lib/tunnels/manager.ts +132 -0
  90. package/src/lib/tunnels/ngrok.test.ts +94 -0
  91. package/src/lib/tunnels/ngrok.ts +261 -0
  92. package/src/lib/tunnels/tailscale.test.ts +103 -0
  93. package/src/lib/tunnels/tailscale.ts +1048 -0
  94. package/src/lib/tunnels/types.ts +44 -0
  95. package/src/lib/validation.test.ts +289 -0
  96. package/src/lib/validation.ts +123 -0
  97. package/src/lib/webhook.ts +51 -0
  98. package/src/lib/workflow-store.ts +209 -0
  99. package/src/lib/ws-v3/protocol.test.ts +127 -0
  100. package/src/lib/ws-v3/protocol.ts +160 -0
  101. package/src/lib/yaml-io.test.ts +125 -0
  102. package/src/lib/yaml-io.ts +27 -0
  103. package/src/ls.test.ts +76 -0
  104. package/src/ls.ts +40 -0
  105. package/src/metrics-cli.ts +177 -0
  106. package/src/notify.ts +85 -0
  107. package/src/orchestrator-status.ts +228 -0
  108. package/src/plan.ts +117 -0
  109. package/src/postinstall.test.ts +79 -0
  110. package/src/remote.test.ts +115 -0
  111. package/src/remote.ts +148 -0
  112. package/src/restart.ts +24 -0
  113. package/src/schemas/domain.ts +422 -0
  114. package/src/schemas/ide-config.ts +148 -0
  115. package/src/schemas/index.ts +53 -0
  116. package/src/send.test.ts +70 -0
  117. package/src/send.ts +181 -0
  118. package/src/skill.test.ts +114 -0
  119. package/src/skill.ts +138 -0
  120. package/src/status.ts +66 -0
  121. package/src/stop.test.ts +176 -0
  122. package/src/stop.ts +47 -0
  123. package/src/task.test.ts +1466 -0
  124. package/src/task.ts +1143 -0
  125. package/src/tunnel.test.ts +80 -0
  126. package/src/tunnel.ts +133 -0
  127. package/src/types.ts +15 -0
  128. package/src/ui/index.ts +32 -0
  129. package/src/ui/terminal/index.ts +9 -0
  130. package/src/ui/types.ts +91 -0
  131. package/src/ui/web/base.css +80 -0
  132. package/src/ui/web/components/Box.tsx +59 -0
  133. package/src/ui/web/components/Input.tsx +32 -0
  134. package/src/ui/web/components/ScrollBox.tsx +60 -0
  135. package/src/ui/web/components/Text.tsx +28 -0
  136. package/src/ui/web/hooks.ts +106 -0
  137. package/src/ui/web/index.ts +27 -0
  138. package/src/ui/web/render.ts +77 -0
  139. package/src/ui/web/utils/color.test.ts +93 -0
  140. package/src/ui/web/utils/color.ts +27 -0
  141. package/src/validate.test.ts +385 -0
  142. package/src/validate.ts +217 -0
  143. package/src/widgets/changes/index.tsx +576 -0
  144. package/src/widgets/config/index.tsx +440 -0
  145. package/src/widgets/costs/index.tsx +216 -0
  146. package/src/widgets/explorer/breadcrumbs.tsx +77 -0
  147. package/src/widgets/explorer/footer.tsx +20 -0
  148. package/src/widgets/explorer/header.tsx +23 -0
  149. package/src/widgets/explorer/index.tsx +408 -0
  150. package/src/widgets/explorer/tree-model.test.ts +214 -0
  151. package/src/widgets/explorer/tree-model.ts +103 -0
  152. package/src/widgets/explorer/tree.tsx +156 -0
  153. package/src/widgets/lib/config-model.ts +116 -0
  154. package/src/widgets/lib/files.test.ts +103 -0
  155. package/src/widgets/lib/files.ts +88 -0
  156. package/src/widgets/lib/git.test.ts +151 -0
  157. package/src/widgets/lib/git.ts +88 -0
  158. package/src/widgets/lib/pane-comms.test.ts +178 -0
  159. package/src/widgets/lib/pane-comms.ts +192 -0
  160. package/src/widgets/lib/theme.ts +151 -0
  161. package/src/widgets/lib/watcher.ts +50 -0
  162. package/src/widgets/mission-control/activity-feed.tsx +59 -0
  163. package/src/widgets/mission-control/agent-panel.tsx +132 -0
  164. package/src/widgets/mission-control/command-bar.tsx +63 -0
  165. package/src/widgets/mission-control/index.tsx +751 -0
  166. package/src/widgets/mission-control/task-panel.tsx +238 -0
  167. package/src/widgets/preview/index.tsx +415 -0
  168. package/src/widgets/resolve.ts +40 -0
  169. package/src/widgets/setup/agent-naming.tsx +112 -0
  170. package/src/widgets/setup/config-tree.tsx +238 -0
  171. package/src/widgets/setup/detect-panel.tsx +72 -0
  172. package/src/widgets/setup/field-editor.tsx +265 -0
  173. package/src/widgets/setup/footer.tsx +107 -0
  174. package/src/widgets/setup/index.tsx +340 -0
  175. package/src/widgets/setup/layout-picker.tsx +96 -0
  176. package/src/widgets/setup/orchestrator-panel.tsx +200 -0
  177. package/src/widgets/setup/review-panel.tsx +140 -0
  178. package/src/widgets/setup/setup-model.test.ts +303 -0
  179. package/src/widgets/setup/setup-model.ts +188 -0
  180. package/src/widgets/tasks/index.tsx +129 -0
  181. package/src/widgets/tasks/task-detail.tsx +309 -0
  182. package/src/widgets/tasks/task-form.tsx +193 -0
  183. package/src/widgets/tasks/task-list.tsx +205 -0
  184. package/src/widgets/tasks/task-model.test.ts +473 -0
  185. package/src/widgets/tasks/task-model.ts +157 -0
  186. package/templates/AGENTS.md +20 -0
  187. package/templates/missions.yml +41 -0
  188. package/templates/skills/backend.md +30 -0
  189. package/templates/skills/frontend.md +30 -0
  190. package/templates/skills/general-worker.md +30 -0
  191. package/templates/skills/researcher.md +69 -0
  192. package/templates/skills/reviewer.md +30 -0
  193. package/dist/attach.d.ts +0 -3
  194. package/dist/attach.js +0 -14
  195. package/dist/config.d.ts +0 -5
  196. package/dist/config.js +0 -269
  197. package/dist/detect.d.ts +0 -15
  198. package/dist/detect.js +0 -228
  199. package/dist/doctor.d.ts +0 -3
  200. package/dist/doctor.js +0 -66
  201. package/dist/init.d.ts +0 -4
  202. package/dist/init.js +0 -67
  203. package/dist/inspect.d.ts +0 -54
  204. package/dist/inspect.js +0 -118
  205. package/dist/launch.d.ts +0 -22
  206. package/dist/launch.js +0 -174
  207. package/dist/lib/dot-path.d.ts +0 -2
  208. package/dist/lib/dot-path.js +0 -17
  209. package/dist/lib/errors.d.ts +0 -29
  210. package/dist/lib/errors.js +0 -37
  211. package/dist/lib/launch-plan.d.ts +0 -6
  212. package/dist/lib/launch-plan.js +0 -40
  213. package/dist/lib/output.d.ts +0 -9
  214. package/dist/lib/output.js +0 -65
  215. package/dist/lib/session-monitor.d.ts +0 -12
  216. package/dist/lib/session-monitor.js +0 -160
  217. package/dist/lib/session-options.d.ts +0 -13
  218. package/dist/lib/session-options.js +0 -85
  219. package/dist/lib/sizes.d.ts +0 -16
  220. package/dist/lib/sizes.js +0 -36
  221. package/dist/lib/tmux.d.ts +0 -42
  222. package/dist/lib/tmux.js +0 -225
  223. package/dist/lib/yaml-io.d.ts +0 -10
  224. package/dist/lib/yaml-io.js +0 -24
  225. package/dist/ls.d.ts +0 -3
  226. package/dist/ls.js +0 -35
  227. package/dist/restart.d.ts +0 -4
  228. package/dist/restart.js +0 -13
  229. package/dist/status.d.ts +0 -3
  230. package/dist/status.js +0 -29
  231. package/dist/stop.d.ts +0 -3
  232. package/dist/stop.js +0 -21
  233. package/dist/types.d.ts +0 -41
  234. package/dist/types.js +0 -1
  235. package/dist/validate.d.ts +0 -4
  236. package/dist/validate.js +0 -172
@@ -0,0 +1,107 @@
1
+ import { RGBA, TextAttributes } from "@opentui/core";
2
+ import type { WidgetTheme, RGBA as RGBAType } from "../lib/theme.ts";
3
+
4
+ function toRGBA(c: RGBAType): RGBA {
5
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
6
+ }
7
+
8
+ export type ViewKind =
9
+ | "detect"
10
+ | "layout-picker"
11
+ | "agent-naming"
12
+ | "orchestrator"
13
+ | "review"
14
+ | "editor-tree"
15
+ | "editor-field";
16
+
17
+ export interface FooterActions {
18
+ onConfirm?: () => void;
19
+ onBack?: () => void;
20
+ onQuit?: () => void;
21
+ onAdd?: () => void;
22
+ onDelete?: () => void;
23
+ onSave?: () => void;
24
+ }
25
+
26
+ interface FooterProps {
27
+ viewKind: ViewKind;
28
+ theme: WidgetTheme;
29
+ actions?: FooterActions;
30
+ }
31
+
32
+ interface HintDef {
33
+ label: string;
34
+ actionKey?: keyof FooterActions;
35
+ }
36
+
37
+ const VIEW_HINTS: Record<ViewKind, HintDef[]> = {
38
+ detect: [
39
+ { label: "Enter:continue", actionKey: "onConfirm" },
40
+ { label: "q:quit", actionKey: "onQuit" },
41
+ ],
42
+ "layout-picker": [
43
+ { label: "j/k:navigate" },
44
+ { label: "Enter:select", actionKey: "onConfirm" },
45
+ { label: "q:quit", actionKey: "onQuit" },
46
+ ],
47
+ "agent-naming": [
48
+ { label: "Tab:next field" },
49
+ { label: "Enter:continue", actionKey: "onConfirm" },
50
+ { label: "Esc:back", actionKey: "onBack" },
51
+ { label: "q:quit", actionKey: "onQuit" },
52
+ ],
53
+ orchestrator: [
54
+ { label: "Space:toggle" },
55
+ { label: "Enter:continue", actionKey: "onConfirm" },
56
+ { label: "Esc:back", actionKey: "onBack" },
57
+ { label: "q:quit", actionKey: "onQuit" },
58
+ ],
59
+ review: [
60
+ { label: "Enter:confirm", actionKey: "onConfirm" },
61
+ { label: "Esc:back", actionKey: "onBack" },
62
+ { label: "q:quit", actionKey: "onQuit" },
63
+ ],
64
+ "editor-tree": [
65
+ { label: "j/k:nav" },
66
+ { label: "Enter:edit", actionKey: "onConfirm" },
67
+ { label: "a:add pane", actionKey: "onAdd" },
68
+ { label: "d:delete", actionKey: "onDelete" },
69
+ { label: "Ctrl+S:save", actionKey: "onSave" },
70
+ { label: "q:quit", actionKey: "onQuit" },
71
+ ],
72
+ "editor-field": [
73
+ { label: "Enter:save", actionKey: "onConfirm" },
74
+ { label: "Esc:cancel", actionKey: "onBack" },
75
+ ],
76
+ };
77
+
78
+ export function Footer(props: FooterProps) {
79
+ const theme = props.theme;
80
+ const hints = () => VIEW_HINTS[props.viewKind] ?? [];
81
+
82
+ return (
83
+ <box flexShrink={0}>
84
+ <box flexShrink={0} height={1}>
85
+ <text fg={toRGBA(theme.border)} wrapMode="none">
86
+ {"─".repeat(60)}
87
+ </text>
88
+ </box>
89
+ <box flexDirection="row" gap={2}>
90
+ {hints().map((hint) => {
91
+ const handler = hint.actionKey ? props.actions?.[hint.actionKey] : undefined;
92
+ const isClickable = !!handler;
93
+ return (
94
+ <text
95
+ fg={toRGBA(isClickable ? theme.fg : theme.fgMuted)}
96
+ attributes={isClickable ? TextAttributes.UNDERLINE : 0}
97
+ wrapMode="none"
98
+ onMouseDown={handler ? () => handler() : undefined}
99
+ >
100
+ {hint.label}
101
+ </text>
102
+ );
103
+ })}
104
+ </box>
105
+ </box>
106
+ );
107
+ }
@@ -0,0 +1,340 @@
1
+ import "@opentui/solid/runtime-plugin-support";
2
+ import { parseArgs } from "node:util";
3
+ import { existsSync } from "node:fs";
4
+ import { execFileSync } from "node:child_process";
5
+ import { join } from "node:path";
6
+ import { render, useTerminalDimensions } from "@opentui/solid";
7
+ import { RGBA } from "@opentui/core";
8
+ import { createSignal, Switch, Match } from "solid-js";
9
+ import { createTheme, type RGBA as RGBAType } from "../lib/theme.ts";
10
+ import { detectStack } from "../../detect.ts";
11
+ import { readConfig, writeConfig } from "../../lib/yaml-io.ts";
12
+ import {
13
+ PRESETS,
14
+ flattenConfigTree,
15
+ updateConfigAtPath,
16
+ addPane,
17
+ removePane,
18
+ validateSetupConfig,
19
+ type LayoutPreset,
20
+ type TreeNode,
21
+ } from "./setup-model.ts";
22
+ import type { IdeConfig } from "../../schemas/ide-config.ts";
23
+ import { DetectPanel, type DetectedStackInfo } from "./detect-panel.tsx";
24
+ import { LayoutPicker } from "./layout-picker.tsx";
25
+ import { AgentNaming } from "./agent-naming.tsx";
26
+ import { ConfigTree } from "./config-tree.tsx";
27
+ import { FieldEditor, type FieldType } from "./field-editor.tsx";
28
+ import { Footer, type ViewKind, type FooterActions } from "./footer.tsx";
29
+
30
+ const { values } = parseArgs({
31
+ options: {
32
+ dir: { type: "string" },
33
+ theme: { type: "string" },
34
+ wizard: { type: "boolean" },
35
+ edit: { type: "boolean" },
36
+ },
37
+ });
38
+
39
+ const dir = values.dir ?? process.cwd();
40
+ const themeConfig = values.theme ? JSON.parse(values.theme) : undefined;
41
+ const forceWizard = values.wizard ?? false;
42
+ const forceEdit = values.edit ?? false;
43
+
44
+ function toRGBA(c: RGBAType): RGBA {
45
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
46
+ }
47
+
48
+ // Determine field type from path for the field editor
49
+ function inferFieldType(path: string[]): { type: FieldType; enumValues?: string[] } {
50
+ const last = path[path.length - 1];
51
+ if (last === "role") return { type: "enum", enumValues: ["lead", "teammate", "planner"] };
52
+ if (last === "type")
53
+ return {
54
+ type: "enum",
55
+ enumValues: ["explorer", "changes", "preview", "tasks", "costs", "config", "mission-control"],
56
+ };
57
+ if (last === "dispatch_mode") return { type: "enum", enumValues: ["tasks", "goals"] };
58
+ if (last === "focus" || last === "enabled" || last === "auto_dispatch" || last === "widgets")
59
+ return { type: "boolean" };
60
+ if (last === "size") return { type: "size" };
61
+ return { type: "string" };
62
+ }
63
+
64
+ // Get current value at a path from config
65
+ function getValueAtPath(config: IdeConfig, path: string[]): string {
66
+ let current: unknown = config;
67
+ for (const key of path) {
68
+ if (current === null || current === undefined) return "";
69
+ if (typeof current === "object") {
70
+ current = (current as Record<string, unknown>)[key];
71
+ } else {
72
+ return "";
73
+ }
74
+ }
75
+ return current === null || current === undefined ? "" : String(current);
76
+ }
77
+
78
+ type View =
79
+ | { kind: "detect" }
80
+ | { kind: "layout-picker" }
81
+ | { kind: "agent-naming"; config: IdeConfig }
82
+ | { kind: "orchestrator" }
83
+ | { kind: "review"; config: IdeConfig }
84
+ | { kind: "editor-tree" }
85
+ | { kind: "editor-field"; path: string[] };
86
+
87
+ render(
88
+ () => {
89
+ const theme = createTheme(themeConfig);
90
+ const dimensions = useTerminalDimensions();
91
+
92
+ // Detect project stack once
93
+ const detected = detectStack(dir);
94
+ const detectedInfo: DetectedStackInfo = {
95
+ packageManager: detected.packageManager,
96
+ language: detected.language,
97
+ frameworks: detected.frameworks,
98
+ devCommand: detected.devCommand,
99
+ };
100
+
101
+ // Determine initial view
102
+ const hasConfig = existsSync(join(dir, "ide.yml"));
103
+ const initialView: View =
104
+ forceEdit && hasConfig
105
+ ? { kind: "editor-tree" }
106
+ : forceWizard || !hasConfig
107
+ ? { kind: "detect" }
108
+ : { kind: "editor-tree" };
109
+
110
+ const [view, setView] = createSignal<View>(initialView);
111
+
112
+ // Config state — loaded from file for editor mode, built from wizard flow
113
+ const initialConfig: IdeConfig = hasConfig
114
+ ? readConfig(dir).config
115
+ : { name: dir.split("/").pop() ?? "project", rows: [{ panes: [{ title: "Shell" }] }] };
116
+ const [config, setConfig] = createSignal<IdeConfig>(initialConfig);
117
+
118
+ function viewKind(): ViewKind {
119
+ return view().kind;
120
+ }
121
+
122
+ function footerActions(): FooterActions {
123
+ const quit = () => process.exit(0);
124
+ const kind = view().kind;
125
+
126
+ switch (kind) {
127
+ case "detect":
128
+ return {
129
+ onConfirm: () => setView({ kind: "layout-picker" }),
130
+ onQuit: quit,
131
+ };
132
+ case "layout-picker":
133
+ return { onQuit: quit };
134
+ case "agent-naming":
135
+ return {
136
+ onBack: () => setView({ kind: "layout-picker" }),
137
+ onQuit: quit,
138
+ };
139
+ case "orchestrator":
140
+ return {
141
+ onBack: () => setView({ kind: "agent-naming", config: config() }),
142
+ onQuit: quit,
143
+ };
144
+ case "review":
145
+ return {
146
+ onBack: () => setView({ kind: "agent-naming", config: config() }),
147
+ onQuit: quit,
148
+ };
149
+ case "editor-tree":
150
+ return {
151
+ onSave: () => handleSaveOnly(config()),
152
+ onQuit: quit,
153
+ };
154
+ case "editor-field":
155
+ return {
156
+ onBack: () => {
157
+ if (hasConfig && !forceWizard) {
158
+ setView({ kind: "editor-tree" });
159
+ } else {
160
+ setView({ kind: "review", config: config() });
161
+ }
162
+ },
163
+ };
164
+ default:
165
+ return { onQuit: quit };
166
+ }
167
+ }
168
+
169
+ function handleSaveAndLaunch(cfg: IdeConfig) {
170
+ writeConfig(dir, cfg);
171
+ try {
172
+ execFileSync("tmux-ide", [], { cwd: dir, stdio: "inherit" });
173
+ } catch {
174
+ // tmux-ide may not be in PATH when running as widget
175
+ }
176
+ process.exit(0);
177
+ }
178
+
179
+ function handleSaveOnly(cfg: IdeConfig) {
180
+ writeConfig(dir, cfg);
181
+ process.exit(0);
182
+ }
183
+
184
+ return (
185
+ <box
186
+ width={dimensions().width}
187
+ height={dimensions().height}
188
+ backgroundColor={toRGBA(theme.bg)}
189
+ >
190
+ <box flexGrow={1}>
191
+ <Switch>
192
+ {/* Wizard flow */}
193
+ <Match when={view().kind === "detect"}>
194
+ <DetectPanel
195
+ detected={detectedInfo}
196
+ onContinue={() => setView({ kind: "layout-picker" })}
197
+ theme={theme}
198
+ />
199
+ </Match>
200
+
201
+ <Match when={view().kind === "layout-picker"}>
202
+ <LayoutPicker
203
+ presets={PRESETS}
204
+ onSelect={(preset: LayoutPreset) => {
205
+ const name = dir.split("/").pop() ?? "project";
206
+ const cfg = preset.buildConfig(name, {
207
+ devCommand: detected.devCommand,
208
+ packageManager: detected.packageManager,
209
+ });
210
+ setConfig(cfg);
211
+
212
+ // If preset has claude panes, go to agent naming
213
+ const hasClaude = cfg.rows.some((r) =>
214
+ r.panes.some((p) => p.command === "claude"),
215
+ );
216
+ if (hasClaude) {
217
+ setView({ kind: "agent-naming", config: cfg });
218
+ } else {
219
+ setView({ kind: "review", config: cfg });
220
+ }
221
+ }}
222
+ theme={theme}
223
+ />
224
+ </Match>
225
+
226
+ <Match when={view().kind === "agent-naming"}>
227
+ <AgentNaming
228
+ config={config()}
229
+ onContinue={(names: string[]) => {
230
+ let cfg = config();
231
+ let nameIdx = 0;
232
+ // Apply names to claude panes
233
+ for (let ri = 0; ri < cfg.rows.length; ri++) {
234
+ for (let pi = 0; pi < cfg.rows[ri]!.panes.length; pi++) {
235
+ if (cfg.rows[ri]!.panes[pi]!.command === "claude" && nameIdx < names.length) {
236
+ cfg = updateConfigAtPath(
237
+ cfg,
238
+ ["rows", String(ri), "panes", String(pi), "title"],
239
+ names[nameIdx],
240
+ );
241
+ nameIdx++;
242
+ }
243
+ }
244
+ }
245
+ setConfig(cfg);
246
+ setView({ kind: "review", config: cfg });
247
+ }}
248
+ theme={theme}
249
+ />
250
+ </Match>
251
+
252
+ <Match when={view().kind === "review"}>
253
+ <ConfigTree
254
+ config={config()}
255
+ onEditField={(path) => setView({ kind: "editor-field", path })}
256
+ onAddPane={(rowIdx) => {
257
+ setConfig(addPane(config(), rowIdx));
258
+ }}
259
+ onDelete={(path) => {
260
+ // Handle pane deletion: path like ["rows","0","panes","1"]
261
+ if (path.length === 4 && path[0] === "rows" && path[2] === "panes") {
262
+ const rowIdx = parseInt(path[1]!, 10);
263
+ const paneIdx = parseInt(path[3]!, 10);
264
+ setConfig(removePane(config(), rowIdx, paneIdx));
265
+ }
266
+ }}
267
+ onSave={(cfg) => handleSaveAndLaunch(cfg)}
268
+ onConfigChange={(cfg) => setConfig(cfg)}
269
+ theme={theme}
270
+ />
271
+ </Match>
272
+
273
+ {/* Editor flow */}
274
+ <Match when={view().kind === "editor-tree"}>
275
+ <ConfigTree
276
+ config={config()}
277
+ onEditField={(path) => setView({ kind: "editor-field", path })}
278
+ onAddPane={(rowIdx) => {
279
+ setConfig(addPane(config(), rowIdx));
280
+ }}
281
+ onDelete={(path) => {
282
+ if (path.length === 4 && path[0] === "rows" && path[2] === "panes") {
283
+ const rowIdx = parseInt(path[1]!, 10);
284
+ const paneIdx = parseInt(path[3]!, 10);
285
+ setConfig(removePane(config(), rowIdx, paneIdx));
286
+ }
287
+ }}
288
+ onSave={(cfg) => handleSaveOnly(cfg)}
289
+ onConfigChange={(cfg) => setConfig(cfg)}
290
+ theme={theme}
291
+ />
292
+ </Match>
293
+
294
+ <Match when={view().kind === "editor-field"}>
295
+ {(() => {
296
+ const v = view() as Extract<View, { kind: "editor-field" }>;
297
+ const fieldInfo = inferFieldType(v.path);
298
+ return (
299
+ <FieldEditor
300
+ path={v.path}
301
+ value={getValueAtPath(config(), v.path)}
302
+ fieldType={fieldInfo.type}
303
+ enumValues={fieldInfo.enumValues}
304
+ onSave={(path, newValue) => {
305
+ setConfig(updateConfigAtPath(config(), path, newValue));
306
+ // Return to appropriate tree view
307
+ const prev = view();
308
+ // Check if we came from editor or wizard review
309
+ if (hasConfig && !forceWizard) {
310
+ setView({ kind: "editor-tree" });
311
+ } else {
312
+ setView({ kind: "review", config: config() });
313
+ }
314
+ }}
315
+ onCancel={() => {
316
+ if (hasConfig && !forceWizard) {
317
+ setView({ kind: "editor-tree" });
318
+ } else {
319
+ setView({ kind: "review", config: config() });
320
+ }
321
+ }}
322
+ theme={theme}
323
+ />
324
+ );
325
+ })()}
326
+ </Match>
327
+ </Switch>
328
+ </box>
329
+
330
+ <Footer viewKind={viewKind()} theme={theme} actions={footerActions()} />
331
+ </box>
332
+ );
333
+ },
334
+ {
335
+ targetFps: 30,
336
+ exitOnCtrlC: true,
337
+ useKittyKeyboard: {},
338
+ autoFocus: false,
339
+ },
340
+ );
@@ -0,0 +1,96 @@
1
+ import { createSignal } from "solid-js";
2
+ import { RGBA, TextAttributes } from "@opentui/core";
3
+ import { useKeyboard } from "@opentui/solid";
4
+ import type { LayoutPreset } from "./setup-model.ts";
5
+ import type { WidgetTheme, RGBA as RGBAType } from "../lib/theme.ts";
6
+
7
+ function toRGBA(c: RGBAType): RGBA {
8
+ return RGBA.fromInts(c.r, c.g, c.b, c.a);
9
+ }
10
+
11
+ interface LayoutPickerProps {
12
+ presets: LayoutPreset[];
13
+ onSelect: (preset: LayoutPreset) => void;
14
+ theme: WidgetTheme;
15
+ }
16
+
17
+ export function LayoutPicker(props: LayoutPickerProps) {
18
+ const [selectedIndex, setSelectedIndex] = createSignal(0);
19
+ const theme = props.theme;
20
+
21
+ useKeyboard((evt) => {
22
+ if (evt.name === "k" || evt.name === "up") {
23
+ setSelectedIndex((i) => Math.max(0, i - 1));
24
+ evt.preventDefault();
25
+ } else if (evt.name === "j" || evt.name === "down") {
26
+ setSelectedIndex((i) => Math.min(props.presets.length - 1, i + 1));
27
+ evt.preventDefault();
28
+ } else if (evt.name === "return") {
29
+ const preset = props.presets[selectedIndex()];
30
+ if (preset) props.onSelect(preset);
31
+ evt.preventDefault();
32
+ }
33
+ });
34
+
35
+ return (
36
+ <box paddingLeft={1} paddingRight={1}>
37
+ {/* Header */}
38
+ <box flexShrink={0} paddingBottom={1}>
39
+ <text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
40
+ Choose Layout
41
+ </text>
42
+ </box>
43
+
44
+ {/* Preset options */}
45
+ {props.presets.map((preset, index) => {
46
+ const isSelected = () => selectedIndex() === index;
47
+ return (
48
+ <box
49
+ flexShrink={0}
50
+ paddingBottom={1}
51
+ backgroundColor={isSelected() ? toRGBA(theme.selected) : undefined}
52
+ onMouseDown={() => setSelectedIndex(index)}
53
+ onMouseMove={() => setSelectedIndex(index)}
54
+ onMouseUp={() => props.onSelect(preset)}
55
+ >
56
+ <box flexDirection="row" gap={1}>
57
+ <text fg={toRGBA(isSelected() ? theme.accent : theme.fgMuted)}>
58
+ {isSelected() ? "▸" : " "}
59
+ </text>
60
+ <text
61
+ fg={toRGBA(isSelected() ? theme.accent : theme.fg)}
62
+ attributes={isSelected() ? TextAttributes.BOLD : 0}
63
+ >
64
+ {preset.label}
65
+ </text>
66
+ <text fg={toRGBA(theme.fgMuted)}>— {preset.description}</text>
67
+ </box>
68
+ {isSelected() ? (
69
+ <box paddingLeft={3}>
70
+ {preset.diagram.map((line) => (
71
+ <text fg={toRGBA(theme.fg)}>{line}</text>
72
+ ))}
73
+ </box>
74
+ ) : null}
75
+ </box>
76
+ );
77
+ })}
78
+
79
+ {/* Spacer */}
80
+ <box flexGrow={1} />
81
+
82
+ {/* Footer */}
83
+ <box flexShrink={0}>
84
+ <box flexShrink={0} height={1}>
85
+ <text fg={toRGBA(theme.border)} wrapMode="none">
86
+ {"─".repeat(40)}
87
+ </text>
88
+ </box>
89
+ <box flexDirection="row" gap={2}>
90
+ <text fg={toRGBA(theme.fgMuted)}>j/k:navigate</text>
91
+ <text fg={toRGBA(theme.fgMuted)}>Enter:select</text>
92
+ </box>
93
+ </box>
94
+ </box>
95
+ );
96
+ }