swarmlancer-cli 0.4.1 → 0.5.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/dist/app.js +152 -133
- package/dist/screens/banner.js +8 -8
- package/dist/screens/dashboard.d.ts +5 -28
- package/dist/screens/dashboard.js +31 -140
- package/dist/screens/menu-screen.d.ts +17 -0
- package/dist/screens/menu-screen.js +59 -0
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { ProcessTerminal, TUI
|
|
1
|
+
import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
|
|
2
2
|
import { getConfig, getAgents, getAgent, saveAgent, deleteAgent, createAgent, migrateLegacyAgent, } from "./config.js";
|
|
3
3
|
import { login } from "./login.js";
|
|
4
4
|
import { setActiveModel, setAgentInstructions } from "./inference.js";
|
|
5
5
|
import { resolveModel, getAvailableModels, isProviderAuthenticated, saveProviderKey, PROVIDERS, } from "./providers.js";
|
|
6
6
|
import { startAgent, stopAgent, sendToServer } from "./agent.js";
|
|
7
7
|
import { colors } from "./theme.js";
|
|
8
|
-
import { BannerComponent } from "./screens/banner.js";
|
|
9
|
-
import { SetupWizardScreen } from "./screens/setup-wizard.js";
|
|
10
8
|
import { DashboardScreen } from "./screens/dashboard.js";
|
|
9
|
+
import { MenuScreen } from "./screens/menu-screen.js";
|
|
11
10
|
import { AgentConfigScreen } from "./screens/agent-config.js";
|
|
12
11
|
import { AgentPickerScreen } from "./screens/agent-picker.js";
|
|
13
12
|
import { AgentEditorScreen } from "./screens/agent-editor.js";
|
|
@@ -16,11 +15,11 @@ import { SettingsScreen } from "./screens/settings.js";
|
|
|
16
15
|
import { DiscoverySettingsScreen } from "./screens/discovery-settings.js";
|
|
17
16
|
import { ModelPickerScreen } from "./screens/model-picker.js";
|
|
18
17
|
import { NameEditorScreen } from "./screens/name-editor.js";
|
|
19
|
-
import { SessionGoalScreen } from "./screens/session-goal.js";
|
|
20
18
|
import { MessageScreen } from "./screens/message.js";
|
|
21
19
|
import { screenProfiles } from "./screening.js";
|
|
22
20
|
let terminal;
|
|
23
21
|
let tui;
|
|
22
|
+
let runningAgentId; // tracks which agent is online (⚡)
|
|
24
23
|
function setScreen(component) {
|
|
25
24
|
tui.clear();
|
|
26
25
|
tui.addChild(component);
|
|
@@ -34,69 +33,29 @@ function showMessage(title, lines, style = "info") {
|
|
|
34
33
|
setScreen(screen);
|
|
35
34
|
});
|
|
36
35
|
}
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
function confirm(question) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const items = [
|
|
39
|
+
{ value: "yes", label: "Yes" },
|
|
40
|
+
{ value: "no", label: "No" },
|
|
41
|
+
];
|
|
42
|
+
const screen = new MenuScreen(tui, question, items);
|
|
43
|
+
screen.onSelect = (v) => resolve(v === "yes");
|
|
44
|
+
screen.onBack = () => resolve(false);
|
|
45
|
+
setScreen(screen);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// ── Silent setup ──────────────────────────────────────────
|
|
49
|
+
async function silentSetup() {
|
|
39
50
|
migrateLegacyAgent();
|
|
40
|
-
|
|
41
|
-
const wizard = new SetupWizardScreen(tui);
|
|
42
|
-
const wrapper = new Container();
|
|
43
|
-
wrapper.addChild(banner);
|
|
44
|
-
wrapper.addChild(wizard);
|
|
45
|
-
tui.clear();
|
|
46
|
-
tui.addChild(wrapper);
|
|
47
|
-
tui.setFocus(wizard);
|
|
48
|
-
tui.requestRender(true);
|
|
49
|
-
// Step 1: Auth
|
|
51
|
+
// Auto-login if not logged in
|
|
50
52
|
const config = getConfig();
|
|
51
|
-
|
|
52
|
-
if (config.token) {
|
|
53
|
-
wizard.setStep(0, "done", "logged in");
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
wizard.setStep(0, "running", "not logged in — opening browser...");
|
|
53
|
+
if (!config.token) {
|
|
57
54
|
try {
|
|
58
55
|
await login();
|
|
59
|
-
wizard.setStep(0, "done", "logged in");
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
wizard.setStep(0, "failed", err instanceof Error ? err.message : "login failed");
|
|
63
56
|
}
|
|
57
|
+
catch { }
|
|
64
58
|
}
|
|
65
|
-
// Step 2: Agents
|
|
66
|
-
wizard.setStep(1, "running");
|
|
67
|
-
const agents = getAgents();
|
|
68
|
-
if (agents.length > 0) {
|
|
69
|
-
wizard.setStep(1, "done", `${agents.length} agent${agents.length === 1 ? "" : "s"}`);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
wizard.setStep(1, "skipped", "no agents yet");
|
|
73
|
-
const shouldCreate = await wizard.askConfirm("You have no agents. Create one now?");
|
|
74
|
-
if (shouldCreate) {
|
|
75
|
-
const name = await askName("New Agent Name", "Give your first agent a name.");
|
|
76
|
-
if (name) {
|
|
77
|
-
createAgent(name);
|
|
78
|
-
wizard.setStep(1, "done", "1 agent created");
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
wizard.setStep(1, "skipped", "skipped");
|
|
82
|
-
}
|
|
83
|
-
tui.clear();
|
|
84
|
-
tui.addChild(wrapper);
|
|
85
|
-
tui.setFocus(wizard);
|
|
86
|
-
tui.requestRender(true);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// Step 3: Providers
|
|
90
|
-
wizard.setStep(2, "running");
|
|
91
|
-
const models = getAvailableModels();
|
|
92
|
-
if (models.length > 0) {
|
|
93
|
-
const providers = [...new Set(models.map((m) => m.provider))];
|
|
94
|
-
wizard.setStep(2, "done", providers.join(", "));
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
wizard.setStep(2, "skipped", "no API keys — add in Swarm → Providers");
|
|
98
|
-
}
|
|
99
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
100
59
|
}
|
|
101
60
|
// ── Sub-screens ───────────────────────────────────────────
|
|
102
61
|
function askName(title, subtitle, current = "") {
|
|
@@ -165,19 +124,10 @@ function editModel(agent) {
|
|
|
165
124
|
setScreen(screen);
|
|
166
125
|
});
|
|
167
126
|
}
|
|
168
|
-
function askSessionGoal() {
|
|
169
|
-
return new Promise((resolve) => {
|
|
170
|
-
const screen = new SessionGoalScreen(tui);
|
|
171
|
-
screen.onSubmit = (goal) => resolve(goal.trim());
|
|
172
|
-
screen.onSkip = () => resolve("");
|
|
173
|
-
setScreen(screen);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
127
|
function pickAgent() {
|
|
177
128
|
const agents = getAgents();
|
|
178
|
-
if (agents.length === 0)
|
|
129
|
+
if (agents.length === 0)
|
|
179
130
|
return showMessage("No agents", ["Create an agent first."], "error").then(() => null);
|
|
180
|
-
}
|
|
181
131
|
if (agents.length === 1)
|
|
182
132
|
return Promise.resolve(agents[0]);
|
|
183
133
|
return new Promise((resolve) => {
|
|
@@ -187,24 +137,6 @@ function pickAgent() {
|
|
|
187
137
|
setScreen(screen);
|
|
188
138
|
});
|
|
189
139
|
}
|
|
190
|
-
// ── Provider login ────────────────────────────────────────
|
|
191
|
-
async function handleProviderLogin(providerId) {
|
|
192
|
-
const provider = PROVIDERS.find((p) => p.id === providerId);
|
|
193
|
-
if (!provider)
|
|
194
|
-
return;
|
|
195
|
-
if (!provider.keyBased) {
|
|
196
|
-
await showMessage("Coming soon", [`${provider.label} is not yet supported.`], "info");
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (isProviderAuthenticated(provider.id)) {
|
|
200
|
-
await showMessage("Already configured", [`${provider.label} is already logged in.`, "", "To change the API key, re-enter it below."], "info");
|
|
201
|
-
}
|
|
202
|
-
const key = await askName(`${provider.label}`, "Paste your API key:");
|
|
203
|
-
if (key) {
|
|
204
|
-
saveProviderKey(provider.id, key);
|
|
205
|
-
await showMessage("Saved", [`${provider.label} API key saved.`], "success");
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
140
|
// ── Agent config ──────────────────────────────────────────
|
|
209
141
|
async function runAgentConfig(agentId) {
|
|
210
142
|
while (true) {
|
|
@@ -218,7 +150,7 @@ async function runAgentConfig(agentId) {
|
|
|
218
150
|
});
|
|
219
151
|
switch (action) {
|
|
220
152
|
case "edit-name": {
|
|
221
|
-
const newName = await askName("Rename Agent", "Enter a new name
|
|
153
|
+
const newName = await askName("Rename Agent", "Enter a new name.", agent.name);
|
|
222
154
|
if (newName) {
|
|
223
155
|
agent.name = newName;
|
|
224
156
|
saveAgent(agent);
|
|
@@ -238,15 +170,114 @@ async function runAgentConfig(agentId) {
|
|
|
238
170
|
await editModel(agent);
|
|
239
171
|
break;
|
|
240
172
|
case "delete": {
|
|
241
|
-
await
|
|
242
|
-
|
|
243
|
-
|
|
173
|
+
const sure = await confirm(`Delete "${agent.name}"?`);
|
|
174
|
+
if (sure) {
|
|
175
|
+
deleteAgent(agent.id);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
244
179
|
}
|
|
245
180
|
case "back": return;
|
|
246
181
|
}
|
|
247
182
|
}
|
|
248
183
|
}
|
|
249
|
-
// ──
|
|
184
|
+
// ── Swarm screen ──────────────────────────────────────────
|
|
185
|
+
async function runManageTop() {
|
|
186
|
+
while (true) {
|
|
187
|
+
const action = await new Promise((resolve) => {
|
|
188
|
+
const items = [
|
|
189
|
+
{ value: "swarm", label: "Swarm", description: "" },
|
|
190
|
+
{ value: "providers", label: "Providers", description: "" },
|
|
191
|
+
];
|
|
192
|
+
const screen = new MenuScreen(tui, "Manage", items);
|
|
193
|
+
screen.onSelect = (v) => resolve(v);
|
|
194
|
+
screen.onBack = () => resolve(null);
|
|
195
|
+
setScreen(screen);
|
|
196
|
+
});
|
|
197
|
+
if (!action)
|
|
198
|
+
return;
|
|
199
|
+
switch (action) {
|
|
200
|
+
case "swarm":
|
|
201
|
+
await runSwarm();
|
|
202
|
+
break;
|
|
203
|
+
case "providers":
|
|
204
|
+
await runProviders();
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ── Manage screen ─────────────────────────────────────────
|
|
210
|
+
async function runSwarm() {
|
|
211
|
+
while (true) {
|
|
212
|
+
const agents = getAgents();
|
|
213
|
+
const items = [
|
|
214
|
+
{ value: "__create__", label: "+ Create", description: "" },
|
|
215
|
+
];
|
|
216
|
+
for (const agent of agents) {
|
|
217
|
+
const isRunning = agent.id === runningAgentId;
|
|
218
|
+
const suffix = isRunning ? " [⚡]" : "";
|
|
219
|
+
items.push({
|
|
220
|
+
value: agent.id,
|
|
221
|
+
label: `${agent.name}${suffix}`,
|
|
222
|
+
description: "",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const action = await new Promise((resolve) => {
|
|
226
|
+
const screen = new MenuScreen(tui, "Swarm", items);
|
|
227
|
+
screen.onSelect = (v) => resolve(v);
|
|
228
|
+
screen.onBack = () => resolve(null);
|
|
229
|
+
setScreen(screen);
|
|
230
|
+
});
|
|
231
|
+
if (!action)
|
|
232
|
+
return;
|
|
233
|
+
if (action === "__create__") {
|
|
234
|
+
const name = await askName("New Agent", "Give your agent a name.");
|
|
235
|
+
if (name) {
|
|
236
|
+
const agent = createAgent(name);
|
|
237
|
+
await runAgentConfig(agent.id);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
await runAgentConfig(action);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ── Providers screen ──────────────────────────────────────
|
|
246
|
+
async function runProviders() {
|
|
247
|
+
while (true) {
|
|
248
|
+
const items = PROVIDERS.map((p) => {
|
|
249
|
+
const loggedIn = isProviderAuthenticated(p.id);
|
|
250
|
+
const prefix = loggedIn ? "→ " : " ";
|
|
251
|
+
const suffix = loggedIn ? " ✓ logged in" : "";
|
|
252
|
+
return {
|
|
253
|
+
value: p.id,
|
|
254
|
+
label: `${prefix}${p.label}${suffix}`,
|
|
255
|
+
description: "",
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
const action = await new Promise((resolve) => {
|
|
259
|
+
const screen = new MenuScreen(tui, "Providers", items);
|
|
260
|
+
screen.onSelect = (v) => resolve(v);
|
|
261
|
+
screen.onBack = () => resolve(null);
|
|
262
|
+
setScreen(screen);
|
|
263
|
+
});
|
|
264
|
+
if (!action)
|
|
265
|
+
return;
|
|
266
|
+
const provider = PROVIDERS.find((p) => p.id === action);
|
|
267
|
+
if (!provider)
|
|
268
|
+
continue;
|
|
269
|
+
if (!provider.keyBased) {
|
|
270
|
+
await showMessage("Coming soon", [`${provider.label} is not yet supported.`], "info");
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const key = await askName(provider.label, "Paste your API key:");
|
|
274
|
+
if (key) {
|
|
275
|
+
saveProviderKey(provider.id, key);
|
|
276
|
+
await showMessage("Saved", [`${provider.label} API key saved.`], "success");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// ── Go online ─────────────────────────────────────────────
|
|
250
281
|
async function fetchCandidates(discovery) {
|
|
251
282
|
const config = getConfig();
|
|
252
283
|
const params = new URLSearchParams();
|
|
@@ -265,14 +296,17 @@ async function fetchCandidates(discovery) {
|
|
|
265
296
|
});
|
|
266
297
|
if (!res.ok)
|
|
267
298
|
throw new Error(`Discover API error: ${res.status}`);
|
|
268
|
-
|
|
269
|
-
return data.profiles;
|
|
299
|
+
return (await res.json()).profiles;
|
|
270
300
|
}
|
|
271
|
-
async function
|
|
301
|
+
async function goOnline() {
|
|
302
|
+
const conf = getConfig();
|
|
303
|
+
if (!conf.token) {
|
|
304
|
+
await showMessage("Not logged in", ["Run login first."], "error");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
272
307
|
const agent = await pickAgent();
|
|
273
308
|
if (!agent)
|
|
274
309
|
return;
|
|
275
|
-
// Resolve model
|
|
276
310
|
const resolved = resolveModel(agent.modelPattern);
|
|
277
311
|
if (!resolved) {
|
|
278
312
|
await showMessage("No model", ["No provider API keys configured.", "Add one in Swarm → Providers."], "error");
|
|
@@ -280,18 +314,14 @@ async function runAgentSession() {
|
|
|
280
314
|
}
|
|
281
315
|
setActiveModel(resolved.provider, resolved.model);
|
|
282
316
|
setAgentInstructions(agent.instructions);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
id: resolved.model,
|
|
286
|
-
name: resolved.model,
|
|
287
|
-
};
|
|
288
|
-
const sessionGoal = "";
|
|
317
|
+
runningAgentId = agent.id;
|
|
318
|
+
const modelInfo = { provider: resolved.provider, id: resolved.model, name: resolved.model };
|
|
289
319
|
const config = getConfig();
|
|
290
320
|
const discovery = agent.discovery;
|
|
291
321
|
const limits = agent.limits;
|
|
292
|
-
const screen = new AgentRunningScreen(tui, modelInfo, config.serverUrl, agent.name
|
|
322
|
+
const screen = new AgentRunningScreen(tui, modelInfo, config.serverUrl, agent.name);
|
|
293
323
|
const done = new Promise((resolve) => {
|
|
294
|
-
screen.onStop = () => { stopAgent(); resolve(); };
|
|
324
|
+
screen.onStop = () => { stopAgent(); runningAgentId = undefined; resolve(); };
|
|
295
325
|
});
|
|
296
326
|
setScreen(screen);
|
|
297
327
|
const log = (line) => screen.addLog(line);
|
|
@@ -307,7 +337,7 @@ async function runAgentSession() {
|
|
|
307
337
|
else {
|
|
308
338
|
log(colors.lime(`📡 Found ${candidates.length} candidates, screening...`));
|
|
309
339
|
screen.setStatus(`screening ${candidates.length} profiles...`);
|
|
310
|
-
const results = await screenProfiles(candidates, agent.instructions,
|
|
340
|
+
const results = await screenProfiles(candidates, agent.instructions, "", discovery.matchThreshold, discovery.maxScreenPerSession, (screened, total, result) => {
|
|
311
341
|
const icon = result.score >= discovery.matchThreshold ? colors.lime("✓") : colors.gray("⊘");
|
|
312
342
|
const scoreStr = result.score >= discovery.matchThreshold
|
|
313
343
|
? colors.lime(`${result.score}/10`) : colors.gray(`${result.score}/10`);
|
|
@@ -321,13 +351,12 @@ async function runAgentSession() {
|
|
|
321
351
|
}
|
|
322
352
|
else {
|
|
323
353
|
log(colors.lime(`Found ${matches.length} match${matches.length === 1 ? "" : "es"}. Starting conversations...`));
|
|
324
|
-
log("");
|
|
325
354
|
for (const match of matches) {
|
|
326
355
|
if (!match.profile.online) {
|
|
327
356
|
log(colors.gray(` ⊘ @${match.profile.githubUsername} offline`));
|
|
328
357
|
continue;
|
|
329
358
|
}
|
|
330
|
-
log(colors.lime(` 🤝 @${match.profile.githubUsername} (${match.score}/10
|
|
359
|
+
log(colors.lime(` 🤝 @${match.profile.githubUsername} (${match.score}/10)`));
|
|
331
360
|
sendToServer({ type: "start_conversation", withUserId: match.profile.id });
|
|
332
361
|
if (limits.cooldownSeconds > 0)
|
|
333
362
|
await new Promise((r) => setTimeout(r, limits.cooldownSeconds * 1000));
|
|
@@ -345,39 +374,29 @@ async function runAgentSession() {
|
|
|
345
374
|
// ── Dashboard loop ────────────────────────────────────────
|
|
346
375
|
async function runDashboard() {
|
|
347
376
|
while (true) {
|
|
348
|
-
const agents = getAgents();
|
|
349
377
|
const action = await new Promise((resolve) => {
|
|
350
|
-
const dashboard = new DashboardScreen(tui
|
|
378
|
+
const dashboard = new DashboardScreen(tui);
|
|
351
379
|
dashboard.onAction = resolve;
|
|
352
380
|
setScreen(dashboard);
|
|
353
381
|
});
|
|
354
|
-
switch (action
|
|
382
|
+
switch (action) {
|
|
355
383
|
case "start": {
|
|
356
|
-
const
|
|
357
|
-
if (
|
|
358
|
-
await
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
await runAgentSession();
|
|
384
|
+
const sure = await confirm("Go online?");
|
|
385
|
+
if (sure)
|
|
386
|
+
await goOnline();
|
|
362
387
|
break;
|
|
363
388
|
}
|
|
364
|
-
case "
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
389
|
+
case "manage":
|
|
390
|
+
await runManageTop();
|
|
391
|
+
break;
|
|
392
|
+
case "exit": {
|
|
393
|
+
const sure = await confirm("Exit?");
|
|
394
|
+
if (sure) {
|
|
395
|
+
shutdown();
|
|
396
|
+
return;
|
|
369
397
|
}
|
|
370
398
|
break;
|
|
371
399
|
}
|
|
372
|
-
case "edit-agent":
|
|
373
|
-
await runAgentConfig(action.agentId);
|
|
374
|
-
break;
|
|
375
|
-
case "provider":
|
|
376
|
-
await handleProviderLogin(action.providerId);
|
|
377
|
-
break;
|
|
378
|
-
case "quit":
|
|
379
|
-
shutdown();
|
|
380
|
-
return;
|
|
381
400
|
}
|
|
382
401
|
}
|
|
383
402
|
}
|
|
@@ -397,6 +416,6 @@ export async function runInteractive() {
|
|
|
397
416
|
}
|
|
398
417
|
return undefined;
|
|
399
418
|
});
|
|
400
|
-
await
|
|
419
|
+
await silentSetup();
|
|
401
420
|
await runDashboard();
|
|
402
421
|
}
|
package/dist/screens/banner.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Container, Text, Spacer } from "@mariozechner/pi-tui";
|
|
2
2
|
import { colors } from "../theme.js";
|
|
3
3
|
const BANNER_ART = [
|
|
4
|
-
`
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
`
|
|
8
|
-
`
|
|
9
|
-
|
|
10
|
-
`
|
|
4
|
+
` █████████ ███`,
|
|
5
|
+
`███ ███ ███`,
|
|
6
|
+
`███ ███`,
|
|
7
|
+
` █████████ ███`,
|
|
8
|
+
` ███ ███`,
|
|
9
|
+
`███ ███ ███`,
|
|
10
|
+
` █████████ ███`,
|
|
11
11
|
];
|
|
12
12
|
export class BannerComponent extends Container {
|
|
13
13
|
constructor() {
|
|
@@ -21,7 +21,7 @@ export class BannerComponent extends Container {
|
|
|
21
21
|
this.addChild(new Text(colors.limeBold(` ${line}`), 1, 0));
|
|
22
22
|
}
|
|
23
23
|
this.addChild(new Spacer(0));
|
|
24
|
-
this.addChild(new Text(colors.gray(" LET THE SWARM BEGIN!"), 1, 0));
|
|
24
|
+
this.addChild(new Text(colors.gray(" [LET THE SWARM BEGIN!]"), 1, 0));
|
|
25
25
|
this.addChild(new Spacer(1));
|
|
26
26
|
}
|
|
27
27
|
invalidate() {
|
|
@@ -1,35 +1,12 @@
|
|
|
1
1
|
import { type Component } from "@mariozechner/pi-tui";
|
|
2
2
|
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
-
|
|
4
|
-
export type MenuAction = {
|
|
5
|
-
type: "start";
|
|
6
|
-
} | {
|
|
7
|
-
type: "create-agent";
|
|
8
|
-
} | {
|
|
9
|
-
type: "edit-agent";
|
|
10
|
-
agentId: string;
|
|
11
|
-
} | {
|
|
12
|
-
type: "provider";
|
|
13
|
-
providerId: string;
|
|
14
|
-
} | {
|
|
15
|
-
type: "quit";
|
|
16
|
-
};
|
|
17
|
-
export interface DashboardData {
|
|
18
|
-
agents: AgentProfile[];
|
|
19
|
-
}
|
|
3
|
+
export type DashboardAction = "start" | "manage" | "exit";
|
|
20
4
|
export declare class DashboardScreen implements Component {
|
|
5
|
+
private container;
|
|
6
|
+
private selectList;
|
|
21
7
|
private tui;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
private flat;
|
|
25
|
-
private selectableIndices;
|
|
26
|
-
private cursor;
|
|
27
|
-
private cachedRender?;
|
|
28
|
-
onAction?: (action: MenuAction) => void;
|
|
29
|
-
constructor(tui: TUI, data: DashboardData);
|
|
30
|
-
private buildTree;
|
|
31
|
-
private rebuildFlat;
|
|
32
|
-
private flattenTree;
|
|
8
|
+
onAction?: (action: DashboardAction) => void;
|
|
9
|
+
constructor(tui: TUI);
|
|
33
10
|
handleInput(data: string): void;
|
|
34
11
|
render(width: number): string[];
|
|
35
12
|
invalidate(): void;
|
|
@@ -1,160 +1,51 @@
|
|
|
1
|
-
import { matchesKey, Key,
|
|
1
|
+
import { Container, Text, Spacer, SelectList, matchesKey, Key, } from "@mariozechner/pi-tui";
|
|
2
2
|
import { colors, theme } from "../theme.js";
|
|
3
3
|
import { BannerComponent } from "./banner.js";
|
|
4
|
-
|
|
4
|
+
const MENU_ITEMS = [
|
|
5
|
+
{ value: "start", label: "Start", description: "" },
|
|
6
|
+
{ value: "manage", label: "Manage", description: "" },
|
|
7
|
+
{ value: "exit", label: "Exit", description: "" },
|
|
8
|
+
];
|
|
5
9
|
export class DashboardScreen {
|
|
10
|
+
container;
|
|
11
|
+
selectList;
|
|
6
12
|
tui;
|
|
7
|
-
banner;
|
|
8
|
-
tree;
|
|
9
|
-
flat = [];
|
|
10
|
-
selectableIndices = [];
|
|
11
|
-
cursor = 0;
|
|
12
|
-
cachedRender;
|
|
13
13
|
onAction;
|
|
14
|
-
constructor(tui
|
|
14
|
+
constructor(tui) {
|
|
15
15
|
this.tui = tui;
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
action: { type: "edit-agent", agentId: agent.id },
|
|
32
|
-
expanded: false,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
// Provider children
|
|
36
|
-
const providerChildren = [
|
|
37
|
-
{ label: "Select provider to login:", indent: 0, isLabel: true, expanded: false },
|
|
38
|
-
];
|
|
39
|
-
for (const p of PROVIDERS) {
|
|
40
|
-
const loggedIn = isProviderAuthenticated(p.id);
|
|
41
|
-
const prefix = loggedIn ? "→ " : " ";
|
|
42
|
-
const suffix = loggedIn ? " ✓ logged in" : "";
|
|
43
|
-
providerChildren.push({
|
|
44
|
-
label: `${prefix}${p.label}${suffix}`,
|
|
45
|
-
indent: 0,
|
|
46
|
-
action: { type: "provider", providerId: p.id },
|
|
47
|
-
expanded: false,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
return [
|
|
51
|
-
{ label: "Go online", indent: 0, action: { type: "start" }, expanded: false },
|
|
52
|
-
{
|
|
53
|
-
label: "Swarm",
|
|
54
|
-
indent: 0,
|
|
55
|
-
expanded: false,
|
|
56
|
-
children: [
|
|
57
|
-
{
|
|
58
|
-
label: "Manage",
|
|
59
|
-
indent: 0,
|
|
60
|
-
expanded: false,
|
|
61
|
-
children: manageChildren,
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
label: "Providers",
|
|
65
|
-
indent: 0,
|
|
66
|
-
expanded: false,
|
|
67
|
-
children: providerChildren,
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
},
|
|
71
|
-
{ label: "Exit", indent: 0, action: { type: "quit" }, expanded: false },
|
|
72
|
-
];
|
|
73
|
-
}
|
|
74
|
-
rebuildFlat() {
|
|
75
|
-
this.flat = [];
|
|
76
|
-
this.flattenTree(this.tree, 0);
|
|
77
|
-
this.selectableIndices = this.flat
|
|
78
|
-
.map((item, i) => (!item.node.isLabel ? i : -1))
|
|
79
|
-
.filter((i) => i >= 0);
|
|
80
|
-
if (this.cursor >= this.selectableIndices.length) {
|
|
81
|
-
this.cursor = Math.max(0, this.selectableIndices.length - 1);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
flattenTree(nodes, indent) {
|
|
85
|
-
for (const node of nodes) {
|
|
86
|
-
this.flat.push({ node, indent });
|
|
87
|
-
if (node.children && node.expanded) {
|
|
88
|
-
this.flattenTree(node.children, indent + 1);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
16
|
+
this.container = new Container();
|
|
17
|
+
this.container.addChild(new BannerComponent());
|
|
18
|
+
this.selectList = new SelectList(MENU_ITEMS, MENU_ITEMS.length, {
|
|
19
|
+
selectedPrefix: (t) => theme.accent(t),
|
|
20
|
+
selectedText: (t) => theme.accent(t),
|
|
21
|
+
description: (t) => colors.gray(t),
|
|
22
|
+
scrollInfo: (t) => colors.gray(t),
|
|
23
|
+
noMatch: (t) => colors.gray(t),
|
|
24
|
+
});
|
|
25
|
+
this.selectList.onSelect = (item) => {
|
|
26
|
+
this.onAction?.(item.value);
|
|
27
|
+
};
|
|
28
|
+
this.container.addChild(this.selectList);
|
|
29
|
+
this.container.addChild(new Spacer(1));
|
|
30
|
+
this.container.addChild(new Text(colors.gray(" ↑↓ navigate • →/enter select • q quit"), 1, 0));
|
|
91
31
|
}
|
|
92
32
|
handleInput(data) {
|
|
93
33
|
if (matchesKey(data, "q")) {
|
|
94
|
-
this.onAction?.(
|
|
34
|
+
this.onAction?.("exit");
|
|
95
35
|
return;
|
|
96
36
|
}
|
|
97
|
-
if (matchesKey(data, Key.
|
|
98
|
-
|
|
99
|
-
this.cursor--;
|
|
100
|
-
this.cachedRender = undefined;
|
|
37
|
+
if (matchesKey(data, Key.right)) {
|
|
38
|
+
this.selectList.handleInput("\r");
|
|
101
39
|
this.tui.requestRender();
|
|
102
40
|
return;
|
|
103
41
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this.cursor++;
|
|
107
|
-
this.cachedRender = undefined;
|
|
108
|
-
this.tui.requestRender();
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (matchesKey(data, Key.enter)) {
|
|
112
|
-
const itemIdx = this.selectableIndices[this.cursor];
|
|
113
|
-
const item = this.flat[itemIdx];
|
|
114
|
-
if (item.node.children) {
|
|
115
|
-
// Branch: toggle expand/collapse
|
|
116
|
-
item.node.expanded = !item.node.expanded;
|
|
117
|
-
this.rebuildFlat();
|
|
118
|
-
this.cachedRender = undefined;
|
|
119
|
-
this.tui.requestRender();
|
|
120
|
-
}
|
|
121
|
-
else if (item.node.action) {
|
|
122
|
-
// Leaf: trigger action
|
|
123
|
-
this.onAction?.(item.node.action);
|
|
124
|
-
}
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
42
|
+
this.selectList.handleInput(data);
|
|
43
|
+
this.tui.requestRender();
|
|
127
44
|
}
|
|
128
45
|
render(width) {
|
|
129
|
-
|
|
130
|
-
return this.cachedRender;
|
|
131
|
-
const lines = this.banner.render(width);
|
|
132
|
-
const selectedFlatIdx = this.selectableIndices[this.cursor];
|
|
133
|
-
for (let i = 0; i < this.flat.length; i++) {
|
|
134
|
-
const { node, indent } = this.flat[i];
|
|
135
|
-
const pad = " ".repeat(indent + 1);
|
|
136
|
-
const isSelected = i === selectedFlatIdx;
|
|
137
|
-
if (node.isLabel) {
|
|
138
|
-
// Non-selectable label
|
|
139
|
-
lines.push(truncateToWidth(`${pad} ${colors.gray(node.label)}`, width));
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const isBranch = !!node.children;
|
|
143
|
-
const arrow = isBranch ? (node.expanded ? " ▾" : " ▸") : "";
|
|
144
|
-
const label = `${node.label}${arrow}`;
|
|
145
|
-
if (isSelected) {
|
|
146
|
-
lines.push(truncateToWidth(`${pad}${theme.accent(`▸ ${label}`)}`, width));
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
lines.push(truncateToWidth(`${pad} ${label}`, width));
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
lines.push("");
|
|
153
|
-
lines.push(truncateToWidth(colors.gray(" ↑↓ navigate • enter expand/select • q quit"), width));
|
|
154
|
-
this.cachedRender = lines;
|
|
155
|
-
return lines;
|
|
46
|
+
return this.container.render(width);
|
|
156
47
|
}
|
|
157
48
|
invalidate() {
|
|
158
|
-
this.
|
|
49
|
+
this.container.invalidate();
|
|
159
50
|
}
|
|
160
51
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SelectItem, type Component } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
+
/**
|
|
4
|
+
* Reusable menu screen with title, items, and left/right/enter/esc navigation.
|
|
5
|
+
* Enter or Right = select, Left or Esc = back.
|
|
6
|
+
*/
|
|
7
|
+
export declare class MenuScreen implements Component {
|
|
8
|
+
private container;
|
|
9
|
+
private selectList;
|
|
10
|
+
private tui;
|
|
11
|
+
onSelect?: (value: string) => void;
|
|
12
|
+
onBack?: () => void;
|
|
13
|
+
constructor(tui: TUI, title: string, items: SelectItem[], subtitle?: string);
|
|
14
|
+
handleInput(data: string): void;
|
|
15
|
+
render(width: number): string[];
|
|
16
|
+
invalidate(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Container, Text, Spacer, SelectList, matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
|
|
2
|
+
import { colors, theme } from "../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reusable menu screen with title, items, and left/right/enter/esc navigation.
|
|
5
|
+
* Enter or Right = select, Left or Esc = back.
|
|
6
|
+
*/
|
|
7
|
+
export class MenuScreen {
|
|
8
|
+
container;
|
|
9
|
+
selectList;
|
|
10
|
+
tui;
|
|
11
|
+
onSelect;
|
|
12
|
+
onBack;
|
|
13
|
+
constructor(tui, title, items, subtitle) {
|
|
14
|
+
this.tui = tui;
|
|
15
|
+
this.container = new Container();
|
|
16
|
+
this.container.addChild(new Spacer(1));
|
|
17
|
+
this.container.addChild(new Text(truncateToWidth(theme.border("─".repeat(60)), 60), 0, 0));
|
|
18
|
+
this.container.addChild(new Text(theme.title(` ${title}`), 1, 0));
|
|
19
|
+
if (subtitle) {
|
|
20
|
+
this.container.addChild(new Text(colors.gray(` ${subtitle}`), 1, 0));
|
|
21
|
+
}
|
|
22
|
+
this.container.addChild(new Text(truncateToWidth(theme.border("─".repeat(60)), 60), 0, 0));
|
|
23
|
+
this.container.addChild(new Spacer(1));
|
|
24
|
+
this.selectList = new SelectList(items, Math.min(items.length, 15), {
|
|
25
|
+
selectedPrefix: (t) => theme.accent(t),
|
|
26
|
+
selectedText: (t) => theme.accent(t),
|
|
27
|
+
description: (t) => colors.gray(t),
|
|
28
|
+
scrollInfo: (t) => colors.gray(t),
|
|
29
|
+
noMatch: (t) => colors.gray(t),
|
|
30
|
+
});
|
|
31
|
+
this.selectList.onSelect = (item) => this.onSelect?.(item.value);
|
|
32
|
+
this.selectList.onCancel = () => this.onBack?.();
|
|
33
|
+
this.container.addChild(this.selectList);
|
|
34
|
+
this.container.addChild(new Spacer(1));
|
|
35
|
+
this.container.addChild(new Text(colors.gray(" ↑↓ navigate • →/enter select • ←/esc back"), 1, 0));
|
|
36
|
+
}
|
|
37
|
+
handleInput(data) {
|
|
38
|
+
// Right arrow = enter
|
|
39
|
+
if (matchesKey(data, Key.right)) {
|
|
40
|
+
// Simulate enter by triggering current selection
|
|
41
|
+
this.selectList.handleInput("\r");
|
|
42
|
+
this.tui.requestRender();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Left arrow = back
|
|
46
|
+
if (matchesKey(data, Key.left)) {
|
|
47
|
+
this.onBack?.();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.selectList.handleInput(data);
|
|
51
|
+
this.tui.requestRender();
|
|
52
|
+
}
|
|
53
|
+
render(width) {
|
|
54
|
+
return this.container.render(width);
|
|
55
|
+
}
|
|
56
|
+
invalidate() {
|
|
57
|
+
this.container.invalidate();
|
|
58
|
+
}
|
|
59
|
+
}
|