swarmlancer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/agent.d.ts +4 -0
- package/dist/agent.js +95 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +303 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +61 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +110 -0
- package/dist/inference.d.ts +9 -0
- package/dist/inference.js +100 -0
- package/dist/login.d.ts +1 -0
- package/dist/login.js +57 -0
- package/dist/profile.d.ts +3 -0
- package/dist/profile.js +32 -0
- package/dist/screens/agent-editor.d.ts +13 -0
- package/dist/screens/agent-editor.js +64 -0
- package/dist/screens/agent-running.d.ts +17 -0
- package/dist/screens/agent-running.js +62 -0
- package/dist/screens/banner.d.ts +6 -0
- package/dist/screens/banner.js +28 -0
- package/dist/screens/dashboard.d.ts +16 -0
- package/dist/screens/dashboard.js +62 -0
- package/dist/screens/message.d.ts +15 -0
- package/dist/screens/message.js +39 -0
- package/dist/screens/model-picker.d.ts +14 -0
- package/dist/screens/model-picker.js +49 -0
- package/dist/screens/profile-editor.d.ts +16 -0
- package/dist/screens/profile-editor.js +129 -0
- package/dist/screens/profile-view.d.ts +11 -0
- package/dist/screens/profile-view.js +38 -0
- package/dist/screens/setup-wizard.d.ts +20 -0
- package/dist/screens/setup-wizard.js +120 -0
- package/dist/screens/status-panel.d.ts +15 -0
- package/dist/screens/status-panel.js +34 -0
- package/dist/theme.d.ts +39 -0
- package/dist/theme.js +47 -0
- package/package.json +49 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { AuthStorage, ModelRegistry, createAgentSession, SessionManager, SettingsManager, createExtensionRuntime, } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { getAgentInstructions } from "./config.js";
|
|
3
|
+
let authStorage;
|
|
4
|
+
let modelRegistry;
|
|
5
|
+
let currentModel;
|
|
6
|
+
export async function initInference(modelPattern) {
|
|
7
|
+
authStorage = AuthStorage.create(); // reads ~/.pi/agent/auth.json
|
|
8
|
+
modelRegistry = new ModelRegistry(authStorage);
|
|
9
|
+
const available = await modelRegistry.getAvailable();
|
|
10
|
+
if (available.length === 0) {
|
|
11
|
+
throw new Error("No models available. Run `pi` first and authenticate with a provider (Anthropic, OpenAI, Google, Ollama, etc.)");
|
|
12
|
+
}
|
|
13
|
+
if (modelPattern) {
|
|
14
|
+
const match = available.find((m) => m.id.includes(modelPattern) ||
|
|
15
|
+
m.name.toLowerCase().includes(modelPattern.toLowerCase()) ||
|
|
16
|
+
`${m.provider}/${m.id}`.includes(modelPattern));
|
|
17
|
+
if (!match) {
|
|
18
|
+
console.error(` Model "${modelPattern}" not found. Available:`);
|
|
19
|
+
for (const m of available)
|
|
20
|
+
console.error(` ${m.provider}/${m.id}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
currentModel = match;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// Pick cheapest reasonable model — prefer latest small models
|
|
27
|
+
const preferences = ["haiku-4", "flash", "gpt-4o-mini", "haiku"];
|
|
28
|
+
for (const pref of preferences) {
|
|
29
|
+
const match = available.find((m) => m.id.includes(pref));
|
|
30
|
+
if (match) {
|
|
31
|
+
currentModel = match;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!currentModel)
|
|
36
|
+
currentModel = available[0];
|
|
37
|
+
}
|
|
38
|
+
return { model: currentModel };
|
|
39
|
+
}
|
|
40
|
+
export function getAvailableModels() {
|
|
41
|
+
return modelRegistry?.getAvailable() ?? Promise.resolve([]);
|
|
42
|
+
}
|
|
43
|
+
export async function runInference(systemPrompt, messages) {
|
|
44
|
+
if (!currentModel || !authStorage) {
|
|
45
|
+
throw new Error("Inference not initialized. Call initInference() first.");
|
|
46
|
+
}
|
|
47
|
+
// Prepend local agent instructions to server-provided system prompt
|
|
48
|
+
const agentInstructions = getAgentInstructions();
|
|
49
|
+
const fullSystemPrompt = agentInstructions
|
|
50
|
+
? `${agentInstructions}\n\n---\n\n${systemPrompt}`
|
|
51
|
+
: systemPrompt;
|
|
52
|
+
const resourceLoader = {
|
|
53
|
+
getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
|
|
54
|
+
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
55
|
+
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
56
|
+
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
57
|
+
getAgentsFiles: () => ({ agentsFiles: [] }),
|
|
58
|
+
getSystemPrompt: () => fullSystemPrompt,
|
|
59
|
+
getAppendSystemPrompt: () => [],
|
|
60
|
+
getPathMetadata: () => new Map(),
|
|
61
|
+
extendResources: () => { },
|
|
62
|
+
reload: async () => { },
|
|
63
|
+
};
|
|
64
|
+
const { session } = await createAgentSession({
|
|
65
|
+
model: currentModel,
|
|
66
|
+
thinkingLevel: "off",
|
|
67
|
+
tools: [],
|
|
68
|
+
authStorage,
|
|
69
|
+
modelRegistry,
|
|
70
|
+
resourceLoader,
|
|
71
|
+
sessionManager: SessionManager.inMemory(),
|
|
72
|
+
settingsManager: SettingsManager.inMemory({
|
|
73
|
+
compaction: { enabled: false },
|
|
74
|
+
retry: { enabled: true, maxRetries: 2 },
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
// Inject conversation history (all but last message)
|
|
78
|
+
if (messages.length > 1) {
|
|
79
|
+
for (const msg of messages.slice(0, -1)) {
|
|
80
|
+
session.agent.state.messages.push(msg.role === "user"
|
|
81
|
+
? { role: "user", content: [{ type: "text", text: msg.content }], timestamp: Date.now() }
|
|
82
|
+
: {
|
|
83
|
+
role: "assistant",
|
|
84
|
+
content: [{ type: "text", text: msg.content }],
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let responseText = "";
|
|
90
|
+
session.subscribe((event) => {
|
|
91
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
92
|
+
responseText += event.assistantMessageEvent.delta;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// Prompt with the last message
|
|
96
|
+
const lastMessage = messages[messages.length - 1];
|
|
97
|
+
await session.prompt(lastMessage.content);
|
|
98
|
+
session.dispose();
|
|
99
|
+
return responseText || "(no response)";
|
|
100
|
+
}
|
package/dist/login.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function login(): Promise<void>;
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getConfig, saveConfig } from "./config.js";
|
|
2
|
+
import open from "open";
|
|
3
|
+
import http from "http";
|
|
4
|
+
export async function login() {
|
|
5
|
+
const config = getConfig();
|
|
6
|
+
// Request GitHub OAuth URL from server, telling it this is a CLI login
|
|
7
|
+
const res = await fetch(`${config.serverUrl}/api/auth/github?source=cli`);
|
|
8
|
+
const { url } = (await res.json());
|
|
9
|
+
console.log("\n Opening GitHub in your browser...\n");
|
|
10
|
+
await open(url);
|
|
11
|
+
// Start a temporary local server to catch the token callback
|
|
12
|
+
// The server-side OAuth callback will try to POST the token here
|
|
13
|
+
const callbackPort = 9876;
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const server = http.createServer(async (req, res) => {
|
|
16
|
+
const reqUrl = new URL(req.url || "/", `http://localhost:${callbackPort}`);
|
|
17
|
+
if (reqUrl.pathname === "/callback") {
|
|
18
|
+
const token = reqUrl.searchParams.get("token");
|
|
19
|
+
const userId = reqUrl.searchParams.get("userId");
|
|
20
|
+
if (token && userId) {
|
|
21
|
+
// CORS headers so the browser page can reach us
|
|
22
|
+
res.writeHead(200, {
|
|
23
|
+
"Content-Type": "text/plain",
|
|
24
|
+
"Access-Control-Allow-Origin": "*",
|
|
25
|
+
});
|
|
26
|
+
res.end("ok");
|
|
27
|
+
saveConfig({ ...config, token, userId });
|
|
28
|
+
server.close();
|
|
29
|
+
console.log(" ✓ Logged in successfully!");
|
|
30
|
+
console.log(` ✓ Token saved to ~/.swarmlancer/config.json\n`);
|
|
31
|
+
resolve();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// CORS preflight
|
|
36
|
+
if (req.method === "OPTIONS") {
|
|
37
|
+
res.writeHead(200, {
|
|
38
|
+
"Access-Control-Allow-Origin": "*",
|
|
39
|
+
"Access-Control-Allow-Methods": "GET",
|
|
40
|
+
});
|
|
41
|
+
res.end();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
res.writeHead(404);
|
|
45
|
+
res.end("Not found");
|
|
46
|
+
});
|
|
47
|
+
server.listen(callbackPort, () => {
|
|
48
|
+
console.log(" Waiting for GitHub authorization...");
|
|
49
|
+
console.log(" (If the browser doesn't open, visit the URL above manually)\n");
|
|
50
|
+
});
|
|
51
|
+
// Timeout after 2 minutes
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
server.close();
|
|
54
|
+
reject(new Error("Login timed out. Try again."));
|
|
55
|
+
}, 120_000);
|
|
56
|
+
});
|
|
57
|
+
}
|
package/dist/profile.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getConfig } from "./config.js";
|
|
2
|
+
async function apiFetch(path, options = {}) {
|
|
3
|
+
const config = getConfig();
|
|
4
|
+
const res = await fetch(`${config.serverUrl}/api${path}`, {
|
|
5
|
+
...options,
|
|
6
|
+
headers: {
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
Authorization: `Bearer ${config.token}`,
|
|
9
|
+
...options.headers,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
throw new Error(`API error: ${res.status}`);
|
|
14
|
+
return res.json();
|
|
15
|
+
}
|
|
16
|
+
export async function getProfile() {
|
|
17
|
+
try {
|
|
18
|
+
return (await apiFetch("/profile/me"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function isProfileComplete(profile) {
|
|
25
|
+
return !!(profile.bio && profile.skills && profile.lookingFor);
|
|
26
|
+
}
|
|
27
|
+
export async function saveProfile(data) {
|
|
28
|
+
await apiFetch("/profile/me", {
|
|
29
|
+
method: "PATCH",
|
|
30
|
+
body: JSON.stringify(data),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Component } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
+
export declare class AgentEditorScreen implements Component {
|
|
4
|
+
private tui;
|
|
5
|
+
private editor;
|
|
6
|
+
private cachedLines?;
|
|
7
|
+
onSave?: (content: string) => void;
|
|
8
|
+
onCancel?: () => void;
|
|
9
|
+
constructor(tui: TUI, content: string);
|
|
10
|
+
handleInput(data: string): void;
|
|
11
|
+
render(width: number): string[];
|
|
12
|
+
invalidate(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Editor, matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
|
|
2
|
+
import { colors, theme } from "../theme.js";
|
|
3
|
+
const EDITOR_THEME = {
|
|
4
|
+
borderColor: (s) => colors.cyan(s),
|
|
5
|
+
selectList: {
|
|
6
|
+
selectedPrefix: (t) => colors.cyan(t),
|
|
7
|
+
selectedText: (t) => colors.cyan(t),
|
|
8
|
+
description: (t) => colors.gray(t),
|
|
9
|
+
scrollInfo: (t) => colors.gray(t),
|
|
10
|
+
noMatch: (t) => colors.yellow(t),
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export class AgentEditorScreen {
|
|
14
|
+
tui;
|
|
15
|
+
editor;
|
|
16
|
+
cachedLines;
|
|
17
|
+
onSave;
|
|
18
|
+
onCancel;
|
|
19
|
+
constructor(tui, content) {
|
|
20
|
+
this.tui = tui;
|
|
21
|
+
this.editor = new Editor(tui, EDITOR_THEME, { paddingX: 1 });
|
|
22
|
+
this.editor.setText(content);
|
|
23
|
+
this.editor.disableSubmit = true; // Enter = new line, not submit
|
|
24
|
+
}
|
|
25
|
+
handleInput(data) {
|
|
26
|
+
// Ctrl+S to save
|
|
27
|
+
if (matchesKey(data, Key.ctrl("s"))) {
|
|
28
|
+
this.onSave?.(this.editor.getText());
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Escape to cancel
|
|
32
|
+
if (matchesKey(data, Key.escape)) {
|
|
33
|
+
this.onCancel?.();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Everything else goes to the editor
|
|
37
|
+
this.editor.handleInput(data);
|
|
38
|
+
this.cachedLines = undefined;
|
|
39
|
+
this.tui.requestRender();
|
|
40
|
+
}
|
|
41
|
+
render(width) {
|
|
42
|
+
if (this.cachedLines)
|
|
43
|
+
return this.cachedLines;
|
|
44
|
+
const lines = [];
|
|
45
|
+
lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
|
|
46
|
+
lines.push(truncateToWidth(` ${theme.title("Edit Agent Instructions")} ${colors.gray("~/.swarmlancer/agent.md")}`, width));
|
|
47
|
+
lines.push(truncateToWidth(colors.gray(" This controls how your agent behaves in conversations."), width));
|
|
48
|
+
lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
|
|
49
|
+
lines.push("");
|
|
50
|
+
// Editor takes most of the space
|
|
51
|
+
for (const line of this.editor.render(width)) {
|
|
52
|
+
lines.push(line);
|
|
53
|
+
}
|
|
54
|
+
lines.push("");
|
|
55
|
+
lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
|
|
56
|
+
lines.push(truncateToWidth(colors.gray(" shift+enter new line • ctrl+s save • esc cancel"), width));
|
|
57
|
+
this.cachedLines = lines;
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
invalidate() {
|
|
61
|
+
this.cachedLines = undefined;
|
|
62
|
+
this.editor.invalidate();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Component } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
+
import type { Model, Api } from "@mariozechner/pi-ai";
|
|
4
|
+
export declare class AgentRunningScreen implements Component {
|
|
5
|
+
private tui;
|
|
6
|
+
private logLines;
|
|
7
|
+
private cachedRender?;
|
|
8
|
+
private model;
|
|
9
|
+
private serverUrl;
|
|
10
|
+
private agentPath;
|
|
11
|
+
onStop?: () => void;
|
|
12
|
+
constructor(tui: TUI, model: Model<Api>, serverUrl: string, agentPath: string);
|
|
13
|
+
addLog(line: string): void;
|
|
14
|
+
handleInput(data: string): void;
|
|
15
|
+
render(width: number): string[];
|
|
16
|
+
invalidate(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
|
|
2
|
+
import { colors, theme } from "../theme.js";
|
|
3
|
+
const MAX_LOG_LINES = 200;
|
|
4
|
+
export class AgentRunningScreen {
|
|
5
|
+
tui;
|
|
6
|
+
logLines = [];
|
|
7
|
+
cachedRender;
|
|
8
|
+
model;
|
|
9
|
+
serverUrl;
|
|
10
|
+
agentPath;
|
|
11
|
+
onStop;
|
|
12
|
+
constructor(tui, model, serverUrl, agentPath) {
|
|
13
|
+
this.tui = tui;
|
|
14
|
+
this.model = model;
|
|
15
|
+
this.serverUrl = serverUrl;
|
|
16
|
+
this.agentPath = agentPath;
|
|
17
|
+
this.addLog(theme.accent("Agent starting..."));
|
|
18
|
+
this.addLog(` Model: ${colors.green(model.provider + "/" + model.id)}`);
|
|
19
|
+
this.addLog(` Server: ${colors.green(serverUrl)}`);
|
|
20
|
+
this.addLog(` Agent: ${colors.green(agentPath)}`);
|
|
21
|
+
this.addLog("");
|
|
22
|
+
}
|
|
23
|
+
addLog(line) {
|
|
24
|
+
this.logLines.push(line);
|
|
25
|
+
if (this.logLines.length > MAX_LOG_LINES) {
|
|
26
|
+
this.logLines.shift();
|
|
27
|
+
}
|
|
28
|
+
this.cachedRender = undefined;
|
|
29
|
+
this.tui.requestRender();
|
|
30
|
+
}
|
|
31
|
+
handleInput(data) {
|
|
32
|
+
if (matchesKey(data, Key.escape) ||
|
|
33
|
+
matchesKey(data, "q") ||
|
|
34
|
+
matchesKey(data, Key.ctrl("c"))) {
|
|
35
|
+
this.onStop?.();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
render(width) {
|
|
39
|
+
if (this.cachedRender)
|
|
40
|
+
return this.cachedRender;
|
|
41
|
+
const lines = [];
|
|
42
|
+
// Header
|
|
43
|
+
lines.push(theme.border("─".repeat(width)));
|
|
44
|
+
const modelInfo = `${this.model.provider}/${this.model.id}`;
|
|
45
|
+
lines.push(truncateToWidth(` ${theme.title("⚡ AGENT ONLINE")} ${colors.gray("model:")} ${colors.green(modelInfo)}`, width));
|
|
46
|
+
lines.push(theme.border("─".repeat(width)));
|
|
47
|
+
lines.push("");
|
|
48
|
+
// Log
|
|
49
|
+
for (const logLine of this.logLines) {
|
|
50
|
+
lines.push(truncateToWidth(` ${logLine}`, width));
|
|
51
|
+
}
|
|
52
|
+
// Footer
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push(theme.border("─".repeat(width)));
|
|
55
|
+
lines.push(truncateToWidth(colors.gray(" esc/q stop agent • waiting for conversations..."), width));
|
|
56
|
+
this.cachedRender = lines;
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
invalidate() {
|
|
60
|
+
this.cachedRender = undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Container, Text, Spacer } from "@mariozechner/pi-tui";
|
|
2
|
+
import { colors, theme } from "../theme.js";
|
|
3
|
+
const BANNER_ART = `
|
|
4
|
+
███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗
|
|
5
|
+
██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║
|
|
6
|
+
███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║
|
|
7
|
+
╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║
|
|
8
|
+
███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║
|
|
9
|
+
╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝`.trim();
|
|
10
|
+
export class BannerComponent extends Container {
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
this.rebuild();
|
|
14
|
+
}
|
|
15
|
+
rebuild() {
|
|
16
|
+
this.clear();
|
|
17
|
+
this.addChild(new Spacer(1));
|
|
18
|
+
for (const line of BANNER_ART.split("\n")) {
|
|
19
|
+
this.addChild(new Text(theme.accent(line), 1, 0));
|
|
20
|
+
}
|
|
21
|
+
this.addChild(new Text(colors.gray(" swarmlancer.com — your agent, your rules"), 1, 0));
|
|
22
|
+
this.addChild(new Spacer(1));
|
|
23
|
+
}
|
|
24
|
+
invalidate() {
|
|
25
|
+
super.invalidate();
|
|
26
|
+
this.rebuild();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Component } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
+
import { type StatusInfo } from "./status-panel.js";
|
|
4
|
+
export type MenuAction = "start" | "profile-edit" | "agent-edit" | "model-pick" | "profile-view" | "quit";
|
|
5
|
+
export declare class DashboardScreen implements Component {
|
|
6
|
+
private container;
|
|
7
|
+
private statusPanel;
|
|
8
|
+
private selectList;
|
|
9
|
+
private tui;
|
|
10
|
+
onAction?: (action: MenuAction) => void;
|
|
11
|
+
constructor(tui: TUI, status: StatusInfo);
|
|
12
|
+
updateStatus(info: Partial<StatusInfo>): void;
|
|
13
|
+
handleInput(data: string): void;
|
|
14
|
+
render(width: number): string[];
|
|
15
|
+
invalidate(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Container, Text, Spacer, SelectList, matchesKey, } from "@mariozechner/pi-tui";
|
|
2
|
+
import { colors, theme } from "../theme.js";
|
|
3
|
+
import { StatusPanel } from "./status-panel.js";
|
|
4
|
+
import { BannerComponent } from "./banner.js";
|
|
5
|
+
const MENU_ITEMS = [
|
|
6
|
+
{ value: "start", label: "Start agent", description: "Connect and start conversations" },
|
|
7
|
+
{ value: "profile-edit", label: "Edit profile", description: "Update your public profile" },
|
|
8
|
+
{ value: "agent-edit", label: "Edit agent instructions", description: "Configure how your agent behaves" },
|
|
9
|
+
{ value: "model-pick", label: "Change model", description: "Pick a different LLM model" },
|
|
10
|
+
{ value: "profile-view", label: "View profile", description: "See your current public profile" },
|
|
11
|
+
{ value: "quit", label: "Quit", description: "Exit swarmlancer" },
|
|
12
|
+
];
|
|
13
|
+
export class DashboardScreen {
|
|
14
|
+
container;
|
|
15
|
+
statusPanel;
|
|
16
|
+
selectList;
|
|
17
|
+
tui;
|
|
18
|
+
onAction;
|
|
19
|
+
constructor(tui, status) {
|
|
20
|
+
this.tui = tui;
|
|
21
|
+
this.container = new Container();
|
|
22
|
+
// Banner
|
|
23
|
+
this.container.addChild(new BannerComponent());
|
|
24
|
+
// Status
|
|
25
|
+
this.statusPanel = new StatusPanel(status);
|
|
26
|
+
this.container.addChild(this.statusPanel);
|
|
27
|
+
this.container.addChild(new Spacer(1));
|
|
28
|
+
// Menu
|
|
29
|
+
this.container.addChild(new Text(colors.bold(" What do you want to do?"), 1, 0));
|
|
30
|
+
this.container.addChild(new Spacer(1));
|
|
31
|
+
this.selectList = new SelectList(MENU_ITEMS, MENU_ITEMS.length, {
|
|
32
|
+
selectedPrefix: (t) => theme.accent(t),
|
|
33
|
+
selectedText: (t) => theme.accent(t),
|
|
34
|
+
description: (t) => colors.gray(t),
|
|
35
|
+
scrollInfo: (t) => colors.gray(t),
|
|
36
|
+
noMatch: (t) => colors.yellow(t),
|
|
37
|
+
});
|
|
38
|
+
this.selectList.onSelect = (item) => {
|
|
39
|
+
this.onAction?.(item.value);
|
|
40
|
+
};
|
|
41
|
+
this.container.addChild(this.selectList);
|
|
42
|
+
this.container.addChild(new Spacer(1));
|
|
43
|
+
this.container.addChild(new Text(colors.gray(" ↑↓ navigate • enter select • q quit"), 1, 0));
|
|
44
|
+
}
|
|
45
|
+
updateStatus(info) {
|
|
46
|
+
this.statusPanel.update(info);
|
|
47
|
+
}
|
|
48
|
+
handleInput(data) {
|
|
49
|
+
if (matchesKey(data, "q")) {
|
|
50
|
+
this.onAction?.("quit");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.selectList.handleInput(data);
|
|
54
|
+
this.tui.requestRender();
|
|
55
|
+
}
|
|
56
|
+
render(width) {
|
|
57
|
+
return this.container.render(width);
|
|
58
|
+
}
|
|
59
|
+
invalidate() {
|
|
60
|
+
this.container.invalidate();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Component } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
+
/**
|
|
4
|
+
* Simple message screen with "press any key" dismissal.
|
|
5
|
+
* Used for errors, info, confirmations.
|
|
6
|
+
*/
|
|
7
|
+
export declare class MessageScreen implements Component {
|
|
8
|
+
private container;
|
|
9
|
+
private tui;
|
|
10
|
+
onClose?: () => void;
|
|
11
|
+
constructor(tui: TUI, title: string, lines: string[], style?: "info" | "error" | "success");
|
|
12
|
+
handleInput(_data: string): void;
|
|
13
|
+
render(width: number): string[];
|
|
14
|
+
invalidate(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Container, Text, Spacer, } from "@mariozechner/pi-tui";
|
|
2
|
+
import { colors, theme } from "../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Simple message screen with "press any key" dismissal.
|
|
5
|
+
* Used for errors, info, confirmations.
|
|
6
|
+
*/
|
|
7
|
+
export class MessageScreen {
|
|
8
|
+
container;
|
|
9
|
+
tui;
|
|
10
|
+
onClose;
|
|
11
|
+
constructor(tui, title, lines, style = "info") {
|
|
12
|
+
this.tui = tui;
|
|
13
|
+
this.container = new Container();
|
|
14
|
+
const colorFn = style === "error"
|
|
15
|
+
? colors.red
|
|
16
|
+
: style === "success"
|
|
17
|
+
? colors.green
|
|
18
|
+
: colors.cyan;
|
|
19
|
+
this.container.addChild(new Spacer(1));
|
|
20
|
+
this.container.addChild(new Text(theme.border("─".repeat(50)), 0, 0));
|
|
21
|
+
this.container.addChild(new Text(colorFn(colors.bold(` ${title}`)), 1, 0));
|
|
22
|
+
this.container.addChild(new Spacer(1));
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
this.container.addChild(new Text(` ${line}`, 0, 0));
|
|
25
|
+
}
|
|
26
|
+
this.container.addChild(new Spacer(1));
|
|
27
|
+
this.container.addChild(new Text(colors.gray(" Press any key to continue"), 1, 0));
|
|
28
|
+
this.container.addChild(new Text(theme.border("─".repeat(50)), 0, 0));
|
|
29
|
+
}
|
|
30
|
+
handleInput(_data) {
|
|
31
|
+
this.onClose?.();
|
|
32
|
+
}
|
|
33
|
+
render(width) {
|
|
34
|
+
return this.container.render(width);
|
|
35
|
+
}
|
|
36
|
+
invalidate() {
|
|
37
|
+
this.container.invalidate();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Component } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
+
import type { Model, Api } from "@mariozechner/pi-ai";
|
|
4
|
+
export declare class ModelPickerScreen implements Component {
|
|
5
|
+
private container;
|
|
6
|
+
private selectList;
|
|
7
|
+
private tui;
|
|
8
|
+
onSelect?: (model: Model<Api>) => void;
|
|
9
|
+
onCancel?: () => void;
|
|
10
|
+
constructor(tui: TUI, models: Model<Api>[]);
|
|
11
|
+
handleInput(data: string): void;
|
|
12
|
+
render(width: number): string[];
|
|
13
|
+
invalidate(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Container, Text, Spacer, SelectList, } from "@mariozechner/pi-tui";
|
|
2
|
+
import { colors, theme } from "../theme.js";
|
|
3
|
+
export class ModelPickerScreen {
|
|
4
|
+
container;
|
|
5
|
+
selectList;
|
|
6
|
+
tui;
|
|
7
|
+
onSelect;
|
|
8
|
+
onCancel;
|
|
9
|
+
constructor(tui, models) {
|
|
10
|
+
this.tui = tui;
|
|
11
|
+
this.container = new Container();
|
|
12
|
+
this.container.addChild(new Spacer(1));
|
|
13
|
+
this.container.addChild(new Text(theme.border("─".repeat(50)), 0, 0));
|
|
14
|
+
this.container.addChild(new Text(theme.title(" Pick a model"), 1, 0));
|
|
15
|
+
this.container.addChild(new Spacer(1));
|
|
16
|
+
const items = models.map((m) => ({
|
|
17
|
+
value: m.id,
|
|
18
|
+
label: `${m.name}`,
|
|
19
|
+
description: `${m.provider}/${m.id}`,
|
|
20
|
+
}));
|
|
21
|
+
this.selectList = new SelectList(items, Math.min(items.length, 15), {
|
|
22
|
+
selectedPrefix: (t) => theme.accent(t),
|
|
23
|
+
selectedText: (t) => theme.accent(t),
|
|
24
|
+
description: (t) => colors.gray(t),
|
|
25
|
+
scrollInfo: (t) => colors.gray(t),
|
|
26
|
+
noMatch: (t) => colors.yellow(t),
|
|
27
|
+
});
|
|
28
|
+
this.selectList.onSelect = (item) => {
|
|
29
|
+
const model = models.find((m) => m.id === item.value);
|
|
30
|
+
if (model)
|
|
31
|
+
this.onSelect?.(model);
|
|
32
|
+
};
|
|
33
|
+
this.selectList.onCancel = () => this.onCancel?.();
|
|
34
|
+
this.container.addChild(this.selectList);
|
|
35
|
+
this.container.addChild(new Spacer(1));
|
|
36
|
+
this.container.addChild(new Text(colors.gray(" ↑↓ navigate • enter select • esc back"), 1, 0));
|
|
37
|
+
this.container.addChild(new Text(theme.border("─".repeat(50)), 0, 0));
|
|
38
|
+
}
|
|
39
|
+
handleInput(data) {
|
|
40
|
+
this.selectList.handleInput(data);
|
|
41
|
+
this.tui.requestRender();
|
|
42
|
+
}
|
|
43
|
+
render(width) {
|
|
44
|
+
return this.container.render(width);
|
|
45
|
+
}
|
|
46
|
+
invalidate() {
|
|
47
|
+
this.container.invalidate();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Component } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { TUI } from "@mariozechner/pi-tui";
|
|
3
|
+
export declare class ProfileEditorScreen implements Component {
|
|
4
|
+
private tui;
|
|
5
|
+
private fields;
|
|
6
|
+
private editors;
|
|
7
|
+
private currentField;
|
|
8
|
+
private cachedLines?;
|
|
9
|
+
onSave?: (data: Record<string, string>) => void;
|
|
10
|
+
onCancel?: () => void;
|
|
11
|
+
constructor(tui: TUI, profile: Record<string, string>);
|
|
12
|
+
private getResult;
|
|
13
|
+
handleInput(data: string): void;
|
|
14
|
+
render(width: number): string[];
|
|
15
|
+
invalidate(): void;
|
|
16
|
+
}
|