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.
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export declare function getProfile(): Promise<Record<string, string> | null>;
2
+ export declare function isProfileComplete(profile: Record<string, string>): boolean;
3
+ export declare function saveProfile(data: Record<string, string>): Promise<void>;
@@ -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,6 @@
1
+ import { Container } from "@mariozechner/pi-tui";
2
+ export declare class BannerComponent extends Container {
3
+ constructor();
4
+ private rebuild;
5
+ invalidate(): void;
6
+ }
@@ -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
+ }