@ekkos/cli 0.3.3 → 1.0.1
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/README.md +57 -0
- package/dist/agent/daemon.d.ts +27 -0
- package/dist/agent/daemon.js +254 -29
- package/dist/agent/health-check.d.ts +35 -0
- package/dist/agent/health-check.js +243 -0
- package/dist/agent/pty-runner.d.ts +1 -0
- package/dist/agent/pty-runner.js +6 -1
- package/dist/capture/transcript-repair.d.ts +1 -0
- package/dist/capture/transcript-repair.js +12 -1
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +244 -0
- package/dist/commands/dashboard.d.ts +25 -0
- package/dist/commands/dashboard.js +1175 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +503 -350
- package/dist/commands/setup-remote.js +146 -37
- package/dist/commands/swarm-dashboard.d.ts +20 -0
- package/dist/commands/swarm-dashboard.js +735 -0
- package/dist/commands/swarm-setup.d.ts +10 -0
- package/dist/commands/swarm-setup.js +956 -0
- package/dist/commands/swarm.d.ts +46 -0
- package/dist/commands/swarm.js +441 -0
- package/dist/commands/test-claude.d.ts +16 -0
- package/dist/commands/test-claude.js +156 -0
- package/dist/commands/usage/blocks.d.ts +8 -0
- package/dist/commands/usage/blocks.js +60 -0
- package/dist/commands/usage/daily.d.ts +9 -0
- package/dist/commands/usage/daily.js +96 -0
- package/dist/commands/usage/dashboard.d.ts +8 -0
- package/dist/commands/usage/dashboard.js +104 -0
- package/dist/commands/usage/formatters.d.ts +41 -0
- package/dist/commands/usage/formatters.js +147 -0
- package/dist/commands/usage/index.d.ts +13 -0
- package/dist/commands/usage/index.js +87 -0
- package/dist/commands/usage/monthly.d.ts +8 -0
- package/dist/commands/usage/monthly.js +66 -0
- package/dist/commands/usage/session.d.ts +11 -0
- package/dist/commands/usage/session.js +193 -0
- package/dist/commands/usage/weekly.d.ts +9 -0
- package/dist/commands/usage/weekly.js +61 -0
- package/dist/deploy/instructions.d.ts +5 -2
- package/dist/deploy/instructions.js +11 -8
- package/dist/index.js +256 -20
- package/dist/lib/tmux-scrollbar.d.ts +14 -0
- package/dist/lib/tmux-scrollbar.js +296 -0
- package/dist/lib/usage-parser.d.ts +95 -5
- package/dist/lib/usage-parser.js +416 -71
- package/dist/utils/log-rotate.d.ts +18 -0
- package/dist/utils/log-rotate.js +74 -0
- package/dist/utils/platform.d.ts +2 -0
- package/dist/utils/platform.js +3 -1
- package/dist/utils/session-binding.d.ts +5 -0
- package/dist/utils/session-binding.js +46 -0
- package/dist/utils/state.js +4 -0
- package/dist/utils/verify-remote-terminal.d.ts +10 -0
- package/dist/utils/verify-remote-terminal.js +415 -0
- package/package.json +16 -11
- package/templates/CLAUDE.md +135 -23
- package/templates/cursor-hooks/after-agent-response.sh +0 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
- package/templates/cursor-hooks/stop.sh +0 -0
- package/templates/ekkos-manifest.json +5 -5
- package/templates/hooks/assistant-response.sh +0 -0
- package/templates/hooks/lib/contract.sh +43 -31
- package/templates/hooks/lib/count-tokens.cjs +86 -0
- package/templates/hooks/lib/ekkos-reminders.sh +98 -0
- package/templates/hooks/lib/state.sh +53 -1
- package/templates/hooks/session-start.sh +0 -0
- package/templates/hooks/stop.sh +150 -388
- package/templates/hooks/user-prompt-submit.sh +353 -443
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/windsurf-hooks/README.md +212 -0
- package/templates/windsurf-hooks/hooks.json +9 -2
- package/templates/windsurf-hooks/install.sh +148 -0
- package/templates/windsurf-hooks/lib/contract.sh +2 -0
- package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
- package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
- package/LICENSE +0 -21
- package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ekkos swarm setup
|
|
4
|
+
*
|
|
5
|
+
* Full-screen blessed TUI wizard for configuring and launching a swarm.
|
|
6
|
+
* 4 steps: Task & Workers → Configuration → Model Selection → Review & Launch.
|
|
7
|
+
*
|
|
8
|
+
* Uses manual focus management (blessed FormElement is unreliable).
|
|
9
|
+
* Transitions seamlessly to the swarm dashboard after launch.
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.swarmSetup = swarmSetup;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const blessed = __importStar(require("blessed"));
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
49
|
+
const swarm_1 = require("./swarm");
|
|
50
|
+
// ── Ghostty detection ──
|
|
51
|
+
const GHOSTTY_PATHS = [
|
|
52
|
+
'/Applications/Ghostty.app',
|
|
53
|
+
'/opt/homebrew/bin/ghostty',
|
|
54
|
+
'/usr/local/bin/ghostty',
|
|
55
|
+
];
|
|
56
|
+
function findGhostty() {
|
|
57
|
+
// Check PATH first
|
|
58
|
+
try {
|
|
59
|
+
const result = (0, child_process_1.execSync)('which ghostty 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
60
|
+
if (result)
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
// Check known locations
|
|
65
|
+
for (const p of GHOSTTY_PATHS) {
|
|
66
|
+
if (fs.existsSync(p))
|
|
67
|
+
return p;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Launch ONE Ghostty window with a tiled tmux layout showing all workers + queen.
|
|
73
|
+
*
|
|
74
|
+
* Creates a "workers-view" tmux window with panes. Each pane uses TMUX='' to
|
|
75
|
+
* nest-attach (read-only) to a linked session viewing a specific worker/queen.
|
|
76
|
+
* Opens a single Ghostty window attached to this overview.
|
|
77
|
+
*
|
|
78
|
+
* On macOS, Ghostty's `-e` flag is broken (wraps through /usr/bin/login).
|
|
79
|
+
* Use `--command="..."` config key instead.
|
|
80
|
+
*/
|
|
81
|
+
function launchGhosttyTiled(tmuxSession, workerCount, hasQueen) {
|
|
82
|
+
const ghosttyPath = findGhostty();
|
|
83
|
+
if (!ghosttyPath)
|
|
84
|
+
return;
|
|
85
|
+
const isApp = ghosttyPath.endsWith('.app');
|
|
86
|
+
// Create a linked session for the Ghostty window (independent window selection)
|
|
87
|
+
const ghosttySession = `${tmuxSession}-ghostty`;
|
|
88
|
+
try {
|
|
89
|
+
(0, child_process_1.execSync)(`tmux new-session -d -t "${tmuxSession}" -s "${ghosttySession}"`, { stdio: 'pipe' });
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Create the "workers-view" window with tiled panes
|
|
95
|
+
try {
|
|
96
|
+
(0, child_process_1.execSync)(`tmux new-window -t "${ghosttySession}" -n "workers-view"`, { stdio: 'pipe' });
|
|
97
|
+
(0, child_process_1.execSync)(`tmux select-window -t "${ghosttySession}:workers-view"`, { stdio: 'pipe' });
|
|
98
|
+
// Build the list of linked sessions we need
|
|
99
|
+
const views = [];
|
|
100
|
+
for (let i = 1; i <= workerCount; i++) {
|
|
101
|
+
views.push({ name: `v-w${i}`, tmuxWindow: `worker-${i}` });
|
|
102
|
+
}
|
|
103
|
+
if (hasQueen) {
|
|
104
|
+
views.push({ name: 'v-queen', tmuxWindow: 'queen' });
|
|
105
|
+
}
|
|
106
|
+
// Create linked sessions for each view
|
|
107
|
+
for (const view of views) {
|
|
108
|
+
const linked = `${tmuxSession}-${view.name}`;
|
|
109
|
+
try {
|
|
110
|
+
(0, child_process_1.execSync)(`tmux new-session -d -t "${tmuxSession}" -s "${linked}"`, { stdio: 'pipe' });
|
|
111
|
+
(0, child_process_1.execSync)(`tmux select-window -t "${linked}:${view.tmuxWindow}"`, { stdio: 'pipe' });
|
|
112
|
+
}
|
|
113
|
+
catch { }
|
|
114
|
+
}
|
|
115
|
+
// First pane is already created — set it to worker-1
|
|
116
|
+
const firstLinked = `${tmuxSession}-${views[0].name}`;
|
|
117
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${ghosttySession}:workers-view.0" "TMUX='' tmux attach -r -t '${firstLinked}'" Enter`, { stdio: 'pipe' });
|
|
118
|
+
// Create additional panes for remaining workers + queen
|
|
119
|
+
for (let i = 1; i < views.length; i++) {
|
|
120
|
+
const linked = `${tmuxSession}-${views[i].name}`;
|
|
121
|
+
// Alternate horizontal/vertical splits for a good tiled layout
|
|
122
|
+
const splitFlag = i % 2 === 1 ? '-h' : '-v';
|
|
123
|
+
(0, child_process_1.execSync)(`tmux split-window ${splitFlag} -t "${ghosttySession}:workers-view"`, { stdio: 'pipe' });
|
|
124
|
+
(0, child_process_1.execSync)(`tmux send-keys -t "${ghosttySession}:workers-view.${i}" "TMUX='' tmux attach -r -t '${linked}'" Enter`, { stdio: 'pipe' });
|
|
125
|
+
}
|
|
126
|
+
// Tile all panes evenly
|
|
127
|
+
(0, child_process_1.execSync)(`tmux select-layout -t "${ghosttySession}:workers-view" tiled`, { stdio: 'pipe' });
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// Non-fatal — dashboard still works
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Launch ONE Ghostty window attached to the workers-view
|
|
134
|
+
const attachCmd = `tmux attach -t "${ghosttySession}"`;
|
|
135
|
+
if (isApp) {
|
|
136
|
+
(0, child_process_1.spawn)('open', [
|
|
137
|
+
'-na', ghosttyPath, '--args',
|
|
138
|
+
`--command=${attachCmd}`,
|
|
139
|
+
`--title=ekkOS Swarm Workers`,
|
|
140
|
+
], {
|
|
141
|
+
stdio: 'ignore',
|
|
142
|
+
detached: true,
|
|
143
|
+
}).unref();
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
(0, child_process_1.spawn)(ghosttyPath, [
|
|
147
|
+
`--command=${attachCmd}`,
|
|
148
|
+
`--title=ekkOS Swarm Workers`,
|
|
149
|
+
], {
|
|
150
|
+
stdio: 'ignore',
|
|
151
|
+
detached: true,
|
|
152
|
+
}).unref();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// ── Constants ──
|
|
156
|
+
const LOGO_CHARS = ['e', 'k', 'k', 'O', 'S', '_'];
|
|
157
|
+
const WAVE_COLORS = ['cyan', 'blue', 'magenta', 'yellow', 'green', 'white'];
|
|
158
|
+
const WORKER_COLORS = ['magenta', 'blue', 'green', 'yellow', 'cyan', 'red', 'white', 'gray'];
|
|
159
|
+
const MODEL_TIERS = ['opus', 'sonnet', 'haiku'];
|
|
160
|
+
const QUEEN_STRATEGIES = ['adaptive-default', 'hierarchical-cascade', 'mesh-consensus'];
|
|
161
|
+
const STEP_LABELS = ['Task', 'Config', 'Models', 'Launch'];
|
|
162
|
+
// ── Main export ──
|
|
163
|
+
async function swarmSetup() {
|
|
164
|
+
// ── State ──
|
|
165
|
+
const state = {
|
|
166
|
+
task: '',
|
|
167
|
+
workers: 4,
|
|
168
|
+
decompose: true,
|
|
169
|
+
queen: true,
|
|
170
|
+
bypass: true,
|
|
171
|
+
queenStrategy: 'adaptive-default',
|
|
172
|
+
sameForAll: true,
|
|
173
|
+
globalModel: 'sonnet',
|
|
174
|
+
perWorkerModels: Array(8).fill('sonnet'),
|
|
175
|
+
currentStep: 0,
|
|
176
|
+
focusIndex: 0,
|
|
177
|
+
};
|
|
178
|
+
// Per-step focusable widget arrays
|
|
179
|
+
const stepWidgets = [[], [], [], []];
|
|
180
|
+
// ── Screen init (reuses /dev/tty isolation from dashboard.ts) ──
|
|
181
|
+
let ttyInput;
|
|
182
|
+
let ttyOutput;
|
|
183
|
+
try {
|
|
184
|
+
const ttyFd = fs.openSync('/dev/tty', 'r+');
|
|
185
|
+
ttyInput = new (require('tty').ReadStream)(ttyFd);
|
|
186
|
+
ttyOutput = new (require('tty').WriteStream)(ttyFd);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
ttyInput = process.stdin;
|
|
190
|
+
ttyOutput = process.stdout;
|
|
191
|
+
}
|
|
192
|
+
const screen = blessed.screen({
|
|
193
|
+
smartCSR: true,
|
|
194
|
+
title: 'ekkOS Swarm Setup',
|
|
195
|
+
fullUnicode: true,
|
|
196
|
+
mouse: false,
|
|
197
|
+
grabKeys: false,
|
|
198
|
+
sendFocus: false,
|
|
199
|
+
ignoreLocked: ['C-c'],
|
|
200
|
+
input: ttyInput,
|
|
201
|
+
output: ttyOutput,
|
|
202
|
+
forceUnicode: true,
|
|
203
|
+
terminal: 'xterm-256color',
|
|
204
|
+
resizeTimeout: 300,
|
|
205
|
+
});
|
|
206
|
+
if (screen.program) {
|
|
207
|
+
screen.program.alternateBuffer = () => { };
|
|
208
|
+
screen.program.normalBuffer = () => { };
|
|
209
|
+
if (screen.program.setRawMode) {
|
|
210
|
+
screen.program.setRawMode = (enabled) => enabled;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ── Layout ──
|
|
214
|
+
const W = '100%';
|
|
215
|
+
const HEADER_H = 3;
|
|
216
|
+
const STEPS_H = 3;
|
|
217
|
+
const PREVIEW_H = 5;
|
|
218
|
+
const FOOTER_H = 3;
|
|
219
|
+
const FIXED_H = HEADER_H + STEPS_H + PREVIEW_H + FOOTER_H;
|
|
220
|
+
function calcLayout() {
|
|
221
|
+
const H = Math.max(24, screen.height);
|
|
222
|
+
const formH = Math.max(8, H - FIXED_H);
|
|
223
|
+
return {
|
|
224
|
+
header: { top: 0, height: HEADER_H },
|
|
225
|
+
steps: { top: HEADER_H, height: STEPS_H },
|
|
226
|
+
form: { top: HEADER_H + STEPS_H, height: formH },
|
|
227
|
+
preview: { top: H - PREVIEW_H - FOOTER_H, height: PREVIEW_H },
|
|
228
|
+
footer: { top: H - FOOTER_H, height: FOOTER_H },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
let layout = calcLayout();
|
|
232
|
+
// ── Header box ──
|
|
233
|
+
const headerBox = blessed.box({
|
|
234
|
+
top: layout.header.top, left: 0, width: W, height: layout.header.height,
|
|
235
|
+
content: ' {gray-fg}Swarm Setup Wizard{/gray-fg}',
|
|
236
|
+
tags: true,
|
|
237
|
+
style: { fg: 'white', border: { fg: 'cyan' } },
|
|
238
|
+
border: { type: 'line' },
|
|
239
|
+
label: ' ekkOS_ ',
|
|
240
|
+
});
|
|
241
|
+
// ── Step indicator ──
|
|
242
|
+
const stepsBox = blessed.box({
|
|
243
|
+
top: layout.steps.top, left: 0, width: W, height: layout.steps.height,
|
|
244
|
+
tags: true,
|
|
245
|
+
style: { fg: 'white', border: { fg: 'gray' } },
|
|
246
|
+
border: { type: 'line' },
|
|
247
|
+
});
|
|
248
|
+
// ── Form area (content changes per step) ──
|
|
249
|
+
const formBox = blessed.box({
|
|
250
|
+
top: layout.form.top, left: 0, width: W, height: layout.form.height,
|
|
251
|
+
tags: true,
|
|
252
|
+
style: { fg: 'white', border: { fg: 'cyan' } },
|
|
253
|
+
border: { type: 'line' },
|
|
254
|
+
label: ' Step 1: Task & Workers ',
|
|
255
|
+
});
|
|
256
|
+
// ── Tmux preview ──
|
|
257
|
+
const previewBox = blessed.box({
|
|
258
|
+
top: layout.preview.top, left: 0, width: W, height: layout.preview.height,
|
|
259
|
+
tags: true,
|
|
260
|
+
style: { fg: 'white', border: { fg: 'gray' } },
|
|
261
|
+
border: { type: 'line' },
|
|
262
|
+
label: ' tmux Preview ',
|
|
263
|
+
});
|
|
264
|
+
// ── Footer ──
|
|
265
|
+
const footerBox = blessed.box({
|
|
266
|
+
top: layout.footer.top, left: 0, width: W, height: layout.footer.height,
|
|
267
|
+
tags: true,
|
|
268
|
+
style: { fg: 'white', border: { fg: 'gray' } },
|
|
269
|
+
border: { type: 'line' },
|
|
270
|
+
});
|
|
271
|
+
screen.append(headerBox);
|
|
272
|
+
screen.append(stepsBox);
|
|
273
|
+
screen.append(formBox);
|
|
274
|
+
screen.append(previewBox);
|
|
275
|
+
screen.append(footerBox);
|
|
276
|
+
// ── Error message box (inside form area) ──
|
|
277
|
+
const errorBox = blessed.box({
|
|
278
|
+
parent: formBox,
|
|
279
|
+
bottom: 0, left: 1, width: '100%-4', height: 1,
|
|
280
|
+
tags: true,
|
|
281
|
+
style: { fg: 'red' },
|
|
282
|
+
});
|
|
283
|
+
// ── Logo wave animation ──
|
|
284
|
+
let waveOffset = 0;
|
|
285
|
+
let waveTimer;
|
|
286
|
+
function renderLogoWave() {
|
|
287
|
+
try {
|
|
288
|
+
const coloredChars = LOGO_CHARS.map((ch, i) => {
|
|
289
|
+
const colorIdx = (i + waveOffset) % WAVE_COLORS.length;
|
|
290
|
+
return `{${WAVE_COLORS[colorIdx]}-fg}${ch}{/${WAVE_COLORS[colorIdx]}-fg}`;
|
|
291
|
+
});
|
|
292
|
+
const logoStr = ` ${coloredChars.join('')} `;
|
|
293
|
+
const rightLabel = ' Swarm Setup ';
|
|
294
|
+
const boxW = screen.width - 2;
|
|
295
|
+
const pad = Math.max(1, boxW - (LOGO_CHARS.length + 2) - rightLabel.length);
|
|
296
|
+
headerBox.setLabel(logoStr + '─'.repeat(pad) + `{gray-fg}${rightLabel}{/gray-fg}`);
|
|
297
|
+
waveOffset = (waveOffset + 1) % WAVE_COLORS.length;
|
|
298
|
+
screen.render();
|
|
299
|
+
}
|
|
300
|
+
catch { }
|
|
301
|
+
}
|
|
302
|
+
function scheduleWave() {
|
|
303
|
+
waveTimer = setTimeout(() => {
|
|
304
|
+
renderLogoWave();
|
|
305
|
+
scheduleWave();
|
|
306
|
+
}, 200);
|
|
307
|
+
}
|
|
308
|
+
scheduleWave();
|
|
309
|
+
// ── Helper: render step indicator ──
|
|
310
|
+
function renderStepIndicator() {
|
|
311
|
+
const parts = STEP_LABELS.map((label, i) => {
|
|
312
|
+
if (i < state.currentStep)
|
|
313
|
+
return `{green-fg}[✓ ${label}]{/green-fg}`;
|
|
314
|
+
if (i === state.currentStep)
|
|
315
|
+
return `{cyan-fg}{bold}[${i + 1} ${label}]{/bold}{/cyan-fg}`;
|
|
316
|
+
return `{gray-fg}[${i + 1} ${label}]{/gray-fg}`;
|
|
317
|
+
});
|
|
318
|
+
stepsBox.setContent(' ' + parts.join(' ── '));
|
|
319
|
+
}
|
|
320
|
+
// ── Helper: render tmux preview ──
|
|
321
|
+
function renderPreview() {
|
|
322
|
+
const n = state.workers;
|
|
323
|
+
const cells = ['{white-fg}[0] dash{/white-fg}'];
|
|
324
|
+
for (let i = 0; i < n; i++) {
|
|
325
|
+
const color = WORKER_COLORS[i % WORKER_COLORS.length];
|
|
326
|
+
const model = state.sameForAll ? state.globalModel : state.perWorkerModels[i];
|
|
327
|
+
const m = model[0].toUpperCase(); // O/S/H
|
|
328
|
+
cells.push(`{${color}-fg}[${i + 1}] W${i + 1}:${m}{/${color}-fg}`);
|
|
329
|
+
}
|
|
330
|
+
if (state.queen)
|
|
331
|
+
cells.push('{yellow-fg}[Q] queen{/yellow-fg}');
|
|
332
|
+
previewBox.setContent(' ' + cells.join(' '));
|
|
333
|
+
}
|
|
334
|
+
// ── Helper: render footer ──
|
|
335
|
+
function renderFooter() {
|
|
336
|
+
const isStep0 = state.currentStep === 0;
|
|
337
|
+
const isStep3 = state.currentStep === 3;
|
|
338
|
+
const nav = isStep0 ? '' : '{cyan-fg}←{/cyan-fg}:prev ';
|
|
339
|
+
const navR = isStep3 ? '' : '{cyan-fg}→{/cyan-fg}:next ';
|
|
340
|
+
const action = isStep3 ? '{green-fg}Enter{/green-fg}:launch ' : '';
|
|
341
|
+
footerBox.setContent(` {cyan-fg}Tab{/cyan-fg}:focus ${nav}${navR}${action}{red-fg}q{/red-fg}:quit`);
|
|
342
|
+
}
|
|
343
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
344
|
+
// WIDGET FACTORIES (manual focus management)
|
|
345
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
346
|
+
function createTextInput(opts) {
|
|
347
|
+
const labelBox = blessed.box({
|
|
348
|
+
parent: opts.parent,
|
|
349
|
+
top: opts.top, left: opts.left, width: opts.label.length + 1, height: 1,
|
|
350
|
+
content: `{white-fg}${opts.label}{/white-fg}`,
|
|
351
|
+
tags: true,
|
|
352
|
+
});
|
|
353
|
+
const inputBox = blessed.box({
|
|
354
|
+
parent: opts.parent,
|
|
355
|
+
top: opts.top, left: opts.left + opts.label.length + 1,
|
|
356
|
+
width: opts.width - opts.label.length - 1, height: 3,
|
|
357
|
+
tags: true,
|
|
358
|
+
style: { fg: 'white', border: { fg: 'gray' } },
|
|
359
|
+
border: { type: 'line' },
|
|
360
|
+
});
|
|
361
|
+
let value = opts.value;
|
|
362
|
+
let cursorPos = value.length;
|
|
363
|
+
let isEditing = false;
|
|
364
|
+
function renderInput() {
|
|
365
|
+
const displayW = inputBox.width - 4;
|
|
366
|
+
let display = value;
|
|
367
|
+
if (display.length > displayW) {
|
|
368
|
+
display = display.slice(display.length - displayW);
|
|
369
|
+
}
|
|
370
|
+
if (isEditing) {
|
|
371
|
+
inputBox.setContent(`{cyan-fg}${display}{/cyan-fg}{white-fg}▏{/white-fg}`);
|
|
372
|
+
}
|
|
373
|
+
else if (value.length === 0) {
|
|
374
|
+
inputBox.setContent('{gray-fg}Type your task...{/gray-fg}');
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
inputBox.setContent(`{white-fg}${display}{/white-fg}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
renderInput();
|
|
381
|
+
return {
|
|
382
|
+
element: inputBox,
|
|
383
|
+
type: 'textbox',
|
|
384
|
+
key: 'task',
|
|
385
|
+
onActivate: () => {
|
|
386
|
+
isEditing = !isEditing;
|
|
387
|
+
if (isEditing) {
|
|
388
|
+
inputBox.style.border = { fg: 'green' };
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
inputBox.style.border = { fg: 'cyan' };
|
|
392
|
+
}
|
|
393
|
+
renderInput();
|
|
394
|
+
},
|
|
395
|
+
getValue: () => {
|
|
396
|
+
return { value, isEditing, handleKey: (ch, key) => {
|
|
397
|
+
if (!isEditing)
|
|
398
|
+
return false;
|
|
399
|
+
if (key.name === 'backspace') {
|
|
400
|
+
value = value.slice(0, -1);
|
|
401
|
+
cursorPos = Math.max(0, cursorPos - 1);
|
|
402
|
+
}
|
|
403
|
+
else if (key.name === 'return' || key.name === 'escape') {
|
|
404
|
+
isEditing = false;
|
|
405
|
+
inputBox.style.border = { fg: 'cyan' };
|
|
406
|
+
}
|
|
407
|
+
else if (ch && !key.ctrl && !key.meta && ch.length === 1) {
|
|
408
|
+
value += ch;
|
|
409
|
+
cursorPos = value.length;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
opts.onChange(value);
|
|
415
|
+
state.task = value;
|
|
416
|
+
renderInput();
|
|
417
|
+
return true;
|
|
418
|
+
} };
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function createNumberSelector(opts) {
|
|
423
|
+
const box = blessed.box({
|
|
424
|
+
parent: opts.parent,
|
|
425
|
+
top: opts.top, left: opts.left, width: opts.label.length + 14, height: 3,
|
|
426
|
+
tags: true,
|
|
427
|
+
style: { fg: 'white', border: { fg: 'gray' } },
|
|
428
|
+
border: { type: 'line' },
|
|
429
|
+
});
|
|
430
|
+
let value = opts.value;
|
|
431
|
+
function render() {
|
|
432
|
+
box.setContent(`{white-fg}${opts.label} {cyan-fg}< {bold}${value}{/bold} >{/cyan-fg}{/white-fg}`);
|
|
433
|
+
}
|
|
434
|
+
render();
|
|
435
|
+
return {
|
|
436
|
+
element: box,
|
|
437
|
+
type: 'number',
|
|
438
|
+
key: 'workers',
|
|
439
|
+
onLeft: () => {
|
|
440
|
+
if (value > opts.min) {
|
|
441
|
+
value--;
|
|
442
|
+
opts.onChange(value);
|
|
443
|
+
render();
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
onRight: () => {
|
|
447
|
+
if (value < opts.max) {
|
|
448
|
+
value++;
|
|
449
|
+
opts.onChange(value);
|
|
450
|
+
render();
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
getValue: () => value,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function createToggle(opts) {
|
|
457
|
+
const box = blessed.box({
|
|
458
|
+
parent: opts.parent,
|
|
459
|
+
top: opts.top, left: opts.left, width: opts.label.length + 8, height: 1,
|
|
460
|
+
tags: true,
|
|
461
|
+
style: { fg: 'white' },
|
|
462
|
+
});
|
|
463
|
+
let value = opts.value;
|
|
464
|
+
function render() {
|
|
465
|
+
const check = value ? '{green-fg}[✓]{/green-fg}' : '{gray-fg}[ ]{/gray-fg}';
|
|
466
|
+
box.setContent(`${check} {white-fg}${opts.label}{/white-fg}`);
|
|
467
|
+
}
|
|
468
|
+
render();
|
|
469
|
+
return {
|
|
470
|
+
element: box,
|
|
471
|
+
type: 'toggle',
|
|
472
|
+
key: opts.label,
|
|
473
|
+
onActivate: () => {
|
|
474
|
+
value = !value;
|
|
475
|
+
opts.onChange(value);
|
|
476
|
+
render();
|
|
477
|
+
},
|
|
478
|
+
getValue: () => value,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function createListSelector(opts) {
|
|
482
|
+
const maxItemLen = Math.max(...opts.items.map(s => s.length));
|
|
483
|
+
const box = blessed.box({
|
|
484
|
+
parent: opts.parent,
|
|
485
|
+
top: opts.top, left: opts.left, width: opts.label.length + maxItemLen + 10, height: opts.items.length + 2,
|
|
486
|
+
tags: true,
|
|
487
|
+
style: { fg: 'white', border: { fg: 'gray' } },
|
|
488
|
+
border: { type: 'line' },
|
|
489
|
+
label: ` ${opts.label} `,
|
|
490
|
+
});
|
|
491
|
+
let selected = opts.selectedIndex;
|
|
492
|
+
function render() {
|
|
493
|
+
const lines = opts.items.map((item, i) => {
|
|
494
|
+
const marker = i === selected ? '{cyan-fg}▸{/cyan-fg}' : ' ';
|
|
495
|
+
const style = i === selected ? '{cyan-fg}{bold}' : '{gray-fg}';
|
|
496
|
+
const end = i === selected ? '{/bold}{/cyan-fg}' : '{/gray-fg}';
|
|
497
|
+
return ` ${marker} ${style}${item}${end}`;
|
|
498
|
+
});
|
|
499
|
+
box.setContent(lines.join('\n'));
|
|
500
|
+
}
|
|
501
|
+
render();
|
|
502
|
+
return {
|
|
503
|
+
element: box,
|
|
504
|
+
type: 'list',
|
|
505
|
+
key: opts.label,
|
|
506
|
+
onActivate: () => {
|
|
507
|
+
selected = (selected + 1) % opts.items.length;
|
|
508
|
+
opts.onChange(selected, opts.items[selected]);
|
|
509
|
+
render();
|
|
510
|
+
},
|
|
511
|
+
onLeft: () => {
|
|
512
|
+
selected = (selected - 1 + opts.items.length) % opts.items.length;
|
|
513
|
+
opts.onChange(selected, opts.items[selected]);
|
|
514
|
+
render();
|
|
515
|
+
},
|
|
516
|
+
onRight: () => {
|
|
517
|
+
selected = (selected + 1) % opts.items.length;
|
|
518
|
+
opts.onChange(selected, opts.items[selected]);
|
|
519
|
+
render();
|
|
520
|
+
},
|
|
521
|
+
getValue: () => opts.items[selected],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function createModelCycler(opts) {
|
|
525
|
+
const color = WORKER_COLORS[opts.workerIndex % WORKER_COLORS.length];
|
|
526
|
+
const box = blessed.box({
|
|
527
|
+
parent: opts.parent,
|
|
528
|
+
top: opts.top, left: opts.left, width: 28, height: 1,
|
|
529
|
+
tags: true,
|
|
530
|
+
style: { fg: 'white' },
|
|
531
|
+
});
|
|
532
|
+
let value = opts.value;
|
|
533
|
+
function render() {
|
|
534
|
+
const modelColor = value === 'opus' ? 'magenta' : value === 'sonnet' ? 'blue' : 'green';
|
|
535
|
+
box.setContent(`{${color}-fg}${opts.label}{/${color}-fg} {${modelColor}-fg}{bold}${value}{/bold}{/${modelColor}-fg}`);
|
|
536
|
+
}
|
|
537
|
+
render();
|
|
538
|
+
return {
|
|
539
|
+
element: box,
|
|
540
|
+
type: 'model-cycler',
|
|
541
|
+
key: `worker-${opts.workerIndex}`,
|
|
542
|
+
onActivate: () => {
|
|
543
|
+
const idx = MODEL_TIERS.indexOf(value);
|
|
544
|
+
value = MODEL_TIERS[(idx + 1) % MODEL_TIERS.length];
|
|
545
|
+
opts.onChange(value);
|
|
546
|
+
render();
|
|
547
|
+
},
|
|
548
|
+
getValue: () => value,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function createButton(opts) {
|
|
552
|
+
const box = blessed.box({
|
|
553
|
+
parent: opts.parent,
|
|
554
|
+
top: opts.top, left: opts.left, width: opts.label.length + 6, height: 3,
|
|
555
|
+
tags: true,
|
|
556
|
+
style: { fg: 'white', border: { fg: 'gray' } },
|
|
557
|
+
border: { type: 'line' },
|
|
558
|
+
});
|
|
559
|
+
function render() {
|
|
560
|
+
box.setContent(` {green-fg}{bold}${opts.label}{/bold}{/green-fg}`);
|
|
561
|
+
}
|
|
562
|
+
render();
|
|
563
|
+
return {
|
|
564
|
+
element: box,
|
|
565
|
+
type: 'button',
|
|
566
|
+
key: 'launch',
|
|
567
|
+
onActivate: opts.onPress,
|
|
568
|
+
getValue: () => null,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
572
|
+
// STEP BUILDERS
|
|
573
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
574
|
+
function clearForm() {
|
|
575
|
+
// Remove all children from formBox except errorBox
|
|
576
|
+
const children = formBox.children.slice();
|
|
577
|
+
for (const child of children) {
|
|
578
|
+
if (child !== errorBox)
|
|
579
|
+
formBox.remove(child);
|
|
580
|
+
}
|
|
581
|
+
errorBox.setContent('');
|
|
582
|
+
}
|
|
583
|
+
function setFocus(widgets, index) {
|
|
584
|
+
for (let i = 0; i < widgets.length; i++) {
|
|
585
|
+
const w = widgets[i];
|
|
586
|
+
if (w.element.border) {
|
|
587
|
+
w.element.style.border = { fg: i === index ? 'cyan' : 'gray' };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
state.focusIndex = index;
|
|
591
|
+
}
|
|
592
|
+
// ── Step 0: Task & Workers ──
|
|
593
|
+
function buildStep0() {
|
|
594
|
+
clearForm();
|
|
595
|
+
formBox.setLabel(' Step 1: Task & Workers ');
|
|
596
|
+
const taskInput = createTextInput({
|
|
597
|
+
parent: formBox,
|
|
598
|
+
top: 1, left: 2, width: formBox.width - 6,
|
|
599
|
+
label: 'Task:',
|
|
600
|
+
value: state.task,
|
|
601
|
+
onChange: (val) => { state.task = val; },
|
|
602
|
+
});
|
|
603
|
+
const workerSelector = createNumberSelector({
|
|
604
|
+
parent: formBox,
|
|
605
|
+
top: 5, left: 2,
|
|
606
|
+
label: 'Workers',
|
|
607
|
+
value: state.workers,
|
|
608
|
+
min: 2, max: 8,
|
|
609
|
+
onChange: (val) => {
|
|
610
|
+
state.workers = val;
|
|
611
|
+
renderPreview();
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
stepWidgets[0] = [taskInput, workerSelector];
|
|
615
|
+
state.focusIndex = 0;
|
|
616
|
+
setFocus(stepWidgets[0], 0);
|
|
617
|
+
}
|
|
618
|
+
// ── Step 1: Configuration ──
|
|
619
|
+
function buildStep1() {
|
|
620
|
+
clearForm();
|
|
621
|
+
formBox.setLabel(' Step 2: Configuration ');
|
|
622
|
+
const decomposeToggle = createToggle({
|
|
623
|
+
parent: formBox, top: 1, left: 2,
|
|
624
|
+
label: 'AI Decomposition',
|
|
625
|
+
value: state.decompose,
|
|
626
|
+
onChange: (val) => { state.decompose = val; },
|
|
627
|
+
});
|
|
628
|
+
const queenToggle = createToggle({
|
|
629
|
+
parent: formBox, top: 3, left: 2,
|
|
630
|
+
label: 'Queen Coordinator',
|
|
631
|
+
value: state.queen,
|
|
632
|
+
onChange: (val) => { state.queen = val; renderPreview(); },
|
|
633
|
+
});
|
|
634
|
+
const bypassToggle = createToggle({
|
|
635
|
+
parent: formBox, top: 5, left: 2,
|
|
636
|
+
label: 'Bypass Permissions',
|
|
637
|
+
value: state.bypass,
|
|
638
|
+
onChange: (val) => { state.bypass = val; },
|
|
639
|
+
});
|
|
640
|
+
const strategyList = createListSelector({
|
|
641
|
+
parent: formBox, top: 7, left: 2,
|
|
642
|
+
label: 'Queen Strategy',
|
|
643
|
+
items: [...QUEEN_STRATEGIES],
|
|
644
|
+
selectedIndex: QUEEN_STRATEGIES.indexOf(state.queenStrategy),
|
|
645
|
+
onChange: (idx, val) => { state.queenStrategy = val; },
|
|
646
|
+
});
|
|
647
|
+
stepWidgets[1] = [decomposeToggle, queenToggle, bypassToggle, strategyList];
|
|
648
|
+
state.focusIndex = 0;
|
|
649
|
+
setFocus(stepWidgets[1], 0);
|
|
650
|
+
}
|
|
651
|
+
// ── Step 2: Model Selection ──
|
|
652
|
+
function buildStep2() {
|
|
653
|
+
clearForm();
|
|
654
|
+
formBox.setLabel(' Step 3: Model Selection ');
|
|
655
|
+
const sameToggle = createToggle({
|
|
656
|
+
parent: formBox, top: 1, left: 2,
|
|
657
|
+
label: 'Same model for all workers',
|
|
658
|
+
value: state.sameForAll,
|
|
659
|
+
onChange: (val) => {
|
|
660
|
+
state.sameForAll = val;
|
|
661
|
+
buildStep2(); // rebuild to show/hide per-worker
|
|
662
|
+
renderPreview();
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
const widgets = [sameToggle];
|
|
666
|
+
if (state.sameForAll) {
|
|
667
|
+
const globalList = createListSelector({
|
|
668
|
+
parent: formBox, top: 3, left: 2,
|
|
669
|
+
label: 'Model',
|
|
670
|
+
items: [...MODEL_TIERS],
|
|
671
|
+
selectedIndex: MODEL_TIERS.indexOf(state.globalModel),
|
|
672
|
+
onChange: (idx, val) => {
|
|
673
|
+
state.globalModel = val;
|
|
674
|
+
renderPreview();
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
widgets.push(globalList);
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
for (let i = 0; i < state.workers; i++) {
|
|
681
|
+
const cycler = createModelCycler({
|
|
682
|
+
parent: formBox,
|
|
683
|
+
top: 3 + i, left: 2,
|
|
684
|
+
label: `Worker ${i + 1}:`,
|
|
685
|
+
workerIndex: i,
|
|
686
|
+
value: state.perWorkerModels[i],
|
|
687
|
+
onChange: (val) => {
|
|
688
|
+
state.perWorkerModels[i] = val;
|
|
689
|
+
renderPreview();
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
widgets.push(cycler);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
stepWidgets[2] = widgets;
|
|
696
|
+
state.focusIndex = 0;
|
|
697
|
+
setFocus(stepWidgets[2], 0);
|
|
698
|
+
}
|
|
699
|
+
// ── Step 3: Review & Launch ──
|
|
700
|
+
function buildStep3() {
|
|
701
|
+
clearForm();
|
|
702
|
+
formBox.setLabel(' Step 4: Review & Launch ');
|
|
703
|
+
const modelInfo = state.sameForAll
|
|
704
|
+
? `{white-fg}${state.globalModel}{/white-fg} (all workers)`
|
|
705
|
+
: state.perWorkerModels.slice(0, state.workers).map((m, i) => {
|
|
706
|
+
const c = WORKER_COLORS[i % WORKER_COLORS.length];
|
|
707
|
+
return `{${c}-fg}W${i + 1}:${m}{/${c}-fg}`;
|
|
708
|
+
}).join(' ');
|
|
709
|
+
const lines = [
|
|
710
|
+
` {gray-fg}Task:{/gray-fg} {white-fg}${state.task || '(empty)'}{/white-fg}`,
|
|
711
|
+
` {gray-fg}Workers:{/gray-fg} {cyan-fg}${state.workers}{/cyan-fg}`,
|
|
712
|
+
` {gray-fg}Decompose:{/gray-fg} ${state.decompose ? '{green-fg}ON{/green-fg}' : '{red-fg}OFF{/red-fg}'}`,
|
|
713
|
+
` {gray-fg}Queen:{/gray-fg} ${state.queen ? '{green-fg}ON{/green-fg}' : '{red-fg}OFF{/red-fg}'} {gray-fg}(${state.queenStrategy}){/gray-fg}`,
|
|
714
|
+
` {gray-fg}Bypass:{/gray-fg} ${state.bypass ? '{green-fg}ON{/green-fg}' : '{red-fg}OFF{/red-fg}'}`,
|
|
715
|
+
` {gray-fg}Models:{/gray-fg} ${modelInfo}`,
|
|
716
|
+
];
|
|
717
|
+
const summaryBox = blessed.box({
|
|
718
|
+
parent: formBox,
|
|
719
|
+
top: 1, left: 1, width: '100%-4', height: lines.length + 1,
|
|
720
|
+
tags: true,
|
|
721
|
+
content: lines.join('\n'),
|
|
722
|
+
});
|
|
723
|
+
const launchButton = createButton({
|
|
724
|
+
parent: formBox,
|
|
725
|
+
top: lines.length + 3, left: Math.floor((formBox.width - 18) / 2),
|
|
726
|
+
label: '🚀 Launch Swarm',
|
|
727
|
+
onPress: doLaunch,
|
|
728
|
+
});
|
|
729
|
+
stepWidgets[3] = [launchButton];
|
|
730
|
+
state.focusIndex = 0;
|
|
731
|
+
setFocus(stepWidgets[3], 0);
|
|
732
|
+
}
|
|
733
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
734
|
+
// STEP NAVIGATION
|
|
735
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
736
|
+
const stepBuilders = [buildStep0, buildStep1, buildStep2, buildStep3];
|
|
737
|
+
function goToStep(step) {
|
|
738
|
+
if (step < 0 || step > 3)
|
|
739
|
+
return;
|
|
740
|
+
// Validate current step before advancing
|
|
741
|
+
if (step > state.currentStep && !validateStep(state.currentStep))
|
|
742
|
+
return;
|
|
743
|
+
state.currentStep = step;
|
|
744
|
+
state.focusIndex = 0;
|
|
745
|
+
stepBuilders[step]();
|
|
746
|
+
renderStepIndicator();
|
|
747
|
+
renderPreview();
|
|
748
|
+
renderFooter();
|
|
749
|
+
screen.render();
|
|
750
|
+
}
|
|
751
|
+
function validateStep(step) {
|
|
752
|
+
if (step === 0) {
|
|
753
|
+
if (!state.task || state.task.trim().length === 0) {
|
|
754
|
+
errorBox.setContent('{red-fg}Task description is required{/red-fg}');
|
|
755
|
+
screen.render();
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
errorBox.setContent('');
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
763
|
+
// LAUNCH
|
|
764
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
765
|
+
let launching = false;
|
|
766
|
+
async function doLaunch() {
|
|
767
|
+
if (launching)
|
|
768
|
+
return;
|
|
769
|
+
if (!validateStep(0)) {
|
|
770
|
+
goToStep(0);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
launching = true;
|
|
774
|
+
// Show launching state in form
|
|
775
|
+
clearForm();
|
|
776
|
+
formBox.setLabel(' Launching... ');
|
|
777
|
+
const msgBox = blessed.box({
|
|
778
|
+
parent: formBox,
|
|
779
|
+
top: 2, left: 2, width: '100%-6', height: 3,
|
|
780
|
+
tags: true,
|
|
781
|
+
content: ' {cyan-fg}🚀 Launching swarm with {bold}' + state.workers + '{/bold} workers...{/cyan-fg}\n' +
|
|
782
|
+
' {gray-fg}This may take 15-20 seconds{/gray-fg}',
|
|
783
|
+
});
|
|
784
|
+
screen.render();
|
|
785
|
+
// Build model tiers array
|
|
786
|
+
const modelTiers = [];
|
|
787
|
+
for (let i = 0; i < state.workers; i++) {
|
|
788
|
+
modelTiers.push(state.sameForAll ? state.globalModel : state.perWorkerModels[i]);
|
|
789
|
+
}
|
|
790
|
+
try {
|
|
791
|
+
const tmuxSession = await (0, swarm_1.swarmLaunch)({
|
|
792
|
+
workers: state.workers,
|
|
793
|
+
task: state.task,
|
|
794
|
+
bypass: state.bypass,
|
|
795
|
+
noDecompose: !state.decompose,
|
|
796
|
+
noQueen: !state.queen,
|
|
797
|
+
queenStrategy: state.queenStrategy,
|
|
798
|
+
modelTiers,
|
|
799
|
+
interactive: true,
|
|
800
|
+
});
|
|
801
|
+
// Clean up blessed screen
|
|
802
|
+
clearTimeout(waveTimer);
|
|
803
|
+
screen.destroy();
|
|
804
|
+
if (tmuxSession) {
|
|
805
|
+
// Launch Ghostty windows for workers + queen (if Ghostty available)
|
|
806
|
+
const ghostty = findGhostty();
|
|
807
|
+
if (ghostty) {
|
|
808
|
+
launchGhosttyTiled(tmuxSession, state.workers, state.queen);
|
|
809
|
+
// Small delay for Ghostty windows to spawn before we take over the main terminal
|
|
810
|
+
(0, child_process_1.execSync)('sleep 0.5', { stdio: 'pipe' });
|
|
811
|
+
}
|
|
812
|
+
// Main terminal attaches to the dashboard
|
|
813
|
+
(0, child_process_1.execSync)(`tmux attach -t "${tmuxSession}"`, { stdio: 'inherit' });
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
catch (err) {
|
|
817
|
+
launching = false;
|
|
818
|
+
clearForm();
|
|
819
|
+
formBox.setLabel(' Launch Failed ');
|
|
820
|
+
blessed.box({
|
|
821
|
+
parent: formBox,
|
|
822
|
+
top: 2, left: 2, width: '100%-6', height: 3,
|
|
823
|
+
tags: true,
|
|
824
|
+
content: ` {red-fg}Launch failed: ${err.message}{/red-fg}\n {gray-fg}Press ← to go back{/gray-fg}`,
|
|
825
|
+
});
|
|
826
|
+
screen.render();
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
830
|
+
// KEYBOARD HANDLING
|
|
831
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
832
|
+
screen.on('keypress', (ch, key) => {
|
|
833
|
+
if (!key)
|
|
834
|
+
return;
|
|
835
|
+
// Quit
|
|
836
|
+
if (key.name === 'q' && !key.ctrl && !key.meta) {
|
|
837
|
+
// Don't quit if we're in a text input
|
|
838
|
+
const widgets = stepWidgets[state.currentStep];
|
|
839
|
+
const focused = widgets[state.focusIndex];
|
|
840
|
+
if (focused && focused.type === 'textbox') {
|
|
841
|
+
const data = focused.getValue?.();
|
|
842
|
+
if (data?.isEditing) {
|
|
843
|
+
data.handleKey(ch, key);
|
|
844
|
+
screen.render();
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
clearTimeout(waveTimer);
|
|
849
|
+
screen.destroy();
|
|
850
|
+
process.exit(0);
|
|
851
|
+
}
|
|
852
|
+
if (key.full === 'C-c') {
|
|
853
|
+
clearTimeout(waveTimer);
|
|
854
|
+
screen.destroy();
|
|
855
|
+
process.exit(0);
|
|
856
|
+
}
|
|
857
|
+
if (launching)
|
|
858
|
+
return;
|
|
859
|
+
const widgets = stepWidgets[state.currentStep];
|
|
860
|
+
const focused = widgets[state.focusIndex];
|
|
861
|
+
// Text input passthrough
|
|
862
|
+
if (focused && focused.type === 'textbox') {
|
|
863
|
+
const data = focused.getValue?.();
|
|
864
|
+
if (data?.isEditing) {
|
|
865
|
+
if (key.name === 'tab') {
|
|
866
|
+
// Force exit editing on tab
|
|
867
|
+
data.handleKey('', { name: 'escape' });
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
const handled = data.handleKey(ch, key);
|
|
871
|
+
if (handled) {
|
|
872
|
+
screen.render();
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// Tab / Shift-Tab: cycle focus
|
|
879
|
+
if (key.name === 'tab') {
|
|
880
|
+
if (widgets.length > 0) {
|
|
881
|
+
const next = key.shift
|
|
882
|
+
? (state.focusIndex - 1 + widgets.length) % widgets.length
|
|
883
|
+
: (state.focusIndex + 1) % widgets.length;
|
|
884
|
+
setFocus(widgets, next);
|
|
885
|
+
screen.render();
|
|
886
|
+
}
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
// Arrow left/right: step navigation or widget adjustment
|
|
890
|
+
if (key.name === 'left') {
|
|
891
|
+
if (focused && (focused.type === 'number' || focused.type === 'list')) {
|
|
892
|
+
focused.onLeft?.();
|
|
893
|
+
screen.render();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
// Ctrl-P or left arrow: prev step
|
|
897
|
+
goToStep(state.currentStep - 1);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (key.name === 'right') {
|
|
901
|
+
if (focused && (focused.type === 'number' || focused.type === 'list')) {
|
|
902
|
+
focused.onRight?.();
|
|
903
|
+
screen.render();
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
// Ctrl-N or right arrow: next step
|
|
907
|
+
goToStep(state.currentStep + 1);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
// Ctrl-N / Ctrl-P: always navigate steps
|
|
911
|
+
if (key.full === 'C-n') {
|
|
912
|
+
goToStep(state.currentStep + 1);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (key.full === 'C-p') {
|
|
916
|
+
goToStep(state.currentStep - 1);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
// Up/Down on list widgets
|
|
920
|
+
if (key.name === 'up' && focused && focused.type === 'list') {
|
|
921
|
+
focused.onLeft?.();
|
|
922
|
+
screen.render();
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (key.name === 'down' && focused && focused.type === 'list') {
|
|
926
|
+
focused.onRight?.();
|
|
927
|
+
screen.render();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
// Enter / Space: activate widget
|
|
931
|
+
if (key.name === 'return' || key.name === 'space') {
|
|
932
|
+
if (focused) {
|
|
933
|
+
focused.onActivate?.();
|
|
934
|
+
screen.render();
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
// Resize handler
|
|
940
|
+
screen.on('resize', () => {
|
|
941
|
+
layout = calcLayout();
|
|
942
|
+
headerBox.top = layout.header.top;
|
|
943
|
+
headerBox.height = layout.header.height;
|
|
944
|
+
stepsBox.top = layout.steps.top;
|
|
945
|
+
stepsBox.height = layout.steps.height;
|
|
946
|
+
formBox.top = layout.form.top;
|
|
947
|
+
formBox.height = layout.form.height;
|
|
948
|
+
previewBox.top = layout.preview.top;
|
|
949
|
+
previewBox.height = layout.preview.height;
|
|
950
|
+
footerBox.top = layout.footer.top;
|
|
951
|
+
footerBox.height = layout.footer.height;
|
|
952
|
+
screen.render();
|
|
953
|
+
});
|
|
954
|
+
// ── Initial render ──
|
|
955
|
+
goToStep(0);
|
|
956
|
+
}
|