swarmlancer-cli 0.2.1 → 0.3.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 +29 -79
- package/dist/inference.d.ts +4 -0
- package/dist/inference.js +13 -0
- package/dist/screens/banner.js +10 -6
- package/dist/screens/dashboard.d.ts +25 -7
- package/dist/screens/dashboard.js +109 -37
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { ProcessTerminal, TUI, Container } 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
|
-
import { initInference, getAvailableModels, setAgentInstructions } from "./inference.js";
|
|
4
|
+
import { initInference, getAvailableModels, setAgentInstructions, getAuthenticatedProviders } from "./inference.js";
|
|
5
5
|
import { startAgent, stopAgent, sendToServer } from "./agent.js";
|
|
6
6
|
import { colors } from "./theme.js";
|
|
7
7
|
import { BannerComponent } from "./screens/banner.js";
|
|
8
8
|
import { SetupWizardScreen } from "./screens/setup-wizard.js";
|
|
9
9
|
import { DashboardScreen } from "./screens/dashboard.js";
|
|
10
|
-
import { AgentListScreen } from "./screens/agent-list.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";
|
|
@@ -112,9 +111,6 @@ async function runSetup() {
|
|
|
112
111
|
await new Promise((r) => setTimeout(r, 800));
|
|
113
112
|
}
|
|
114
113
|
// ── Sub-screens that return promises ──────────────────────
|
|
115
|
-
/**
|
|
116
|
-
* Ask the user for a name via the NameEditorScreen.
|
|
117
|
-
*/
|
|
118
114
|
function askName(title, subtitle, current = "") {
|
|
119
115
|
return new Promise((resolve) => {
|
|
120
116
|
const screen = new NameEditorScreen(tui, current, title, subtitle);
|
|
@@ -123,9 +119,6 @@ function askName(title, subtitle, current = "") {
|
|
|
123
119
|
setScreen(screen);
|
|
124
120
|
});
|
|
125
121
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Inline agent instructions editor.
|
|
128
|
-
*/
|
|
129
122
|
function editInstructions(agent) {
|
|
130
123
|
return new Promise((resolve) => {
|
|
131
124
|
const screen = new AgentEditorScreen(tui, agent.instructions);
|
|
@@ -144,9 +137,6 @@ function editInstructions(agent) {
|
|
|
144
137
|
setScreen(screen);
|
|
145
138
|
});
|
|
146
139
|
}
|
|
147
|
-
/**
|
|
148
|
-
* Agent limits settings screen.
|
|
149
|
-
*/
|
|
150
140
|
function editLimits(agent) {
|
|
151
141
|
return new Promise((resolve) => {
|
|
152
142
|
const screen = new SettingsScreen(tui, agent.limits);
|
|
@@ -165,9 +155,6 @@ function editLimits(agent) {
|
|
|
165
155
|
setScreen(screen);
|
|
166
156
|
});
|
|
167
157
|
}
|
|
168
|
-
/**
|
|
169
|
-
* Discovery settings screen.
|
|
170
|
-
*/
|
|
171
158
|
function editDiscovery(agent) {
|
|
172
159
|
return new Promise((resolve) => {
|
|
173
160
|
const screen = new DiscoverySettingsScreen(tui, agent.discovery);
|
|
@@ -186,16 +173,13 @@ function editDiscovery(agent) {
|
|
|
186
173
|
setScreen(screen);
|
|
187
174
|
});
|
|
188
175
|
}
|
|
189
|
-
/**
|
|
190
|
-
* Model picker screen — saves the model pattern to the agent.
|
|
191
|
-
*/
|
|
192
176
|
function editModel(agent) {
|
|
193
177
|
return new Promise(async (resolve) => {
|
|
194
178
|
try {
|
|
195
179
|
await initInference();
|
|
196
180
|
const models = await getAvailableModels();
|
|
197
181
|
if (models.length === 0) {
|
|
198
|
-
await showMessage("No models", ["No models available.", "
|
|
182
|
+
await showMessage("No models", ["No models available.", "Select a provider to authenticate."], "error");
|
|
199
183
|
resolve();
|
|
200
184
|
return;
|
|
201
185
|
}
|
|
@@ -215,9 +199,6 @@ function editModel(agent) {
|
|
|
215
199
|
}
|
|
216
200
|
});
|
|
217
201
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Ask for an optional session goal before starting.
|
|
220
|
-
*/
|
|
221
202
|
function askSessionGoal() {
|
|
222
203
|
return new Promise((resolve) => {
|
|
223
204
|
const screen = new SessionGoalScreen(tui);
|
|
@@ -226,13 +207,10 @@ function askSessionGoal() {
|
|
|
226
207
|
setScreen(screen);
|
|
227
208
|
});
|
|
228
209
|
}
|
|
229
|
-
/**
|
|
230
|
-
* Pick which agent to start (or auto-select if only one).
|
|
231
|
-
*/
|
|
232
210
|
function pickAgent() {
|
|
233
211
|
const agents = getAgents();
|
|
234
212
|
if (agents.length === 0) {
|
|
235
|
-
return showMessage("No agents", ["Create an agent first
|
|
213
|
+
return showMessage("No agents", ["Create an agent first."], "error").then(() => null);
|
|
236
214
|
}
|
|
237
215
|
if (agents.length === 1) {
|
|
238
216
|
return Promise.resolve(agents[0]);
|
|
@@ -249,7 +227,7 @@ async function runAgentConfig(agentId) {
|
|
|
249
227
|
while (true) {
|
|
250
228
|
const agent = getAgent(agentId);
|
|
251
229
|
if (!agent)
|
|
252
|
-
return;
|
|
230
|
+
return;
|
|
253
231
|
const action = await new Promise((resolve) => {
|
|
254
232
|
const screen = new AgentConfigScreen(tui, agent);
|
|
255
233
|
screen.onAction = resolve;
|
|
@@ -286,37 +264,7 @@ async function runAgentConfig(agentId) {
|
|
|
286
264
|
}
|
|
287
265
|
}
|
|
288
266
|
}
|
|
289
|
-
// ── Agent list ────────────────────────────────────────────
|
|
290
|
-
async function runAgentList() {
|
|
291
|
-
while (true) {
|
|
292
|
-
const agents = getAgents();
|
|
293
|
-
const action = await new Promise((resolve) => {
|
|
294
|
-
const screen = new AgentListScreen(tui, agents);
|
|
295
|
-
screen.onAction = resolve;
|
|
296
|
-
setScreen(screen);
|
|
297
|
-
});
|
|
298
|
-
switch (action.type) {
|
|
299
|
-
case "create": {
|
|
300
|
-
const name = await askName("New Agent", "Give your agent a name.");
|
|
301
|
-
if (name) {
|
|
302
|
-
const agent = createAgent(name);
|
|
303
|
-
// Go straight into editing the new agent
|
|
304
|
-
await runAgentConfig(agent.id);
|
|
305
|
-
}
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
case "select":
|
|
309
|
-
await runAgentConfig(action.agent.id);
|
|
310
|
-
break;
|
|
311
|
-
case "back":
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
267
|
// ── Running the agent ─────────────────────────────────────
|
|
317
|
-
/**
|
|
318
|
-
* Fetch candidate profiles from the server's /api/discover endpoint.
|
|
319
|
-
*/
|
|
320
268
|
async function fetchCandidates(discovery) {
|
|
321
269
|
const config = getConfig();
|
|
322
270
|
const params = new URLSearchParams();
|
|
@@ -341,15 +289,10 @@ async function fetchCandidates(discovery) {
|
|
|
341
289
|
const data = (await res.json());
|
|
342
290
|
return data.profiles;
|
|
343
291
|
}
|
|
344
|
-
/**
|
|
345
|
-
* Start the agent: pick agent → session goal → discover → screen → connect → run.
|
|
346
|
-
*/
|
|
347
292
|
async function runAgentSession() {
|
|
348
|
-
// 1. Pick agent
|
|
349
293
|
const agent = await pickAgent();
|
|
350
294
|
if (!agent)
|
|
351
295
|
return;
|
|
352
|
-
// 2. Init model for this agent
|
|
353
296
|
let activeModel;
|
|
354
297
|
try {
|
|
355
298
|
const { model } = await initInference(agent.modelPattern);
|
|
@@ -359,14 +302,11 @@ async function runAgentSession() {
|
|
|
359
302
|
await showMessage("Model error", [err instanceof Error ? err.message : "Failed to initialize model"], "error");
|
|
360
303
|
return;
|
|
361
304
|
}
|
|
362
|
-
// 3. Set agent instructions for inference
|
|
363
305
|
setAgentInstructions(agent.instructions);
|
|
364
|
-
// 4. Ask for session goal
|
|
365
306
|
const sessionGoal = await askSessionGoal();
|
|
366
307
|
const config = getConfig();
|
|
367
308
|
const discovery = agent.discovery;
|
|
368
309
|
const limits = agent.limits;
|
|
369
|
-
// 5. Show the running screen
|
|
370
310
|
const screen = new AgentRunningScreen(tui, activeModel, config.serverUrl, agent.name, sessionGoal);
|
|
371
311
|
const done = new Promise((resolve) => {
|
|
372
312
|
screen.onStop = () => {
|
|
@@ -376,9 +316,7 @@ async function runAgentSession() {
|
|
|
376
316
|
});
|
|
377
317
|
setScreen(screen);
|
|
378
318
|
const log = (line) => screen.addLog(line);
|
|
379
|
-
// 6. Start the WebSocket agent (for incoming requests) with per-agent limits
|
|
380
319
|
startAgent(limits, agent.id, agent.name, log);
|
|
381
|
-
// 7. Discovery + screening (outbound)
|
|
382
320
|
screen.setStatus("discovering candidates...");
|
|
383
321
|
try {
|
|
384
322
|
const candidates = await fetchCandidates(discovery);
|
|
@@ -409,19 +347,16 @@ async function runAgentSession() {
|
|
|
409
347
|
else {
|
|
410
348
|
log(colors.lime(`Found ${matches.length} match${matches.length === 1 ? "" : "es"}. Starting conversations...`));
|
|
411
349
|
log("");
|
|
412
|
-
// Initiate conversations with matches
|
|
413
350
|
for (const match of matches) {
|
|
414
351
|
if (!match.profile.online) {
|
|
415
352
|
log(colors.gray(` ⊘ @${match.profile.githubUsername} is offline, skipping`));
|
|
416
353
|
continue;
|
|
417
354
|
}
|
|
418
355
|
log(colors.lime(` 🤝 Reaching out to @${match.profile.githubUsername} (${match.score}/10: ${match.reason})`));
|
|
419
|
-
// Send start_conversation via WebSocket
|
|
420
356
|
sendToServer({
|
|
421
357
|
type: "start_conversation",
|
|
422
358
|
withUserId: match.profile.id,
|
|
423
359
|
});
|
|
424
|
-
// Respect cooldown between outreach
|
|
425
360
|
if (limits.cooldownSeconds > 0) {
|
|
426
361
|
await new Promise((r) => setTimeout(r, limits.cooldownSeconds * 1000));
|
|
427
362
|
}
|
|
@@ -439,19 +374,17 @@ async function runAgentSession() {
|
|
|
439
374
|
// ── Dashboard loop ────────────────────────────────────────
|
|
440
375
|
async function runDashboard() {
|
|
441
376
|
while (true) {
|
|
442
|
-
const config = getConfig();
|
|
443
377
|
const agents = getAgents();
|
|
378
|
+
const authenticatedProviders = await getAuthenticatedProviders();
|
|
444
379
|
const action = await new Promise((resolve) => {
|
|
445
380
|
const dashboard = new DashboardScreen(tui, {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
agentCount: agents.length,
|
|
449
|
-
serverUrl: config.serverUrl,
|
|
381
|
+
agents,
|
|
382
|
+
authenticatedProviders,
|
|
450
383
|
});
|
|
451
384
|
dashboard.onAction = resolve;
|
|
452
385
|
setScreen(dashboard);
|
|
453
386
|
});
|
|
454
|
-
switch (action) {
|
|
387
|
+
switch (action.type) {
|
|
455
388
|
case "start": {
|
|
456
389
|
const conf = getConfig();
|
|
457
390
|
if (!conf.token) {
|
|
@@ -461,8 +394,27 @@ async function runDashboard() {
|
|
|
461
394
|
await runAgentSession();
|
|
462
395
|
break;
|
|
463
396
|
}
|
|
464
|
-
case "
|
|
465
|
-
await
|
|
397
|
+
case "create-agent": {
|
|
398
|
+
const name = await askName("New Agent", "Give your agent a name.");
|
|
399
|
+
if (name) {
|
|
400
|
+
const agent = createAgent(name);
|
|
401
|
+
await runAgentConfig(agent.id);
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case "edit-agent":
|
|
406
|
+
await runAgentConfig(action.agentId);
|
|
407
|
+
break;
|
|
408
|
+
case "provider":
|
|
409
|
+
await showMessage("Provider Login", [
|
|
410
|
+
`To authenticate with this provider, run:`,
|
|
411
|
+
``,
|
|
412
|
+
` pi`,
|
|
413
|
+
` /login`,
|
|
414
|
+
``,
|
|
415
|
+
`Then select the provider in pi's login flow.`,
|
|
416
|
+
`Your credentials are stored locally in ~/.pi/agent/auth.json`,
|
|
417
|
+
], "info");
|
|
466
418
|
break;
|
|
467
419
|
case "quit":
|
|
468
420
|
shutdown();
|
|
@@ -482,10 +434,8 @@ export async function runInteractive() {
|
|
|
482
434
|
terminal = new ProcessTerminal();
|
|
483
435
|
tui = new TUI(terminal, true);
|
|
484
436
|
tui.start();
|
|
485
|
-
// Handle Ctrl+C globally
|
|
486
437
|
tui.addInputListener((data) => {
|
|
487
438
|
if (data === "\x03") {
|
|
488
|
-
// Ctrl+C
|
|
489
439
|
shutdown();
|
|
490
440
|
return { consume: true };
|
|
491
441
|
}
|
package/dist/inference.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export declare function initInference(modelPattern?: string): Promise<{
|
|
|
3
3
|
model: Model<Api>;
|
|
4
4
|
}>;
|
|
5
5
|
export declare function getAvailableModels(): Model<Api>[];
|
|
6
|
+
/**
|
|
7
|
+
* Return unique provider names that have at least one authenticated model.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getAuthenticatedProviders(): Promise<string[]>;
|
|
6
10
|
/**
|
|
7
11
|
* Set the agent instructions that will be prepended to every inference call.
|
|
8
12
|
*/
|
package/dist/inference.js
CHANGED
|
@@ -40,6 +40,19 @@ export async function initInference(modelPattern) {
|
|
|
40
40
|
export function getAvailableModels() {
|
|
41
41
|
return modelRegistry?.getAvailable() ?? Promise.resolve([]);
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Return unique provider names that have at least one authenticated model.
|
|
45
|
+
*/
|
|
46
|
+
export async function getAuthenticatedProviders() {
|
|
47
|
+
try {
|
|
48
|
+
const models = await getAvailableModels();
|
|
49
|
+
const providers = new Set(models.map((m) => m.provider.toLowerCase()));
|
|
50
|
+
return Array.from(providers);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
43
56
|
/**
|
|
44
57
|
* Set the agent instructions that will be prepended to every inference call.
|
|
45
58
|
*/
|
package/dist/screens/banner.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
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
|
-
|
|
4
|
+
` █████████ ███`,
|
|
5
|
+
` ███░░░░░███ ███░ `,
|
|
6
|
+
`░███ ░░░ ███░ `,
|
|
7
|
+
`░░█████████ ███░ `,
|
|
8
|
+
` ░░░░░░░░███░░░███ `,
|
|
9
|
+
` ███ ░███ ░░░███ `,
|
|
10
|
+
`░░█████████ ░░░███`,
|
|
11
|
+
` ░░░░░░░░░ ░░░ `,
|
|
8
12
|
];
|
|
9
13
|
export class BannerComponent extends Container {
|
|
10
14
|
constructor() {
|
|
@@ -15,10 +19,10 @@ export class BannerComponent extends Container {
|
|
|
15
19
|
this.clear();
|
|
16
20
|
this.addChild(new Spacer(1));
|
|
17
21
|
for (const line of BANNER_ART) {
|
|
18
|
-
this.addChild(new Text(colors.limeBold(line), 1, 0));
|
|
22
|
+
this.addChild(new Text(colors.limeBold(` ${line}`), 1, 0));
|
|
19
23
|
}
|
|
20
24
|
this.addChild(new Spacer(0));
|
|
21
|
-
this.addChild(new Text(colors.gray("
|
|
25
|
+
this.addChild(new Text(colors.gray(" LET THE SWARM BEGIN"), 1, 0));
|
|
22
26
|
this.addChild(new Spacer(1));
|
|
23
27
|
}
|
|
24
28
|
invalidate() {
|
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
import { type Component } from "@mariozechner/pi-tui";
|
|
2
2
|
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
-
import {
|
|
4
|
-
export type MenuAction =
|
|
3
|
+
import type { AgentProfile } from "../config.js";
|
|
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
|
+
authenticatedProviders: string[];
|
|
20
|
+
}
|
|
5
21
|
export declare class DashboardScreen implements Component {
|
|
6
|
-
private container;
|
|
7
|
-
private statusPanel;
|
|
8
|
-
private selectList;
|
|
9
22
|
private tui;
|
|
23
|
+
private banner;
|
|
24
|
+
private items;
|
|
25
|
+
private selectableIndices;
|
|
26
|
+
private cursor;
|
|
27
|
+
private cachedRender?;
|
|
10
28
|
onAction?: (action: MenuAction) => void;
|
|
11
|
-
constructor(tui: TUI,
|
|
12
|
-
|
|
29
|
+
constructor(tui: TUI, data: DashboardData);
|
|
30
|
+
private buildItems;
|
|
13
31
|
handleInput(data: string): void;
|
|
14
32
|
render(width: number): string[];
|
|
15
33
|
invalidate(): void;
|
|
@@ -1,56 +1,128 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
|
|
2
2
|
import { colors, theme } from "../theme.js";
|
|
3
|
-
import { StatusPanel } from "./status-panel.js";
|
|
4
3
|
import { BannerComponent } from "./banner.js";
|
|
5
|
-
const
|
|
6
|
-
{
|
|
7
|
-
{
|
|
8
|
-
{
|
|
4
|
+
const KNOWN_PROVIDERS = [
|
|
5
|
+
{ id: "anthropic", label: "Anthropic (Claude Pro/Max)", match: ["anthropic"] },
|
|
6
|
+
{ id: "copilot", label: "GitHub Copilot", match: ["copilot", "github"] },
|
|
7
|
+
{ id: "google", label: "Google Cloud Code Assist (Gemini CLI)", match: ["google", "vertex"] },
|
|
8
|
+
{ id: "antigravity", label: "Antigravity (Gemini 3, Claude, GPT-OSS)", match: ["antigravity"] },
|
|
9
|
+
{ id: "openai", label: "ChatGPT Plus/Pro (Codex Subscription)", match: ["openai", "chatgpt"] },
|
|
9
10
|
];
|
|
10
11
|
export class DashboardScreen {
|
|
11
|
-
container;
|
|
12
|
-
statusPanel;
|
|
13
|
-
selectList;
|
|
14
12
|
tui;
|
|
13
|
+
banner;
|
|
14
|
+
items;
|
|
15
|
+
selectableIndices;
|
|
16
|
+
cursor = 0;
|
|
17
|
+
cachedRender;
|
|
15
18
|
onAction;
|
|
16
|
-
constructor(tui,
|
|
19
|
+
constructor(tui, data) {
|
|
17
20
|
this.tui = tui;
|
|
18
|
-
this.
|
|
19
|
-
|
|
20
|
-
this.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.container.addChild(this.statusPanel);
|
|
24
|
-
this.container.addChild(new Spacer(1));
|
|
25
|
-
this.selectList = new SelectList(MENU_ITEMS, MENU_ITEMS.length, {
|
|
26
|
-
selectedPrefix: (t) => theme.accent(t),
|
|
27
|
-
selectedText: (t) => theme.accent(t),
|
|
28
|
-
description: (t) => colors.gray(t),
|
|
29
|
-
scrollInfo: (t) => colors.gray(t),
|
|
30
|
-
noMatch: (t) => colors.gray(t),
|
|
31
|
-
});
|
|
32
|
-
this.selectList.onSelect = (item) => {
|
|
33
|
-
this.onAction?.(item.value);
|
|
34
|
-
};
|
|
35
|
-
this.container.addChild(this.selectList);
|
|
36
|
-
this.container.addChild(new Spacer(1));
|
|
37
|
-
this.container.addChild(new Text(colors.gray(" ↑↓ navigate • enter select • q quit"), 1, 0));
|
|
21
|
+
this.banner = new BannerComponent();
|
|
22
|
+
this.items = this.buildItems(data);
|
|
23
|
+
this.selectableIndices = this.items
|
|
24
|
+
.map((item, i) => (item.kind === "action" ? i : -1))
|
|
25
|
+
.filter((i) => i >= 0);
|
|
38
26
|
}
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
buildItems(data) {
|
|
28
|
+
const items = [];
|
|
29
|
+
items.push({ kind: "action", text: "Go online", indent: 0, action: { type: "start" } });
|
|
30
|
+
items.push({ kind: "spacer", text: "", indent: 0 });
|
|
31
|
+
items.push({ kind: "header", text: "Swarm", indent: 0 });
|
|
32
|
+
// Manage
|
|
33
|
+
items.push({ kind: "header", text: "Manage", indent: 1 });
|
|
34
|
+
items.push({ kind: "action", text: "+ Create", indent: 2, action: { type: "create-agent" } });
|
|
35
|
+
for (const agent of data.agents) {
|
|
36
|
+
const ready = agent.instructions.length > 0;
|
|
37
|
+
const suffix = ready ? " [⚡]" : "";
|
|
38
|
+
items.push({
|
|
39
|
+
kind: "action",
|
|
40
|
+
text: `${agent.name}${suffix}`,
|
|
41
|
+
indent: 2,
|
|
42
|
+
action: { type: "edit-agent", agentId: agent.id },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// Providers
|
|
46
|
+
items.push({ kind: "header", text: "Providers", indent: 1 });
|
|
47
|
+
items.push({ kind: "label", text: "Select provider to login:", indent: 2 });
|
|
48
|
+
for (const p of KNOWN_PROVIDERS) {
|
|
49
|
+
const loggedIn = p.match.some((m) => data.authenticatedProviders.some((ap) => ap.includes(m)));
|
|
50
|
+
const prefix = loggedIn ? "→ " : " ";
|
|
51
|
+
const suffix = loggedIn ? " ✓ logged in" : "";
|
|
52
|
+
items.push({
|
|
53
|
+
kind: "action",
|
|
54
|
+
text: `${prefix}${p.label}${suffix}`,
|
|
55
|
+
indent: 3,
|
|
56
|
+
action: { type: "provider", providerId: p.id },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
items.push({ kind: "spacer", text: "", indent: 0 });
|
|
60
|
+
items.push({ kind: "action", text: "Exit", indent: 0, action: { type: "quit" } });
|
|
61
|
+
return items;
|
|
41
62
|
}
|
|
42
63
|
handleInput(data) {
|
|
43
64
|
if (matchesKey(data, "q")) {
|
|
44
|
-
this.onAction?.("quit");
|
|
65
|
+
this.onAction?.({ type: "quit" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (matchesKey(data, Key.up)) {
|
|
69
|
+
if (this.cursor > 0)
|
|
70
|
+
this.cursor--;
|
|
71
|
+
this.cachedRender = undefined;
|
|
72
|
+
this.tui.requestRender();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (matchesKey(data, Key.down)) {
|
|
76
|
+
if (this.cursor < this.selectableIndices.length - 1)
|
|
77
|
+
this.cursor++;
|
|
78
|
+
this.cachedRender = undefined;
|
|
79
|
+
this.tui.requestRender();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (matchesKey(data, Key.enter)) {
|
|
83
|
+
const itemIndex = this.selectableIndices[this.cursor];
|
|
84
|
+
const item = this.items[itemIndex];
|
|
85
|
+
if (item.action) {
|
|
86
|
+
this.onAction?.(item.action);
|
|
87
|
+
}
|
|
45
88
|
return;
|
|
46
89
|
}
|
|
47
|
-
this.selectList.handleInput(data);
|
|
48
|
-
this.tui.requestRender();
|
|
49
90
|
}
|
|
50
91
|
render(width) {
|
|
51
|
-
|
|
92
|
+
if (this.cachedRender)
|
|
93
|
+
return this.cachedRender;
|
|
94
|
+
const lines = this.banner.render(width);
|
|
95
|
+
const selectedItemIndex = this.selectableIndices[this.cursor];
|
|
96
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
97
|
+
const item = this.items[i];
|
|
98
|
+
const pad = " ".repeat(item.indent);
|
|
99
|
+
if (item.kind === "spacer") {
|
|
100
|
+
lines.push("");
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (item.kind === "header") {
|
|
104
|
+
lines.push(truncateToWidth(` ${pad}${colors.bold(item.text)}`, width));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (item.kind === "label") {
|
|
108
|
+
lines.push(truncateToWidth(` ${pad}${colors.gray(item.text)}`, width));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Selectable action
|
|
112
|
+
const isSelected = i === selectedItemIndex;
|
|
113
|
+
if (isSelected) {
|
|
114
|
+
lines.push(truncateToWidth(` ${pad}${theme.accent(`▸ ${item.text}`)}`, width));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
lines.push(truncateToWidth(` ${pad} ${item.text}`, width));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
lines.push("");
|
|
121
|
+
lines.push(truncateToWidth(colors.gray(" ↑↓ navigate • enter select • q quit"), width));
|
|
122
|
+
this.cachedRender = lines;
|
|
123
|
+
return lines;
|
|
52
124
|
}
|
|
53
125
|
invalidate() {
|
|
54
|
-
this.
|
|
126
|
+
this.cachedRender = undefined;
|
|
55
127
|
}
|
|
56
128
|
}
|