swarmlancer-cli 0.2.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.
Files changed (48) hide show
  1. package/README.md +15 -0
  2. package/dist/agent.d.ts +13 -0
  3. package/dist/agent.js +202 -0
  4. package/dist/app.d.ts +4 -0
  5. package/dist/app.js +496 -0
  6. package/dist/config.d.ts +49 -0
  7. package/dist/config.js +175 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +129 -0
  10. package/dist/inference.d.ts +13 -0
  11. package/dist/inference.js +105 -0
  12. package/dist/login.d.ts +1 -0
  13. package/dist/login.js +57 -0
  14. package/dist/screening.d.ts +22 -0
  15. package/dist/screening.js +101 -0
  16. package/dist/screens/agent-config.d.ts +14 -0
  17. package/dist/screens/agent-config.js +64 -0
  18. package/dist/screens/agent-editor.d.ts +13 -0
  19. package/dist/screens/agent-editor.js +64 -0
  20. package/dist/screens/agent-list.d.ts +22 -0
  21. package/dist/screens/agent-list.js +73 -0
  22. package/dist/screens/agent-picker.d.ts +15 -0
  23. package/dist/screens/agent-picker.js +51 -0
  24. package/dist/screens/agent-running.d.ts +20 -0
  25. package/dist/screens/agent-running.js +68 -0
  26. package/dist/screens/banner.d.ts +6 -0
  27. package/dist/screens/banner.js +27 -0
  28. package/dist/screens/dashboard.d.ts +16 -0
  29. package/dist/screens/dashboard.js +59 -0
  30. package/dist/screens/discovery-settings.d.ts +17 -0
  31. package/dist/screens/discovery-settings.js +189 -0
  32. package/dist/screens/message.d.ts +15 -0
  33. package/dist/screens/message.js +39 -0
  34. package/dist/screens/model-picker.d.ts +14 -0
  35. package/dist/screens/model-picker.js +49 -0
  36. package/dist/screens/name-editor.d.ts +15 -0
  37. package/dist/screens/name-editor.js +67 -0
  38. package/dist/screens/session-goal.d.ts +13 -0
  39. package/dist/screens/session-goal.js +61 -0
  40. package/dist/screens/settings.d.ts +15 -0
  41. package/dist/screens/settings.js +126 -0
  42. package/dist/screens/setup-wizard.d.ts +20 -0
  43. package/dist/screens/setup-wizard.js +120 -0
  44. package/dist/screens/status-panel.d.ts +15 -0
  45. package/dist/screens/status-panel.js +37 -0
  46. package/dist/theme.d.ts +42 -0
  47. package/dist/theme.js +56 -0
  48. package/package.json +49 -0
