tmux-ide 1.2.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.
- package/bin/cli.js +4 -216
- package/bin/cli.ts +536 -0
- package/package.json +33 -14
- package/src/__tests__/support.ts +186 -0
- package/src/{attach.js → attach.ts} +7 -4
- package/src/cli.test.ts +314 -0
- package/src/command-center/discovery.test.ts +265 -0
- package/src/command-center/discovery.ts +296 -0
- package/src/command-center/index.ts +31 -0
- package/src/command-center/schemas.ts +57 -0
- package/src/command-center/server.test.ts +818 -0
- package/src/command-center/server.ts +1426 -0
- package/src/config-cli.test.ts +86 -0
- package/src/config.test.ts +205 -0
- package/src/config.ts +313 -0
- package/src/detect.test.ts +142 -0
- package/src/{detect.js → detect.ts} +31 -15
- package/src/dispatch.ts +102 -0
- package/src/doctor.test.ts +119 -0
- package/src/doctor.ts +167 -0
- package/src/init.test.ts +101 -0
- package/src/init.ts +144 -0
- package/src/inspect.test.ts +67 -0
- package/src/{inspect.js → inspect.ts} +95 -10
- package/src/integration.test.ts.skip +276 -0
- package/src/js-yaml.d.ts +10 -0
- package/src/launch.test.ts +222 -0
- package/src/launch.ts +494 -0
- package/src/lib/auth/auth-service.test.ts +178 -0
- package/src/lib/auth/auth-service.ts +227 -0
- package/src/lib/auth/middleware.test.ts +119 -0
- package/src/lib/auth/middleware.ts +56 -0
- package/src/lib/auth/types.ts +13 -0
- package/src/lib/authorship.test.ts +242 -0
- package/src/lib/authorship.ts +280 -0
- package/src/lib/cast/recorder.test.ts +127 -0
- package/src/lib/cast/recorder.ts +148 -0
- package/src/lib/daemon-watchdog.ts +102 -0
- package/src/lib/daemon.ts +417 -0
- package/src/lib/dot-path.test.ts +66 -0
- package/src/lib/dot-path.ts +17 -0
- package/src/lib/errors.ts +44 -0
- package/src/lib/event-log.test.ts +189 -0
- package/src/lib/event-log.ts +237 -0
- package/src/lib/github-pr.ts +182 -0
- package/src/lib/hq/client.test.ts +154 -0
- package/src/lib/hq/client.ts +142 -0
- package/src/lib/hq/mdns.test.ts +54 -0
- package/src/lib/hq/mdns.ts +88 -0
- package/src/lib/hq/registry.test.ts +237 -0
- package/src/lib/hq/registry.ts +145 -0
- package/src/lib/hq/types.ts +44 -0
- package/src/lib/ipc/socket-protocol.test.ts +126 -0
- package/src/lib/ipc/socket-protocol.ts +324 -0
- package/src/lib/launch-plan.test.ts +119 -0
- package/src/lib/launch-plan.ts +90 -0
- package/src/lib/log.test.ts +39 -0
- package/src/lib/log.ts +71 -0
- package/src/lib/metrics.test.ts +213 -0
- package/src/lib/metrics.ts +345 -0
- package/src/lib/orchestrator.test.ts +2358 -0
- package/src/lib/orchestrator.ts +1792 -0
- package/src/lib/{output.js → output.ts} +18 -10
- package/src/lib/plan-store.test.ts +148 -0
- package/src/lib/plan-store.ts +131 -0
- package/src/lib/research.test.ts +208 -0
- package/src/lib/research.ts +475 -0
- package/src/lib/round-trip.test.ts +341 -0
- package/src/lib/session-monitor.test.ts +152 -0
- package/src/lib/session-monitor.ts +282 -0
- package/src/lib/session-options.test.ts +146 -0
- package/src/lib/{session-options.js → session-options.ts} +11 -6
- package/src/lib/shell.test.ts +20 -0
- package/src/lib/shell.ts +8 -0
- package/src/lib/sizes.test.ts +73 -0
- package/src/lib/{sizes.js → sizes.ts} +4 -4
- package/src/lib/skill-registry.test.ts +134 -0
- package/src/lib/skill-registry.ts +86 -0
- package/src/lib/slugify.test.ts +35 -0
- package/src/lib/slugify.ts +10 -0
- package/src/lib/task-store.ts +352 -0
- package/src/lib/tmux.test.ts +500 -0
- package/src/lib/tmux.ts +372 -0
- package/src/lib/token-tracker.test.ts +102 -0
- package/src/lib/token-tracker.ts +70 -0
- package/src/lib/tunnels/cloudflare.test.ts +73 -0
- package/src/lib/tunnels/cloudflare.ts +254 -0
- package/src/lib/tunnels/manager.test.ts +63 -0
- package/src/lib/tunnels/manager.ts +132 -0
- package/src/lib/tunnels/ngrok.test.ts +94 -0
- package/src/lib/tunnels/ngrok.ts +261 -0
- package/src/lib/tunnels/tailscale.test.ts +103 -0
- package/src/lib/tunnels/tailscale.ts +1048 -0
- package/src/lib/tunnels/types.ts +44 -0
- package/src/lib/validation.test.ts +289 -0
- package/src/lib/validation.ts +123 -0
- package/src/lib/webhook.ts +51 -0
- package/src/lib/workflow-store.ts +209 -0
- package/src/lib/ws-v3/protocol.test.ts +127 -0
- package/src/lib/ws-v3/protocol.ts +160 -0
- package/src/lib/yaml-io.test.ts +125 -0
- package/src/lib/{yaml-io.js → yaml-io.ts} +5 -4
- package/src/ls.test.ts +76 -0
- package/src/{ls.js → ls.ts} +4 -4
- package/src/metrics-cli.ts +177 -0
- package/src/notify.ts +85 -0
- package/src/orchestrator-status.ts +228 -0
- package/src/plan.ts +117 -0
- package/src/postinstall.test.ts +79 -0
- package/src/remote.test.ts +115 -0
- package/src/remote.ts +148 -0
- package/src/restart.ts +24 -0
- package/src/schemas/domain.ts +422 -0
- package/src/schemas/ide-config.ts +148 -0
- package/src/schemas/index.ts +53 -0
- package/src/send.test.ts +70 -0
- package/src/send.ts +181 -0
- package/src/skill.test.ts +114 -0
- package/src/skill.ts +138 -0
- package/src/status.ts +66 -0
- package/src/stop.test.ts +176 -0
- package/src/stop.ts +47 -0
- package/src/task.test.ts +1466 -0
- package/src/task.ts +1143 -0
- package/src/tunnel.test.ts +80 -0
- package/src/tunnel.ts +133 -0
- package/src/types.ts +15 -0
- package/src/ui/index.ts +32 -0
- package/src/ui/terminal/index.ts +9 -0
- package/src/ui/types.ts +91 -0
- package/src/ui/web/base.css +80 -0
- package/src/ui/web/components/Box.tsx +59 -0
- package/src/ui/web/components/Input.tsx +32 -0
- package/src/ui/web/components/ScrollBox.tsx +60 -0
- package/src/ui/web/components/Text.tsx +28 -0
- package/src/ui/web/hooks.ts +106 -0
- package/src/ui/web/index.ts +27 -0
- package/src/ui/web/render.ts +77 -0
- package/src/ui/web/utils/color.test.ts +93 -0
- package/src/ui/web/utils/color.ts +27 -0
- package/src/validate.test.ts +385 -0
- package/src/validate.ts +217 -0
- package/src/widgets/changes/index.tsx +576 -0
- package/src/widgets/config/index.tsx +440 -0
- package/src/widgets/costs/index.tsx +216 -0
- package/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/src/widgets/explorer/footer.tsx +20 -0
- package/src/widgets/explorer/header.tsx +23 -0
- package/src/widgets/explorer/index.tsx +408 -0
- package/src/widgets/explorer/tree-model.test.ts +214 -0
- package/src/widgets/explorer/tree-model.ts +103 -0
- package/src/widgets/explorer/tree.tsx +156 -0
- package/src/widgets/lib/config-model.ts +116 -0
- package/src/widgets/lib/files.test.ts +103 -0
- package/src/widgets/lib/files.ts +88 -0
- package/src/widgets/lib/git.test.ts +151 -0
- package/src/widgets/lib/git.ts +88 -0
- package/src/widgets/lib/pane-comms.test.ts +178 -0
- package/src/widgets/lib/pane-comms.ts +192 -0
- package/src/widgets/lib/theme.ts +151 -0
- package/src/widgets/lib/watcher.ts +50 -0
- package/src/widgets/mission-control/activity-feed.tsx +59 -0
- package/src/widgets/mission-control/agent-panel.tsx +132 -0
- package/src/widgets/mission-control/command-bar.tsx +63 -0
- package/src/widgets/mission-control/index.tsx +751 -0
- package/src/widgets/mission-control/task-panel.tsx +238 -0
- package/src/widgets/preview/index.tsx +415 -0
- package/src/widgets/resolve.ts +40 -0
- package/src/widgets/setup/agent-naming.tsx +112 -0
- package/src/widgets/setup/config-tree.tsx +238 -0
- package/src/widgets/setup/detect-panel.tsx +72 -0
- package/src/widgets/setup/field-editor.tsx +265 -0
- package/src/widgets/setup/footer.tsx +107 -0
- package/src/widgets/setup/index.tsx +340 -0
- package/src/widgets/setup/layout-picker.tsx +96 -0
- package/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/src/widgets/setup/review-panel.tsx +140 -0
- package/src/widgets/setup/setup-model.test.ts +303 -0
- package/src/widgets/setup/setup-model.ts +188 -0
- package/src/widgets/tasks/index.tsx +129 -0
- package/src/widgets/tasks/task-detail.tsx +309 -0
- package/src/widgets/tasks/task-form.tsx +193 -0
- package/src/widgets/tasks/task-list.tsx +205 -0
- package/src/widgets/tasks/task-model.test.ts +473 -0
- package/src/widgets/tasks/task-model.ts +157 -0
- package/templates/AGENTS.md +20 -0
- package/templates/missions.yml +41 -0
- package/templates/skills/backend.md +30 -0
- package/templates/skills/frontend.md +30 -0
- package/templates/skills/general-worker.md +30 -0
- package/templates/skills/researcher.md +69 -0
- package/templates/skills/reviewer.md +30 -0
- package/src/config.js +0 -341
- package/src/doctor.js +0 -91
- package/src/init.js +0 -73
- package/src/launch.js +0 -245
- package/src/lib/dot-path.js +0 -16
- package/src/lib/errors.js +0 -35
- package/src/lib/launch-plan.js +0 -49
- package/src/lib/session-monitor.js +0 -179
- package/src/lib/tmux.js +0 -237
- package/src/restart.js +0 -16
- package/src/status.js +0 -35
- package/src/stop.js +0 -25
- package/src/validate.js +0 -154
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { createSignal, For } from "solid-js";
|
|
2
|
+
import { RGBA, TextAttributes } from "@opentui/core";
|
|
3
|
+
import { useKeyboard } from "@opentui/solid";
|
|
4
|
+
import type { WidgetTheme, RGBA as RGBAType } from "../lib/theme.ts";
|
|
5
|
+
|
|
6
|
+
function toRGBA(c: RGBAType): RGBA {
|
|
7
|
+
return RGBA.fromInts(c.r, c.g, c.b, c.a);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface OrchestratorPanelConfig {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
auto_dispatch: boolean;
|
|
13
|
+
master_pane: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface OrchestratorPanelProps {
|
|
17
|
+
claudePaneNames: string[];
|
|
18
|
+
theme: WidgetTheme;
|
|
19
|
+
onContinue: (config: OrchestratorPanelConfig) => void;
|
|
20
|
+
onBack: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Field = "enabled" | "master_pane" | "auto_dispatch" | "confirm";
|
|
24
|
+
|
|
25
|
+
export function OrchestratorPanel(props: OrchestratorPanelProps) {
|
|
26
|
+
const [enabled, setEnabled] = createSignal(true);
|
|
27
|
+
const [autoDispatch, setAutoDispatch] = createSignal(true);
|
|
28
|
+
const [masterIdx, setMasterIdx] = createSignal(0);
|
|
29
|
+
const [activeField, setActiveField] = createSignal<Field>("enabled");
|
|
30
|
+
|
|
31
|
+
const theme = props.theme;
|
|
32
|
+
|
|
33
|
+
const fields: Field[] = ["enabled", "master_pane", "auto_dispatch", "confirm"];
|
|
34
|
+
|
|
35
|
+
function fieldIndex() {
|
|
36
|
+
return fields.indexOf(activeField());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
useKeyboard((evt) => {
|
|
40
|
+
if (evt.name === "escape") {
|
|
41
|
+
props.onBack();
|
|
42
|
+
evt.preventDefault();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
47
|
+
const next = Math.min(fieldIndex() + 1, fields.length - 1);
|
|
48
|
+
setActiveField(fields[next]!);
|
|
49
|
+
evt.preventDefault();
|
|
50
|
+
} else if (evt.name === "up" || evt.name === "k") {
|
|
51
|
+
const prev = Math.max(fieldIndex() - 1, 0);
|
|
52
|
+
setActiveField(fields[prev]!);
|
|
53
|
+
evt.preventDefault();
|
|
54
|
+
} else if (evt.name === "space" || evt.name === "return") {
|
|
55
|
+
const field = activeField();
|
|
56
|
+
if (field === "enabled") {
|
|
57
|
+
setEnabled(!enabled());
|
|
58
|
+
} else if (field === "master_pane" && props.claudePaneNames.length > 0) {
|
|
59
|
+
setMasterIdx((masterIdx() + 1) % props.claudePaneNames.length);
|
|
60
|
+
} else if (field === "auto_dispatch") {
|
|
61
|
+
setAutoDispatch(!autoDispatch());
|
|
62
|
+
} else if (field === "confirm") {
|
|
63
|
+
props.onContinue({
|
|
64
|
+
enabled: enabled(),
|
|
65
|
+
auto_dispatch: autoDispatch(),
|
|
66
|
+
master_pane: props.claudePaneNames[masterIdx()] ?? null,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
evt.preventDefault();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function toggleLabel(active: boolean): string {
|
|
74
|
+
return active ? "[x]" : "[ ]";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isActive(field: Field): boolean {
|
|
78
|
+
return activeField() === field;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
83
|
+
{/* Header */}
|
|
84
|
+
<box flexShrink={0} paddingBottom={1}>
|
|
85
|
+
<text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
|
|
86
|
+
Orchestrator Configuration
|
|
87
|
+
</text>
|
|
88
|
+
<text fg={toRGBA(theme.fgMuted)}>
|
|
89
|
+
Configure multi-agent orchestration for your team layout.
|
|
90
|
+
</text>
|
|
91
|
+
</box>
|
|
92
|
+
|
|
93
|
+
{/* Enabled toggle */}
|
|
94
|
+
<box
|
|
95
|
+
flexShrink={0}
|
|
96
|
+
flexDirection="row"
|
|
97
|
+
gap={1}
|
|
98
|
+
paddingBottom={1}
|
|
99
|
+
onMouseDown={() => {
|
|
100
|
+
setActiveField("enabled");
|
|
101
|
+
setEnabled(!enabled());
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<text
|
|
105
|
+
fg={toRGBA(isActive("enabled") ? theme.accent : theme.fg)}
|
|
106
|
+
attributes={isActive("enabled") ? TextAttributes.BOLD : undefined}
|
|
107
|
+
>
|
|
108
|
+
{isActive("enabled") ? ">" : " "} {toggleLabel(enabled())} Enabled
|
|
109
|
+
</text>
|
|
110
|
+
</box>
|
|
111
|
+
|
|
112
|
+
{/* Master pane picker */}
|
|
113
|
+
<box flexShrink={0} paddingBottom={1}>
|
|
114
|
+
<text
|
|
115
|
+
fg={toRGBA(isActive("master_pane") ? theme.accent : theme.fg)}
|
|
116
|
+
attributes={isActive("master_pane") ? TextAttributes.BOLD : undefined}
|
|
117
|
+
>
|
|
118
|
+
{isActive("master_pane") ? ">" : " "} Master Pane
|
|
119
|
+
</text>
|
|
120
|
+
<box paddingLeft={4}>
|
|
121
|
+
<For each={props.claudePaneNames}>
|
|
122
|
+
{(name, i) => (
|
|
123
|
+
<text
|
|
124
|
+
fg={toRGBA(i() === masterIdx() ? theme.accent : theme.fgMuted)}
|
|
125
|
+
onMouseDown={() => {
|
|
126
|
+
setActiveField("master_pane");
|
|
127
|
+
setMasterIdx(i());
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{i() === masterIdx() ? "● " : "○ "}
|
|
131
|
+
{name}
|
|
132
|
+
</text>
|
|
133
|
+
)}
|
|
134
|
+
</For>
|
|
135
|
+
</box>
|
|
136
|
+
<box paddingLeft={4}>
|
|
137
|
+
<text fg={toRGBA(theme.fgMuted)}>Space/Enter to cycle selection</text>
|
|
138
|
+
</box>
|
|
139
|
+
</box>
|
|
140
|
+
|
|
141
|
+
{/* Auto dispatch toggle */}
|
|
142
|
+
<box
|
|
143
|
+
flexShrink={0}
|
|
144
|
+
flexDirection="row"
|
|
145
|
+
gap={1}
|
|
146
|
+
paddingBottom={1}
|
|
147
|
+
onMouseDown={() => {
|
|
148
|
+
setActiveField("auto_dispatch");
|
|
149
|
+
setAutoDispatch(!autoDispatch());
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
<text
|
|
153
|
+
fg={toRGBA(isActive("auto_dispatch") ? theme.accent : theme.fg)}
|
|
154
|
+
attributes={isActive("auto_dispatch") ? TextAttributes.BOLD : undefined}
|
|
155
|
+
>
|
|
156
|
+
{isActive("auto_dispatch") ? ">" : " "} {toggleLabel(autoDispatch())} Auto-dispatch tasks
|
|
157
|
+
</text>
|
|
158
|
+
</box>
|
|
159
|
+
|
|
160
|
+
{/* Spacer */}
|
|
161
|
+
<box flexGrow={1} />
|
|
162
|
+
|
|
163
|
+
{/* Continue button */}
|
|
164
|
+
<box
|
|
165
|
+
flexShrink={0}
|
|
166
|
+
paddingBottom={1}
|
|
167
|
+
onMouseDown={() => {
|
|
168
|
+
setActiveField("confirm");
|
|
169
|
+
props.onContinue({
|
|
170
|
+
enabled: enabled(),
|
|
171
|
+
auto_dispatch: autoDispatch(),
|
|
172
|
+
master_pane: props.claudePaneNames[masterIdx()] ?? null,
|
|
173
|
+
});
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
<text
|
|
177
|
+
fg={toRGBA(isActive("confirm") ? theme.accent : theme.fgMuted)}
|
|
178
|
+
attributes={isActive("confirm") ? TextAttributes.BOLD : undefined}
|
|
179
|
+
>
|
|
180
|
+
{isActive("confirm") ? ">" : " "} [ Continue → ]
|
|
181
|
+
</text>
|
|
182
|
+
</box>
|
|
183
|
+
|
|
184
|
+
{/* Footer */}
|
|
185
|
+
<box flexShrink={0}>
|
|
186
|
+
<box flexShrink={0} height={1}>
|
|
187
|
+
<text fg={toRGBA(theme.border)} wrapMode="none">
|
|
188
|
+
{"─".repeat(40)}
|
|
189
|
+
</text>
|
|
190
|
+
</box>
|
|
191
|
+
<box flexDirection="row" gap={2}>
|
|
192
|
+
<text fg={toRGBA(theme.fgMuted)}>j/k:navigate</text>
|
|
193
|
+
<text fg={toRGBA(theme.fgMuted)}>Space:toggle</text>
|
|
194
|
+
<text fg={toRGBA(theme.fgMuted)}>Enter:select</text>
|
|
195
|
+
<text fg={toRGBA(theme.fgMuted)}>Esc:back</text>
|
|
196
|
+
</box>
|
|
197
|
+
</box>
|
|
198
|
+
</box>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createSignal, createMemo, Show } from "solid-js";
|
|
2
|
+
import { RGBA, TextAttributes } from "@opentui/core";
|
|
3
|
+
import { useKeyboard } from "@opentui/solid";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import type { IdeConfig } from "../../schemas/ide-config.ts";
|
|
6
|
+
import { validateConfig } from "../../validate.ts";
|
|
7
|
+
import { writeConfig } from "../../lib/yaml-io.ts";
|
|
8
|
+
import type { WidgetTheme, RGBA as RGBAType } from "../lib/theme.ts";
|
|
9
|
+
|
|
10
|
+
function toRGBA(c: RGBAType): RGBA {
|
|
11
|
+
return RGBA.fromInts(c.r, c.g, c.b, c.a);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ReviewAction = "write" | "launch";
|
|
15
|
+
|
|
16
|
+
interface ReviewPanelProps {
|
|
17
|
+
config: IdeConfig;
|
|
18
|
+
dir: string;
|
|
19
|
+
theme: WidgetTheme;
|
|
20
|
+
onDone: (action: ReviewAction) => void;
|
|
21
|
+
onBack: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ReviewPanel(props: ReviewPanelProps) {
|
|
25
|
+
const [selectedAction, setSelectedAction] = createSignal<0 | 1>(0);
|
|
26
|
+
|
|
27
|
+
const theme = props.theme;
|
|
28
|
+
|
|
29
|
+
const yamlPreview = createMemo(() =>
|
|
30
|
+
yaml.dump(props.config, { lineWidth: -1, noRefs: true, quotingType: '"' }),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const errors = createMemo(() => validateConfig(props.config));
|
|
34
|
+
const isValid = createMemo(() => errors().length === 0);
|
|
35
|
+
|
|
36
|
+
useKeyboard((evt) => {
|
|
37
|
+
if (evt.name === "escape") {
|
|
38
|
+
props.onBack();
|
|
39
|
+
evt.preventDefault();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (evt.name === "j" || evt.name === "down" || evt.name === "tab") {
|
|
44
|
+
setSelectedAction((prev) => (prev === 0 ? 1 : 0) as 0 | 1);
|
|
45
|
+
evt.preventDefault();
|
|
46
|
+
} else if (evt.name === "k" || evt.name === "up") {
|
|
47
|
+
setSelectedAction((prev) => (prev === 0 ? 1 : 0) as 0 | 1);
|
|
48
|
+
evt.preventDefault();
|
|
49
|
+
} else if (evt.name === "return") {
|
|
50
|
+
if (!isValid()) return;
|
|
51
|
+
writeConfig(props.dir, props.config);
|
|
52
|
+
props.onDone(selectedAction() === 0 ? "write" : "launch");
|
|
53
|
+
evt.preventDefault();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
59
|
+
{/* Header */}
|
|
60
|
+
<box flexShrink={0} paddingBottom={1}>
|
|
61
|
+
<text fg={toRGBA(theme.accent)} attributes={TextAttributes.BOLD}>
|
|
62
|
+
Review Configuration
|
|
63
|
+
</text>
|
|
64
|
+
</box>
|
|
65
|
+
|
|
66
|
+
{/* YAML preview */}
|
|
67
|
+
<box flexGrow={1} flexShrink={1}>
|
|
68
|
+
<text fg={toRGBA(theme.fgMuted)} attributes={TextAttributes.BOLD}>
|
|
69
|
+
ide.yml preview:
|
|
70
|
+
</text>
|
|
71
|
+
<scrollbox>
|
|
72
|
+
<text fg={toRGBA(theme.fg)}>{yamlPreview()}</text>
|
|
73
|
+
</scrollbox>
|
|
74
|
+
</box>
|
|
75
|
+
|
|
76
|
+
{/* Validation status */}
|
|
77
|
+
<box flexShrink={0} paddingTop={1} paddingBottom={1}>
|
|
78
|
+
<Show
|
|
79
|
+
when={isValid()}
|
|
80
|
+
fallback={
|
|
81
|
+
<box>
|
|
82
|
+
<text fg={RGBA.fromInts(255, 80, 80, 255)} attributes={TextAttributes.BOLD}>
|
|
83
|
+
Validation errors:
|
|
84
|
+
</text>
|
|
85
|
+
{errors().map((e) => (
|
|
86
|
+
<text fg={RGBA.fromInts(255, 80, 80, 255)}> - {e}</text>
|
|
87
|
+
))}
|
|
88
|
+
</box>
|
|
89
|
+
}
|
|
90
|
+
>
|
|
91
|
+
<text fg={toRGBA(theme.gitAdded)} attributes={TextAttributes.BOLD}>
|
|
92
|
+
Config is valid
|
|
93
|
+
</text>
|
|
94
|
+
</Show>
|
|
95
|
+
</box>
|
|
96
|
+
|
|
97
|
+
{/* Action buttons */}
|
|
98
|
+
<box flexShrink={0} paddingBottom={1}>
|
|
99
|
+
<text
|
|
100
|
+
fg={toRGBA(selectedAction() === 0 ? theme.accent : theme.fgMuted)}
|
|
101
|
+
attributes={selectedAction() === 0 ? TextAttributes.BOLD : undefined}
|
|
102
|
+
onMouseDown={() => setSelectedAction(0)}
|
|
103
|
+
onMouseUp={() => {
|
|
104
|
+
if (!isValid()) return;
|
|
105
|
+
writeConfig(props.dir, props.config);
|
|
106
|
+
props.onDone("write");
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{selectedAction() === 0 ? ">" : " "} [ Write ide.yml ]
|
|
110
|
+
</text>
|
|
111
|
+
<text
|
|
112
|
+
fg={toRGBA(selectedAction() === 1 ? theme.accent : theme.fgMuted)}
|
|
113
|
+
attributes={selectedAction() === 1 ? TextAttributes.BOLD : undefined}
|
|
114
|
+
onMouseDown={() => setSelectedAction(1)}
|
|
115
|
+
onMouseUp={() => {
|
|
116
|
+
if (!isValid()) return;
|
|
117
|
+
writeConfig(props.dir, props.config);
|
|
118
|
+
props.onDone("launch");
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{selectedAction() === 1 ? ">" : " "} [ Write & Launch ]
|
|
122
|
+
</text>
|
|
123
|
+
</box>
|
|
124
|
+
|
|
125
|
+
{/* Footer */}
|
|
126
|
+
<box flexShrink={0}>
|
|
127
|
+
<box flexShrink={0} height={1}>
|
|
128
|
+
<text fg={toRGBA(theme.border)} wrapMode="none">
|
|
129
|
+
{"─".repeat(40)}
|
|
130
|
+
</text>
|
|
131
|
+
</box>
|
|
132
|
+
<box flexDirection="row" gap={2}>
|
|
133
|
+
<text fg={toRGBA(theme.fgMuted)}>j/k:select action</text>
|
|
134
|
+
<text fg={toRGBA(theme.fgMuted)}>Enter:confirm</text>
|
|
135
|
+
<text fg={toRGBA(theme.fgMuted)}>Esc:back</text>
|
|
136
|
+
</box>
|
|
137
|
+
</box>
|
|
138
|
+
</box>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
PRESETS,
|
|
4
|
+
getPreset,
|
|
5
|
+
flattenConfigTree,
|
|
6
|
+
updateConfigAtPath,
|
|
7
|
+
addPane,
|
|
8
|
+
removePane,
|
|
9
|
+
validateSetupConfig,
|
|
10
|
+
} from "./setup-model.ts";
|
|
11
|
+
import type { IdeConfig } from "../../schemas/ide-config.ts";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Layout Presets
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
describe("PRESETS", () => {
|
|
18
|
+
it("has 4 presets", () => {
|
|
19
|
+
expect(PRESETS.length).toBe(4);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("each preset has required fields", () => {
|
|
23
|
+
for (const preset of PRESETS) {
|
|
24
|
+
expect(preset.id, `preset missing id`).toBeTruthy();
|
|
25
|
+
expect(preset.label, `preset ${preset.id} missing label`).toBeTruthy();
|
|
26
|
+
expect(preset.description, `preset ${preset.id} missing description`).toBeTruthy();
|
|
27
|
+
expect(preset.diagram.length > 0, `preset ${preset.id} missing diagram`).toBeTruthy();
|
|
28
|
+
expect(typeof preset.buildConfig).toBe("function");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("each preset builds a valid IdeConfig", () => {
|
|
33
|
+
for (const preset of PRESETS) {
|
|
34
|
+
const config = preset.buildConfig("test-project");
|
|
35
|
+
const result = validateSetupConfig(config);
|
|
36
|
+
expect(
|
|
37
|
+
result.valid,
|
|
38
|
+
`preset ${preset.id} produced invalid config: ${!result.valid ? result.errors.join(", ") : ""}`,
|
|
39
|
+
).toBeTruthy();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("dual-claude preset builds 2 claude panes + dev/shell", () => {
|
|
44
|
+
const config = getPreset("dual-claude")!.buildConfig("my-app");
|
|
45
|
+
expect(config.rows.length).toBe(2);
|
|
46
|
+
expect(config.rows[0]!.size).toBe("70%");
|
|
47
|
+
expect(config.rows[0]!.panes.length).toBe(2);
|
|
48
|
+
expect(config.rows[0]!.panes[0]!.command).toBe("claude");
|
|
49
|
+
expect(config.rows[0]!.panes[1]!.command).toBe("claude");
|
|
50
|
+
expect(config.rows[1]!.panes.length).toBe(2);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("triple-claude preset builds 3 claude panes + dev/shell", () => {
|
|
54
|
+
const config = getPreset("triple-claude")!.buildConfig("my-app");
|
|
55
|
+
expect(config.rows[0]!.panes.length).toBe(3);
|
|
56
|
+
expect(config.rows[0]!.size).toBe("70%");
|
|
57
|
+
for (const pane of config.rows[0]!.panes) {
|
|
58
|
+
expect(pane.command).toBe("claude");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("single-claude preset builds 1 claude pane 60% + 3 bottom panes 40%", () => {
|
|
63
|
+
const config = getPreset("single-claude")!.buildConfig("my-app");
|
|
64
|
+
expect(config.rows[0]!.size).toBe("60%");
|
|
65
|
+
expect(config.rows[0]!.panes.length).toBe(1);
|
|
66
|
+
expect(config.rows[0]!.panes[0]!.command).toBe("claude");
|
|
67
|
+
expect(config.rows[1]!.panes.length).toBe(3);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("agent-team preset has team, orchestrator, and role assignments", () => {
|
|
71
|
+
const config = getPreset("agent-team")!.buildConfig("my-app");
|
|
72
|
+
expect(config.team, "should have team config").toBeTruthy();
|
|
73
|
+
expect(config.orchestrator, "should have orchestrator config").toBeTruthy();
|
|
74
|
+
expect(config.orchestrator!.enabled).toBe(true);
|
|
75
|
+
expect(config.rows[0]!.panes[0]!.role).toBe("lead");
|
|
76
|
+
expect(config.rows[0]!.panes[1]!.role).toBe("teammate");
|
|
77
|
+
expect(config.rows[0]!.panes[2]!.role).toBe("teammate");
|
|
78
|
+
expect(config.rows[1]!.panes.length).toBe(3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("presets use detected devCommand when provided", () => {
|
|
82
|
+
const config = getPreset("dual-claude")!.buildConfig("my-app", {
|
|
83
|
+
devCommand: "pnpm dev",
|
|
84
|
+
packageManager: "pnpm",
|
|
85
|
+
});
|
|
86
|
+
expect(config.rows[1]!.panes[0]!.command).toBe("pnpm dev");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("presets fall back to packageManager for dev command", () => {
|
|
90
|
+
const config = getPreset("dual-claude")!.buildConfig("my-app", {
|
|
91
|
+
devCommand: null,
|
|
92
|
+
packageManager: "yarn",
|
|
93
|
+
});
|
|
94
|
+
expect(config.rows[1]!.panes[0]!.command).toBe("yarn dev");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("getPreset", () => {
|
|
99
|
+
it("returns preset by id", () => {
|
|
100
|
+
expect(getPreset("dual-claude")!.id).toBe("dual-claude");
|
|
101
|
+
expect(getPreset("agent-team")!.id).toBe("agent-team");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns undefined for unknown id", () => {
|
|
105
|
+
expect(getPreset("nonexistent")).toBe(undefined);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// flattenConfigTree
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
describe("flattenConfigTree", () => {
|
|
114
|
+
const simple: IdeConfig = {
|
|
115
|
+
name: "test",
|
|
116
|
+
rows: [
|
|
117
|
+
{
|
|
118
|
+
size: "70%",
|
|
119
|
+
panes: [{ title: "Claude 1", command: "claude" }, { title: "Shell" }],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
it("produces nodes for all config values", () => {
|
|
125
|
+
const nodes = flattenConfigTree(simple);
|
|
126
|
+
expect(nodes.length > 0).toBeTruthy();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("includes leaf nodes with string values", () => {
|
|
130
|
+
const nodes = flattenConfigTree(simple);
|
|
131
|
+
const nameNode = nodes.find((n) => n.path.length === 1 && n.path[0] === "name");
|
|
132
|
+
expect(nameNode).toBeTruthy();
|
|
133
|
+
expect(nameNode!.value).toBe("test");
|
|
134
|
+
expect(nameNode!.expandable).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("includes container nodes with null value", () => {
|
|
138
|
+
const nodes = flattenConfigTree(simple);
|
|
139
|
+
const rowsNode = nodes.find((n) => n.path.length === 1 && n.path[0] === "rows");
|
|
140
|
+
expect(rowsNode).toBeTruthy();
|
|
141
|
+
expect(rowsNode!.value).toBe(null);
|
|
142
|
+
expect(rowsNode!.expandable).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("creates correct paths for deeply nested values", () => {
|
|
146
|
+
const nodes = flattenConfigTree(simple);
|
|
147
|
+
const titleNode = nodes.find(
|
|
148
|
+
(n) =>
|
|
149
|
+
n.path.length === 4 &&
|
|
150
|
+
n.path[0] === "rows" &&
|
|
151
|
+
n.path[1] === "0" &&
|
|
152
|
+
n.path[2] === "panes" &&
|
|
153
|
+
n.path[3] === "0",
|
|
154
|
+
);
|
|
155
|
+
expect(titleNode, "should have node for rows.0.panes.0").toBeTruthy();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("sets depth correctly", () => {
|
|
159
|
+
const nodes = flattenConfigTree(simple);
|
|
160
|
+
const nameNode = nodes.find((n) => n.label === "name" && n.path.length === 1);
|
|
161
|
+
expect(nameNode!.depth).toBe(0);
|
|
162
|
+
|
|
163
|
+
const paneTitle = nodes.find((n) => n.label === "title" && n.value === "Claude 1");
|
|
164
|
+
expect(paneTitle).toBeTruthy();
|
|
165
|
+
expect(paneTitle!.depth).toBe(4); // rows.0.panes.0.title
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("skips undefined optional fields", () => {
|
|
169
|
+
const config: IdeConfig = { rows: [{ panes: [{ title: "X" }] }] };
|
|
170
|
+
const nodes = flattenConfigTree(config);
|
|
171
|
+
const nameNode = nodes.find((n) => n.label === "name");
|
|
172
|
+
expect(nameNode).toBe(undefined);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// updateConfigAtPath
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
describe("updateConfigAtPath", () => {
|
|
181
|
+
const base: IdeConfig = {
|
|
182
|
+
name: "test",
|
|
183
|
+
rows: [
|
|
184
|
+
{
|
|
185
|
+
size: "70%",
|
|
186
|
+
panes: [{ title: "Claude", command: "claude" }],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
it("updates a top-level string value", () => {
|
|
192
|
+
const updated = updateConfigAtPath(base, ["name"], "new-name");
|
|
193
|
+
expect(updated.name).toBe("new-name");
|
|
194
|
+
expect(base.name).toBe("test"); // original unchanged
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("updates a nested value", () => {
|
|
198
|
+
const updated = updateConfigAtPath(base, ["rows", "0", "panes", "0", "title"], "Renamed");
|
|
199
|
+
expect(updated.rows[0]!.panes[0]!.title).toBe("Renamed");
|
|
200
|
+
expect(base.rows[0]!.panes[0]!.title).toBe("Claude"); // original unchanged
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("updates row size", () => {
|
|
204
|
+
const updated = updateConfigAtPath(base, ["rows", "0", "size"], "50%");
|
|
205
|
+
expect(updated.rows[0]!.size).toBe("50%");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// addPane / removePane
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
describe("addPane", () => {
|
|
214
|
+
const base: IdeConfig = {
|
|
215
|
+
name: "test",
|
|
216
|
+
rows: [{ panes: [{ title: "Existing" }] }],
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
it("adds a new pane to the specified row", () => {
|
|
220
|
+
const updated = addPane(base, 0);
|
|
221
|
+
expect(updated.rows[0]!.panes.length).toBe(2);
|
|
222
|
+
expect(updated.rows[0]!.panes[1]!.title).toBe("New Pane");
|
|
223
|
+
expect(base.rows[0]!.panes.length).toBe(1); // original unchanged
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("returns clone unchanged for invalid row index", () => {
|
|
227
|
+
const updated = addPane(base, 99);
|
|
228
|
+
expect(updated.rows[0]!.panes.length).toBe(1);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("removePane", () => {
|
|
233
|
+
const base: IdeConfig = {
|
|
234
|
+
name: "test",
|
|
235
|
+
rows: [{ panes: [{ title: "First" }, { title: "Second" }, { title: "Third" }] }],
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
it("removes the pane at the specified index", () => {
|
|
239
|
+
const updated = removePane(base, 0, 1);
|
|
240
|
+
expect(updated.rows[0]!.panes.length).toBe(2);
|
|
241
|
+
expect(updated.rows[0]!.panes[0]!.title).toBe("First");
|
|
242
|
+
expect(updated.rows[0]!.panes[1]!.title).toBe("Third");
|
|
243
|
+
expect(base.rows[0]!.panes.length).toBe(3); // original unchanged
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("does not remove the last pane in a row", () => {
|
|
247
|
+
const single: IdeConfig = {
|
|
248
|
+
rows: [{ panes: [{ title: "Only" }] }],
|
|
249
|
+
};
|
|
250
|
+
const updated = removePane(single, 0, 0);
|
|
251
|
+
expect(updated.rows[0]!.panes.length).toBe(1);
|
|
252
|
+
expect(updated.rows[0]!.panes[0]!.title).toBe("Only");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("returns clone unchanged for invalid row index", () => {
|
|
256
|
+
const updated = removePane(base, 99, 0);
|
|
257
|
+
expect(updated.rows[0]!.panes.length).toBe(3);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// validateSetupConfig
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
describe("validateSetupConfig", () => {
|
|
266
|
+
it("returns valid for a correct config", () => {
|
|
267
|
+
const config: IdeConfig = {
|
|
268
|
+
name: "test",
|
|
269
|
+
rows: [{ panes: [{ title: "Shell" }] }],
|
|
270
|
+
};
|
|
271
|
+
const result = validateSetupConfig(config);
|
|
272
|
+
expect(result.valid).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("returns errors for missing rows", () => {
|
|
276
|
+
const result = validateSetupConfig({ name: "test" });
|
|
277
|
+
expect(result.valid).toBe(false);
|
|
278
|
+
expect(result.errors.length > 0).toBeTruthy();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("returns errors for empty rows array", () => {
|
|
282
|
+
const result = validateSetupConfig({ name: "test", rows: [] });
|
|
283
|
+
expect(result.valid).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("returns errors for empty panes array in a row", () => {
|
|
287
|
+
const result = validateSetupConfig({
|
|
288
|
+
name: "test",
|
|
289
|
+
rows: [{ panes: [] }],
|
|
290
|
+
});
|
|
291
|
+
expect(result.valid).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("returns valid for config with optional fields", () => {
|
|
295
|
+
const config = {
|
|
296
|
+
name: "test",
|
|
297
|
+
rows: [{ size: "70%", panes: [{ title: "X", command: "claude", focus: true }] }],
|
|
298
|
+
theme: { accent: "colour75" },
|
|
299
|
+
};
|
|
300
|
+
const result = validateSetupConfig(config);
|
|
301
|
+
expect(result.valid).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
});
|