@@ -0,0 +1,189 @@
1
+ import { Input, matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
2
+ import { colors, theme } from "../theme.js";
3
+ const FIELDS = [
4
+ {
5
+ key: "matchThreshold",
6
+ label: "Match threshold",
7
+ description: "1-10 — how picky your agent is (higher = fewer, better matches)",
8
+ type: "number", min: 1, max: 10, step: 1, suffix: "/10",
9
+ },
10
+ {
11
+ key: "maxScreenPerSession",
12
+ label: "Max profiles to screen",
13
+ description: "Don't burn tokens evaluating more than this per session",
14
+ type: "number", min: 5, max: 200, step: 5, suffix: "",
15
+ },
16
+ {
17
+ key: "recontactAfterDays",
18
+ label: "Re-contact after days",
19
+ description: "0 = only new people, 30 = re-engage after a month",
20
+ type: "number", min: 0, max: 365, step: 5, suffix: "d",
21
+ },
22
+ {
23
+ key: "onlineOnly",
24
+ label: "Online only",
25
+ description: "Only discover users with agents currently running",
26
+ type: "boolean",
27
+ },
28
+ {
29
+ key: "includeKeywords",
30
+ label: "Include keywords",
31
+ description: "Only profiles mentioning these (comma-separated, empty = all)",
32
+ type: "keywords",
33
+ },
34
+ {
35
+ key: "excludeKeywords",
36
+ label: "Exclude keywords",
37
+ description: "Skip profiles mentioning these (comma-separated)",
38
+ type: "keywords",
39
+ },
40
+ ];
41
+ export class DiscoverySettingsScreen {
42
+ tui;
43
+ settings;
44
+ selectedIndex = 0;
45
+ editingKeywords = false;
46
+ keywordInput;
47
+ cachedRender;
48
+ onSave;
49
+ onCancel;
50
+ constructor(tui, settings) {
51
+ this.tui = tui;
52
+ this.settings = { ...settings };
53
+ this.keywordInput = new Input();
54
+ }
55
+ handleInput(data) {
56
+ // Keyword editing mode
57
+ if (this.editingKeywords) {
58
+ if (matchesKey(data, Key.enter)) {
59
+ const field = FIELDS[this.selectedIndex];
60
+ const val = this.keywordInput.getValue().split(",").map((s) => s.trim()).filter(Boolean);
61
+ this.settings[field.key] = val;
62
+ this.editingKeywords = false;
63
+ this.cachedRender = undefined;
64
+ this.tui.requestRender();
65
+ return;
66
+ }
67
+ if (matchesKey(data, Key.escape)) {
68
+ this.editingKeywords = false;
69
+ this.cachedRender = undefined;
70
+ this.tui.requestRender();
71
+ return;
72
+ }
73
+ this.keywordInput.handleInput(data);
74
+ this.cachedRender = undefined;
75
+ this.tui.requestRender();
76
+ return;
77
+ }
78
+ if (matchesKey(data, Key.escape)) {
79
+ this.onCancel?.();
80
+ return;
81
+ }
82
+ if (matchesKey(data, Key.ctrl("s"))) {
83
+ this.onSave?.({ ...this.settings });
84
+ return;
85
+ }
86
+ // Navigate
87
+ if (matchesKey(data, Key.up)) {
88
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
89
+ this.cachedRender = undefined;
90
+ this.tui.requestRender();
91
+ return;
92
+ }
93
+ if (matchesKey(data, Key.down)) {
94
+ this.selectedIndex = Math.min(FIELDS.length - 1, this.selectedIndex + 1);
95
+ this.cachedRender = undefined;
96
+ this.tui.requestRender();
97
+ return;
98
+ }
99
+ const field = FIELDS[this.selectedIndex];
100
+ // Enter to edit keywords or save
101
+ if (matchesKey(data, Key.enter)) {
102
+ if (field.type === "keywords") {
103
+ this.editingKeywords = true;
104
+ this.keywordInput.setValue(this.settings[field.key].join(", "));
105
+ this.cachedRender = undefined;
106
+ this.tui.requestRender();
107
+ return;
108
+ }
109
+ this.onSave?.({ ...this.settings });
110
+ return;
111
+ }
112
+ // Adjust values with left/right
113
+ if (matchesKey(data, Key.right) || matchesKey(data, Key.left)) {
114
+ const dir = matchesKey(data, Key.right) ? 1 : -1;
115
+ if (field.type === "boolean") {
116
+ this.settings[field.key] = !this.settings[field.key];
117
+ }
118
+ else if (field.type === "number") {
119
+ const step = field.step ?? 1;
120
+ const min = field.min ?? 0;
121
+ const max = field.max ?? 999;
122
+ const cur = this.settings[field.key];
123
+ this.settings[field.key] = Math.max(min, Math.min(max, cur + dir * step));
124
+ }
125
+ this.cachedRender = undefined;
126
+ this.tui.requestRender();
127
+ }
128
+ }
129
+ render(width) {
130
+ if (this.cachedRender)
131
+ return this.cachedRender;
132
+ const lines = [];
133
+ lines.push("");
134
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
135
+ lines.push(truncateToWidth(theme.title(" Discovery Settings"), width));
136
+ lines.push(truncateToWidth(colors.gray(" Control how your agent finds people to talk to."), width));
137
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
138
+ lines.push("");
139
+ for (let i = 0; i < FIELDS.length; i++) {
140
+ const f = FIELDS[i];
141
+ const isActive = i === this.selectedIndex;
142
+ const prefix = isActive ? theme.accent("▸ ") : " ";
143
+ const label = isActive ? theme.accent(f.label) : f.label;
144
+ if (f.type === "number") {
145
+ const value = this.settings[f.key];
146
+ const pct = ((value - (f.min ?? 0)) / ((f.max ?? 100) - (f.min ?? 0)));
147
+ const barW = 16;
148
+ const filled = Math.round(pct * barW);
149
+ const bar = colors.cyan("█".repeat(filled)) + colors.gray("░".repeat(barW - filled));
150
+ const valStr = `${value}${f.suffix ?? ""}`;
151
+ lines.push(truncateToWidth(`${prefix}${label}`, width));
152
+ lines.push(truncateToWidth(` ${colors.gray("◀")} ${bar} ${colors.gray("▶")} ${isActive ? colors.brightCyan(valStr) : colors.gray(valStr)}`, width));
153
+ }
154
+ else if (f.type === "boolean") {
155
+ const value = this.settings[f.key];
156
+ const valStr = value ? colors.green("ON") : colors.red("OFF");
157
+ lines.push(truncateToWidth(`${prefix}${label} ${colors.gray("◀")} ${valStr} ${colors.gray("▶")}`, width));
158
+ }
159
+ else if (f.type === "keywords") {
160
+ const value = this.settings[f.key];
161
+ const display = value.length > 0 ? value.join(", ") : colors.gray("(none)");
162
+ lines.push(truncateToWidth(`${prefix}${label}`, width));
163
+ if (this.editingKeywords && isActive) {
164
+ const inputLines = this.keywordInput.render(width - 4);
165
+ for (const il of inputLines) {
166
+ lines.push(truncateToWidth(` ${il}`, width));
167
+ }
168
+ }
169
+ else {
170
+ lines.push(truncateToWidth(` ${display}`, width));
171
+ }
172
+ }
173
+ if (isActive) {
174
+ lines.push(truncateToWidth(` ${colors.gray(f.description)}`, width));
175
+ }
176
+ lines.push("");
177
+ }
178
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
179
+ const help = this.editingKeywords
180
+ ? colors.gray(" type keywords, comma-separated • enter confirm • esc cancel")
181
+ : colors.gray(" ↑↓ select • ←→ adjust • enter edit/save • ctrl+s save • esc back");
182
+ lines.push(truncateToWidth(help, width));
183
+ this.cachedRender = lines;
184
+ return lines;
185
+ }
186
+ invalidate() {
187
+ this.cachedRender = undefined;
188
+ }
189
+ }
@@ -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,15 @@
1
+ import { type Component } from "@mariozechner/pi-tui";
2
+ import type { TUI } from "@mariozechner/pi-tui";
3
+ export declare class NameEditorScreen implements Component {
4
+ private tui;
5
+ private editor;
6
+ private cachedLines?;
7
+ private title;
8
+ private subtitle;
9
+ onSave?: (name: string) => void;
10
+ onCancel?: () => void;
11
+ constructor(tui: TUI, currentName: string, title?: string, subtitle?: string);
12
+ handleInput(data: string): void;
13
+ render(width: number): string[];
14
+ invalidate(): void;
15
+ }
@@ -0,0 +1,67 @@
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 NameEditorScreen {
14
+ tui;
15
+ editor;
16
+ cachedLines;
17
+ title;
18
+ subtitle;
19
+ onSave;
20
+ onCancel;
21
+ constructor(tui, currentName, title = "Agent Name", subtitle = "Give your agent a name to identify it.") {
22
+ this.tui = tui;
23
+ this.title = title;
24
+ this.subtitle = subtitle;
25
+ this.editor = new Editor(tui, EDITOR_THEME, { paddingX: 1 });
26
+ this.editor.setText(currentName);
27
+ this.editor.disableSubmit = false; // Enter submits
28
+ this.editor.onSubmit = (text) => {
29
+ const trimmed = text.trim();
30
+ if (trimmed.length > 0) {
31
+ this.onSave?.(trimmed);
32
+ }
33
+ };
34
+ }
35
+ handleInput(data) {
36
+ if (matchesKey(data, Key.escape)) {
37
+ this.onCancel?.();
38
+ return;
39
+ }
40
+ this.editor.handleInput(data);
41
+ this.cachedLines = undefined;
42
+ this.tui.requestRender();
43
+ }
44
+ render(width) {
45
+ if (this.cachedLines)
46
+ return this.cachedLines;
47
+ const lines = [];
48
+ lines.push("");
49
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
50
+ lines.push(truncateToWidth(theme.title(` ${this.title}`), width));
51
+ lines.push(truncateToWidth(colors.gray(` ${this.subtitle}`), width));
52
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
53
+ lines.push("");
54
+ for (const line of this.editor.render(width)) {
55
+ lines.push(line);
56
+ }
57
+ lines.push("");
58
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
59
+ lines.push(truncateToWidth(colors.gray(" enter save • esc cancel"), width));
60
+ this.cachedLines = lines;
61
+ return lines;
62
+ }
63
+ invalidate() {
64
+ this.cachedLines = undefined;
65
+ this.editor.invalidate();
66
+ }
67
+ }
@@ -0,0 +1,13 @@
1
+ import { type Component } from "@mariozechner/pi-tui";
2
+ import type { TUI } from "@mariozechner/pi-tui";
3
+ export declare class SessionGoalScreen implements Component {
4
+ private tui;
5
+ private editor;
6
+ private cachedRender?;
7
+ onSubmit?: (goal: string) => void;
8
+ onSkip?: () => void;
9
+ constructor(tui: TUI);
10
+ handleInput(data: string): void;
11
+ render(width: number): string[];
12
+ invalidate(): void;
13
+ }
@@ -0,0 +1,61 @@
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 SessionGoalScreen {
14
+ tui;
15
+ editor;
16
+ cachedRender;
17
+ onSubmit;
18
+ onSkip;
19
+ constructor(tui) {
20
+ this.tui = tui;
21
+ this.editor = new Editor(tui, EDITOR_THEME, { paddingX: 1 });
22
+ this.editor.disableSubmit = false; // Enter submits
23
+ this.editor.onSubmit = (text) => {
24
+ this.onSubmit?.(text);
25
+ };
26
+ }
27
+ handleInput(data) {
28
+ if (matchesKey(data, Key.escape)) {
29
+ this.onSkip?.();
30
+ return;
31
+ }
32
+ this.editor.handleInput(data);
33
+ this.cachedRender = undefined;
34
+ this.tui.requestRender();
35
+ }
36
+ render(width) {
37
+ if (this.cachedRender)
38
+ return this.cachedRender;
39
+ const lines = [];
40
+ lines.push("");
41
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
42
+ lines.push(truncateToWidth(theme.title(" What are you looking for today?"), width));
43
+ lines.push(truncateToWidth(colors.gray(" Optional — sharpens who your agent reaches out to. Leave empty to use defaults from agent.md."), width));
44
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
45
+ lines.push("");
46
+ lines.push(truncateToWidth(colors.gray(' Examples: "Rust developers for CLI collaboration" or "Anyone doing AI agents"'), width));
47
+ lines.push("");
48
+ for (const line of this.editor.render(width)) {
49
+ lines.push(line);
50
+ }
51
+ lines.push("");
52
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
53
+ lines.push(truncateToWidth(colors.gray(" enter submit • esc skip (use defaults)"), width));
54
+ this.cachedRender = lines;
55
+ return lines;
56
+ }
57
+ invalidate() {
58
+ this.cachedRender = undefined;
59
+ this.editor.invalidate();
60
+ }
61
+ }
@@ -0,0 +1,15 @@
1
+ import { type Component } from "@mariozechner/pi-tui";
2
+ import type { TUI } from "@mariozechner/pi-tui";
3
+ import type { AgentLimits } from "../config.js";
4
+ export declare class SettingsScreen implements Component {
5
+ private tui;
6
+ private limits;
7
+ private selectedIndex;
8
+ private cachedRender?;
9
+ onSave?: (limits: AgentLimits) => void;
10
+ onCancel?: () => void;
11
+ constructor(tui: TUI, limits: AgentLimits);
12
+ handleInput(data: string): void;
13
+ render(width: number): string[];
14
+ invalidate(): void;
15
+ }
@@ -0,0 +1,126 @@
1
+ import { matchesKey, Key, truncateToWidth, } from "@mariozechner/pi-tui";
2
+ import { colors, theme } from "../theme.js";
3
+ const SETTINGS = [
4
+ {
5
+ key: "maxConcurrentConversations",
6
+ label: "Max concurrent conversations",
7
+ description: "How many conversations at once",
8
+ min: 1, max: 10, step: 1, suffix: "",
9
+ },
10
+ {
11
+ key: "maxMessagesPerConversation",
12
+ label: "Max messages per conversation",
13
+ description: "Stop a conversation after this many messages",
14
+ min: 2, max: 50, step: 2, suffix: " msgs",
15
+ },
16
+ {
17
+ key: "maxResponseLength",
18
+ label: "Max response length",
19
+ description: "Truncate agent responses beyond this",
20
+ min: 200, max: 10000, step: 200, suffix: " chars",
21
+ },
22
+ {
23
+ key: "cooldownSeconds",
24
+ label: "Cooldown between conversations",
25
+ description: "Wait before starting the next one",
26
+ min: 0, max: 300, step: 10, suffix: "s",
27
+ },
28
+ {
29
+ key: "maxConversationsPerSession",
30
+ label: "Max conversations per session",
31
+ description: "Agent stops after this many total",
32
+ min: 1, max: 500, step: 10, suffix: "",
33
+ },
34
+ {
35
+ key: "autoStopIdleMinutes",
36
+ label: "Auto-stop after idle",
37
+ description: "Disconnect if no conversations for this long",
38
+ min: 5, max: 1440, step: 5, suffix: " min",
39
+ },
40
+ ];
41
+ export class SettingsScreen {
42
+ tui;
43
+ limits;
44
+ selectedIndex = 0;
45
+ cachedRender;
46
+ onSave;
47
+ onCancel;
48
+ constructor(tui, limits) {
49
+ this.tui = tui;
50
+ this.limits = { ...limits };
51
+ }
52
+ handleInput(data) {
53
+ if (matchesKey(data, Key.escape)) {
54
+ this.onCancel?.();
55
+ return;
56
+ }
57
+ if (matchesKey(data, Key.ctrl("s")) || matchesKey(data, Key.enter)) {
58
+ this.onSave?.({ ...this.limits });
59
+ return;
60
+ }
61
+ // Navigate settings
62
+ if (matchesKey(data, Key.up)) {
63
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
64
+ this.cachedRender = undefined;
65
+ this.tui.requestRender();
66
+ return;
67
+ }
68
+ if (matchesKey(data, Key.down)) {
69
+ this.selectedIndex = Math.min(SETTINGS.length - 1, this.selectedIndex + 1);
70
+ this.cachedRender = undefined;
71
+ this.tui.requestRender();
72
+ return;
73
+ }
74
+ // Adjust value
75
+ const setting = SETTINGS[this.selectedIndex];
76
+ if (matchesKey(data, Key.right)) {
77
+ this.limits[setting.key] = Math.min(setting.max, this.limits[setting.key] + setting.step);
78
+ this.cachedRender = undefined;
79
+ this.tui.requestRender();
80
+ return;
81
+ }
82
+ if (matchesKey(data, Key.left)) {
83
+ this.limits[setting.key] = Math.max(setting.min, this.limits[setting.key] - setting.step);
84
+ this.cachedRender = undefined;
85
+ this.tui.requestRender();
86
+ return;
87
+ }
88
+ }
89
+ render(width) {
90
+ if (this.cachedRender)
91
+ return this.cachedRender;
92
+ const lines = [];
93
+ lines.push("");
94
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
95
+ lines.push(truncateToWidth(theme.title(" Agent Limits"), width));
96
+ lines.push(truncateToWidth(colors.gray(" Control how your agent behaves in conversations."), width));
97
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
98
+ lines.push("");
99
+ for (let i = 0; i < SETTINGS.length; i++) {
100
+ const s = SETTINGS[i];
101
+ const isActive = i === this.selectedIndex;
102
+ const value = this.limits[s.key];
103
+ const prefix = isActive ? theme.accent("▸ ") : " ";
104
+ const label = isActive ? theme.accent(s.label) : s.label;
105
+ const valueStr = `${value}${s.suffix}`;
106
+ // Slider bar
107
+ const pct = (value - s.min) / (s.max - s.min);
108
+ const barWidth = 20;
109
+ const filled = Math.round(pct * barWidth);
110
+ const bar = colors.cyan("█".repeat(filled)) + colors.gray("░".repeat(barWidth - filled));
111
+ lines.push(truncateToWidth(`${prefix}${label}`, width));
112
+ lines.push(truncateToWidth(` ${colors.gray("◀")} ${bar} ${colors.gray("▶")} ${isActive ? colors.brightCyan(valueStr) : colors.gray(valueStr)}`, width));
113
+ if (isActive) {
114
+ lines.push(truncateToWidth(` ${colors.gray(s.description)}`, width));
115
+ }
116
+ lines.push("");
117
+ }
118
+ lines.push(truncateToWidth(theme.border("─".repeat(width)), width));
119
+ lines.push(truncateToWidth(colors.gray(" ↑↓ select • ←→ adjust • enter save • esc cancel"), width));
120
+ this.cachedRender = lines;
121
+ return lines;
122
+ }
123
+ invalidate() {
124
+ this.cachedRender = undefined;
125
+ }
126
+ }
@@ -0,0 +1,20 @@
1
+ import { type Component } from "@mariozechner/pi-tui";
2
+ import type { TUI } from "@mariozechner/pi-tui";
3
+ type StepStatus = "pending" | "running" | "done" | "failed" | "skipped";
4
+ export declare class SetupWizardScreen implements Component {
5
+ private tui;
6
+ private steps;
7
+ private cachedRender?;
8
+ private promptActive;
9
+ private promptText;
10
+ private promptList?;
11
+ private promptResolve?;
12
+ constructor(tui: TUI);
13
+ setStep(index: number, status: StepStatus, detail?: string): void;
14
+ /** Show a yes/no prompt and return the user's choice */
15
+ askConfirm(question: string): Promise<boolean>;
16
+ handleInput(data: string): void;
17
+ render(width: number): string[];
18
+ invalidate(): void;
19
+ }
20
+ export {};