@hemansubedi/aether-ai 1.0.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/.gitattributes +3 -0
- package/.github/workflows/live-stats.yml +42 -0
- package/.github/workflows/publish.yml +34 -0
- package/.github/workflows/update-preview.yml +41 -0
- package/INSTALL.md +59 -0
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/assets/aether-arena.svg +72 -0
- package/assets/aether-banner.svg +62 -0
- package/assets/aether-router.svg +129 -0
- package/dist/agent.js +125 -0
- package/dist/arena.js +486 -0
- package/dist/checkpoint.js +105 -0
- package/dist/client.js +95 -0
- package/dist/combos.js +176 -0
- package/dist/commands.js +483 -0
- package/dist/config.js +104 -0
- package/dist/cost.js +176 -0
- package/dist/git.js +52 -0
- package/dist/health.js +81 -0
- package/dist/index.js +272 -0
- package/dist/keys.js +128 -0
- package/dist/memory.js +98 -0
- package/dist/modes.js +68 -0
- package/dist/providers/index.js +32 -0
- package/dist/providers/ollama.js +206 -0
- package/dist/providers/openai-compat.js +181 -0
- package/dist/providers/openrouter.js +189 -0
- package/dist/providers/registry.js +211 -0
- package/dist/router-engine.js +200 -0
- package/dist/router.js +171 -0
- package/dist/server.js +210 -0
- package/dist/session.js +97 -0
- package/dist/settings.js +97 -0
- package/dist/skills.js +100 -0
- package/dist/tokensaver.js +50 -0
- package/dist/tools/filesystem.js +243 -0
- package/dist/tools/git.js +53 -0
- package/dist/tools/glob.js +175 -0
- package/dist/tools/grep.js +193 -0
- package/dist/tools/registry.js +39 -0
- package/dist/tools/vision.js +140 -0
- package/dist/tools/websearch.js +118 -0
- package/dist/tui.js +562 -0
- package/dist/types.js +8 -0
- package/docs/preview.txt +51 -0
- package/docs/screenshots.md +110 -0
- package/docs/stats.md +5 -0
- package/install.ps1 +170 -0
- package/install.sh +196 -0
- package/package.json +34 -0
- package/scripts/generate-stats-card.ts +62 -0
- package/scripts/patch_index.ps1 +17 -0
- package/scripts/release.sh +7 -0
- package/src/agent.ts +146 -0
- package/src/arena.ts +584 -0
- package/src/checkpoint.ts +111 -0
- package/src/client.ts +172 -0
- package/src/combos.ts +199 -0
- package/src/commands.ts +973 -0
- package/src/config.ts +122 -0
- package/src/cost.ts +206 -0
- package/src/git.ts +68 -0
- package/src/health.ts +90 -0
- package/src/index.ts +281 -0
- package/src/keys.ts +135 -0
- package/src/memory.ts +101 -0
- package/src/modes.ts +84 -0
- package/src/providers/index.ts +59 -0
- package/src/providers/ollama.ts +222 -0
- package/src/providers/openai-compat.ts +188 -0
- package/src/providers/openrouter.ts +198 -0
- package/src/providers/registry.ts +223 -0
- package/src/router-engine.ts +214 -0
- package/src/router.ts +195 -0
- package/src/server.ts +242 -0
- package/src/session.ts +111 -0
- package/src/settings.ts +125 -0
- package/src/skills.ts +106 -0
- package/src/tokensaver.ts +57 -0
- package/src/tools/filesystem.ts +258 -0
- package/src/tools/git.ts +53 -0
- package/src/tools/glob.ts +180 -0
- package/src/tools/grep.ts +192 -0
- package/src/tools/registry.ts +54 -0
- package/src/tools/vision.ts +152 -0
- package/src/tools/websearch.ts +130 -0
- package/src/tui.ts +664 -0
- package/src/types.ts +77 -0
- package/tsconfig.json +16 -0
package/src/tui.ts
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { ChatChunk, Message } from "./types.js";
|
|
5
|
+
import { Agent } from "./agent.js";
|
|
6
|
+
import { Session } from "./session.js";
|
|
7
|
+
import { Router } from "./router.js";
|
|
8
|
+
import { Arena, type ArenaResult } from "./arena.js";
|
|
9
|
+
import { CommandHandler, parseCommand, type CommandContext } from "./commands.js";
|
|
10
|
+
import { getConfig } from "./config.js";
|
|
11
|
+
import { messageText } from "./types.js";
|
|
12
|
+
import { HealthTracker } from "./health.js";
|
|
13
|
+
import { ToolRegistry } from "./tools/registry.js";
|
|
14
|
+
import type { CostTracker } from "./cost.js";
|
|
15
|
+
import type { Settings } from "./settings.js";
|
|
16
|
+
import type { Skills } from "./skills.js";
|
|
17
|
+
import type { Checkpoint } from "./checkpoint.js";
|
|
18
|
+
import {
|
|
19
|
+
makeReadFileTool,
|
|
20
|
+
makeWriteFileTool,
|
|
21
|
+
makeEditFileTool,
|
|
22
|
+
makeListDirTool,
|
|
23
|
+
makeBashTool,
|
|
24
|
+
} from "./tools/filesystem.js";
|
|
25
|
+
|
|
26
|
+
const ESC = "\u001b";
|
|
27
|
+
const CURSOR_HIDE = `${ESC}[?25l`;
|
|
28
|
+
const CURSOR_SHOW = `${ESC}[?25h`;
|
|
29
|
+
const ALT_SCREEN = `${ESC}[?1049h`;
|
|
30
|
+
const ALT_SCREEN_OFF = `${ESC}[?1049l`;
|
|
31
|
+
const CLEAR = `${ESC}[2J${ESC}[H`;
|
|
32
|
+
const RESET = `${ESC}[0m`;
|
|
33
|
+
const BOLD = `${ESC}[1m`;
|
|
34
|
+
const DIM = `${ESC}[2m`;
|
|
35
|
+
const ITALIC = `${ESC}[3m`;
|
|
36
|
+
const UNDERLINE = `${ESC}[4m`;
|
|
37
|
+
|
|
38
|
+
const FG = {
|
|
39
|
+
red: `${ESC}[31m`,
|
|
40
|
+
green: `${ESC}[32m`,
|
|
41
|
+
yellow: `${ESC}[33m`,
|
|
42
|
+
blue: `${ESC}[34m`,
|
|
43
|
+
magenta: `${ESC}[35m`,
|
|
44
|
+
cyan: `${ESC}[36m`,
|
|
45
|
+
white: `${ESC}[37m`,
|
|
46
|
+
gray: `${ESC}[90m`,
|
|
47
|
+
brightRed: `${ESC}[91m`,
|
|
48
|
+
brightGreen: `${ESC}[92m`,
|
|
49
|
+
brightYellow: `${ESC}[93m`,
|
|
50
|
+
brightCyan: `${ESC}[96m`,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
const BG = {
|
|
55
|
+
blue: `\u001b[44m`,
|
|
56
|
+
darkGray: `\u001b[100m`,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const SPINNER = ['�', '?', '?', '?', '?', '?'];
|
|
60
|
+
|
|
61
|
+
interface Msg {
|
|
62
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
63
|
+
content: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class TUI {
|
|
67
|
+
readonly agent: Agent;
|
|
68
|
+
readonly router: Router;
|
|
69
|
+
readonly session: Session;
|
|
70
|
+
readonly arena: Arena;
|
|
71
|
+
readonly costTracker?: CostTracker;
|
|
72
|
+
readonly skills?: Skills;
|
|
73
|
+
readonly checkpoint?: Checkpoint;
|
|
74
|
+
readonly settings?: Settings;
|
|
75
|
+
|
|
76
|
+
private rl: readline.Interface;
|
|
77
|
+
private isTty: boolean;
|
|
78
|
+
private running = true;
|
|
79
|
+
private messages: Msg[] = [];
|
|
80
|
+
private inputLines: string[] = [""];
|
|
81
|
+
private inputCursor = 0;
|
|
82
|
+
private scrollOffset = 0;
|
|
83
|
+
private spinnerIdx = 0;
|
|
84
|
+
private spinnerTimer: NodeJS.Timeout | null = null;
|
|
85
|
+
private thinking = false;
|
|
86
|
+
private arenaMode = false;
|
|
87
|
+
private lastRenderHeight = 0;
|
|
88
|
+
private resizeTimer: NodeJS.Timeout | null = null;
|
|
89
|
+
|
|
90
|
+
constructor(opts: {
|
|
91
|
+
agent: Agent;
|
|
92
|
+
router: Router;
|
|
93
|
+
session: Session;
|
|
94
|
+
arena: Arena;
|
|
95
|
+
costTracker?: CostTracker;
|
|
96
|
+
skills?: Skills;
|
|
97
|
+
checkpoint?: Checkpoint;
|
|
98
|
+
settings?: Settings;
|
|
99
|
+
}) {
|
|
100
|
+
this.agent = opts.agent;
|
|
101
|
+
this.router = opts.router;
|
|
102
|
+
this.session = opts.session;
|
|
103
|
+
this.arena = opts.arena;
|
|
104
|
+
this.costTracker = opts.costTracker;
|
|
105
|
+
this.skills = opts.skills;
|
|
106
|
+
this.checkpoint = opts.checkpoint;
|
|
107
|
+
this.settings = opts.settings;
|
|
108
|
+
|
|
109
|
+
this.isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
110
|
+
this.rl = readline.createInterface({
|
|
111
|
+
input: process.stdin,
|
|
112
|
+
output: process.stdout,
|
|
113
|
+
terminal: false,
|
|
114
|
+
crlfDelay: Infinity,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
for (const m of this.session.messages) {
|
|
118
|
+
if (m.role === "system") continue;
|
|
119
|
+
this.messages.push({ role: m.role as Msg["role"], content: messageText(m) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
showSystem(text: string): void {
|
|
124
|
+
this.messages.push({ role: "system", content: text });
|
|
125
|
+
this.render();
|
|
126
|
+
if (!this.isTty) process.stdout.write(text + "\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
exit(): void {
|
|
130
|
+
this.running = false;
|
|
131
|
+
this.cleanup();
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
enterArena(): void {
|
|
136
|
+
this.arenaMode = true;
|
|
137
|
+
this.messages.push({
|
|
138
|
+
role: "system",
|
|
139
|
+
content:
|
|
140
|
+
"Arena mode: type a prompt to compare models side-by-side. /arena exit to return to chat.",
|
|
141
|
+
});
|
|
142
|
+
this.render();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
exitArena(): void {
|
|
146
|
+
this.arenaMode = false;
|
|
147
|
+
this.messages.push({ role: "system", content: "Returned to chat mode." });
|
|
148
|
+
this.render();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
start(): void {
|
|
152
|
+
if (!this.isTty) {
|
|
153
|
+
this.runNonTty();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.initTty();
|
|
157
|
+
this.render();
|
|
158
|
+
this.bindKeys();
|
|
159
|
+
this.prompt();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private initTty(): void {
|
|
163
|
+
process.stdin.setEncoding("utf8");
|
|
164
|
+
process.stdin.resume();
|
|
165
|
+
process.stdout.write(CURSOR_HIDE);
|
|
166
|
+
process.stdout.write(ALT_SCREEN);
|
|
167
|
+
process.stdin.on("resize", () => this.scheduleResize());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private scheduleResize(): void {
|
|
171
|
+
if (this.resizeTimer) clearTimeout(this.resizeTimer);
|
|
172
|
+
this.resizeTimer = setTimeout(() => {
|
|
173
|
+
this.scrollOffset = 0;
|
|
174
|
+
this.render();
|
|
175
|
+
}, 100);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private cleanup(): void {
|
|
179
|
+
if (this.spinnerTimer) {
|
|
180
|
+
clearInterval(this.spinnerTimer);
|
|
181
|
+
this.spinnerTimer = null;
|
|
182
|
+
}
|
|
183
|
+
if (this.resizeTimer) {
|
|
184
|
+
clearTimeout(this.resizeTimer);
|
|
185
|
+
this.resizeTimer = null;
|
|
186
|
+
}
|
|
187
|
+
process.stdout.write(CURSOR_SHOW);
|
|
188
|
+
process.stdout.write(ALT_SCREEN_OFF);
|
|
189
|
+
process.stdout.write(RESET);
|
|
190
|
+
this.rl.close();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private runNonTty(): void {
|
|
194
|
+
this.thinking = false;
|
|
195
|
+
this.rl.on("line", async (line: string) => {
|
|
196
|
+
await this.handleLine(line);
|
|
197
|
+
});
|
|
198
|
+
this.rl.on("close", () => {
|
|
199
|
+
this.running = false;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private prompt(): void {
|
|
204
|
+
if (!this.isTty) return;
|
|
205
|
+
this.render();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private bindKeys(): void {
|
|
209
|
+
process.stdin.on("data", (data: Buffer) => {
|
|
210
|
+
this.handleRawInput(data.toString("utf8"));
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private termWidth(): number {
|
|
215
|
+
return process.stdout.columns || 80;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private termHeight(): number {
|
|
219
|
+
return process.stdout.rows || 24;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private wrap(text: string, width: number): string[] {
|
|
223
|
+
const lines: string[] = [];
|
|
224
|
+
for (const para of text.split("\n")) {
|
|
225
|
+
if (para.length === 0) {
|
|
226
|
+
lines.push("");
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
let start = 0;
|
|
230
|
+
while (start < para.length) {
|
|
231
|
+
const chunk = para.slice(start, start + width);
|
|
232
|
+
lines.push(chunk);
|
|
233
|
+
start += width;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return lines;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private renderMessage(m: Msg, width: number): string[] {
|
|
240
|
+
const color =
|
|
241
|
+
m.role === "user"
|
|
242
|
+
? FG.brightCyan
|
|
243
|
+
: m.role === "assistant"
|
|
244
|
+
? FG.brightGreen
|
|
245
|
+
: m.role === "tool"
|
|
246
|
+
? FG.gray
|
|
247
|
+
: FG.brightYellow;
|
|
248
|
+
const label =
|
|
249
|
+
m.role === "user"
|
|
250
|
+
? "You"
|
|
251
|
+
: m.role === "assistant"
|
|
252
|
+
? "Assistant"
|
|
253
|
+
: m.role === "tool"
|
|
254
|
+
? "Tool"
|
|
255
|
+
: "System";
|
|
256
|
+
const out: string[] = [];
|
|
257
|
+
out.push(`${color}${label}${RESET}`);
|
|
258
|
+
const body = this.wrap(m.content, width - 2);
|
|
259
|
+
for (const line of body) {
|
|
260
|
+
out.push(`${DIM}${line}${RESET}`);
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private banner(): string {
|
|
266
|
+
const provider = this.router.getActiveProvider() ?? "auto";
|
|
267
|
+
const model = this.router.getActiveModel() ?? "(default)";
|
|
268
|
+
const mode = this.arenaMode ? "ARENA" : "CHAT";
|
|
269
|
+
const line = `aether | ${mode} | ${provider} | ${model}`;
|
|
270
|
+
const pad = Math.max(0, this.termWidth() - line.length);
|
|
271
|
+
return `${BG.blue}${FG.white}${BOLD} ${line}${" ".repeat(pad)} ${RESET}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private render(): void {
|
|
275
|
+
if (!this.isTty) return;
|
|
276
|
+
const width = this.termWidth();
|
|
277
|
+
const height = this.termHeight();
|
|
278
|
+
|
|
279
|
+
const rendered: string[] = [];
|
|
280
|
+
rendered.push(this.banner());
|
|
281
|
+
rendered.push(`${FG.gray}${"-".repeat(width)}${RESET}`);
|
|
282
|
+
|
|
283
|
+
const bodyWidth = width - 2;
|
|
284
|
+
const allLines: string[] = [];
|
|
285
|
+
const start = Math.max(0, this.messages.length - 60);
|
|
286
|
+
for (let i = start; i < this.messages.length; i++) {
|
|
287
|
+
allLines.push(...this.renderMessage(this.messages[i], bodyWidth));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Apply scroll offset (scroll down to newest by default).
|
|
291
|
+
const maxScroll = Math.max(0, allLines.length - (height - 10));
|
|
292
|
+
if (this.scrollOffset > maxScroll) this.scrollOffset = maxScroll;
|
|
293
|
+
const visible = allLines.slice(this.scrollOffset, this.scrollOffset + height - 10);
|
|
294
|
+
|
|
295
|
+
for (const line of visible) {
|
|
296
|
+
rendered.push(` ${line}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
while (rendered.length < height - 4) {
|
|
300
|
+
rendered.push("");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (this.thinking) {
|
|
304
|
+
const sp = SPINNER[this.spinnerIdx % SPINNER.length];
|
|
305
|
+
rendered.push(`${FG.brightYellow}${sp} thinking...${RESET}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Input box.
|
|
309
|
+
rendered.push(`${FG.gray}${"-".repeat(width)}${RESET}`);
|
|
310
|
+
const inputText = this.inputLines.join("\n");
|
|
311
|
+
const inputDisplay = this.wrap(inputText || "", width - 4);
|
|
312
|
+
if (inputDisplay.length === 0) inputDisplay.push("");
|
|
313
|
+
for (const line of inputDisplay) {
|
|
314
|
+
rendered.push(` ${FG.brightCyan}${line}${RESET}`);
|
|
315
|
+
}
|
|
316
|
+
rendered.push(`${FG.brightCyan}> ${RESET}`);
|
|
317
|
+
|
|
318
|
+
process.stdout.write(CLEAR);
|
|
319
|
+
process.stdout.write(rendered.join("\n"));
|
|
320
|
+
this.lastRenderHeight = rendered.length;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private scrollUp(n = 1): void {
|
|
324
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - n);
|
|
325
|
+
this.render();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private scrollDown(n = 1): void {
|
|
329
|
+
this.scrollOffset += n;
|
|
330
|
+
this.render();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private startSpinner(): void {
|
|
334
|
+
if (this.spinnerTimer) return;
|
|
335
|
+
this.thinking = true;
|
|
336
|
+
this.spinnerIdx = 0;
|
|
337
|
+
this.spinnerTimer = setInterval(() => {
|
|
338
|
+
this.spinnerIdx++;
|
|
339
|
+
this.render();
|
|
340
|
+
}, 120);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private stopSpinner(): void {
|
|
344
|
+
if (this.spinnerTimer) {
|
|
345
|
+
clearInterval(this.spinnerTimer);
|
|
346
|
+
this.spinnerTimer = null;
|
|
347
|
+
}
|
|
348
|
+
this.thinking = false;
|
|
349
|
+
}
|
|
350
|
+
private handleRawInput(s: string): void {
|
|
351
|
+
if (s === "\u0003") {
|
|
352
|
+
// Ctrl+C: clear input
|
|
353
|
+
this.inputLines = [""];
|
|
354
|
+
this.inputCursor = 0;
|
|
355
|
+
this.render();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (s === "\u001b") {
|
|
359
|
+
// Esc: clear input line
|
|
360
|
+
this.inputLines = [""];
|
|
361
|
+
this.inputCursor = 0;
|
|
362
|
+
this.render();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (s === "\u001b[A") {
|
|
366
|
+
this.scrollUp(2);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (s === "\u001b[B") {
|
|
370
|
+
this.scrollDown(2);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (s === "\u001b[C") {
|
|
374
|
+
// right arrow: noop for now
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (s === "\u001b[D") {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (s === "\u000c") {
|
|
381
|
+
// Ctrl+L: redraw
|
|
382
|
+
this.render();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (s === "\u0004") {
|
|
386
|
+
// Ctrl+D: exit
|
|
387
|
+
this.exit();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (s === "\r" || s === "\n") {
|
|
391
|
+
this.submitLine();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (s === "\u0008" || s === "\u007f") {
|
|
395
|
+
// backspace
|
|
396
|
+
this.backspace();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (s === "\t") {
|
|
400
|
+
this.cycleProvider();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (s === "\u001b\r" || s === "\u001b\n") {
|
|
404
|
+
// Alt+Enter: newline
|
|
405
|
+
this.insertText("\n");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (s.length === 1 && s >= " ") {
|
|
409
|
+
this.insertText(s);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
this.render();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private insertText(s: string): void {
|
|
416
|
+
const line = this.inputLines[this.inputLines.length - 1] ?? "";
|
|
417
|
+
this.inputLines[this.inputLines.length - 1] = line + s;
|
|
418
|
+
this.inputCursor++;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private backspace(): void {
|
|
422
|
+
const last = this.inputLines.length - 1;
|
|
423
|
+
const line = this.inputLines[last] ?? "";
|
|
424
|
+
if (line.length > 0) {
|
|
425
|
+
this.inputLines[last] = line.slice(0, -1);
|
|
426
|
+
} else if (this.inputLines.length > 1) {
|
|
427
|
+
this.inputLines.pop();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private submitLine(): void {
|
|
432
|
+
const text = this.inputLines.join("\n").trim();
|
|
433
|
+
this.inputLines = [""];
|
|
434
|
+
this.inputCursor = 0;
|
|
435
|
+
if (!text) return;
|
|
436
|
+
void this.handleInput(text);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private cycleProvider(): void {
|
|
440
|
+
const names = this.router.getProviderNames();
|
|
441
|
+
if (names.length === 0) {
|
|
442
|
+
this.showSystem("No providers available.");
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const current = this.router.getActiveProvider();
|
|
446
|
+
const idx = current ? names.indexOf(current) : -1;
|
|
447
|
+
const next = names[(idx + 1) % names.length];
|
|
448
|
+
this.router.setActiveProvider(next);
|
|
449
|
+
this.showSystem(`Provider: ${next}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private async handleInput(text: string): Promise<void> {
|
|
453
|
+
if (text.startsWith("/")) {
|
|
454
|
+
await this.handleCommand(text);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (this.arenaMode) {
|
|
459
|
+
await this.runArena(text);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this.messages.push({ role: "user", content: text });
|
|
464
|
+
this.session.add({ role: "user", content: text });
|
|
465
|
+
this.scrollOffset = 0;
|
|
466
|
+
this.render();
|
|
467
|
+
this.startSpinner();
|
|
468
|
+
|
|
469
|
+
const history = this.session.messages.filter(
|
|
470
|
+
(m): m is Message => m.role !== "system"
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
let assistantText = "";
|
|
474
|
+
let toolCalls: any[] = [];
|
|
475
|
+
try {
|
|
476
|
+
for await (const chunk of this.agent.run(text, history)) {
|
|
477
|
+
this.handleStreamChunk(chunk, { assistantText, toolCalls });
|
|
478
|
+
}
|
|
479
|
+
} catch (err) {
|
|
480
|
+
const msg = (err as Error).message;
|
|
481
|
+
this.messages.push({ role: "system", content: `[error] ${msg}` });
|
|
482
|
+
} finally {
|
|
483
|
+
this.stopSpinner();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const finalText = assistantText;
|
|
487
|
+
if (finalText.trim()) {
|
|
488
|
+
this.messages.push({ role: "assistant", content: finalText });
|
|
489
|
+
this.session.add({ role: "assistant", content: finalText, tool_calls: toolCalls.length ? toolCalls : undefined });
|
|
490
|
+
}
|
|
491
|
+
this.scrollOffset = 0;
|
|
492
|
+
this.render();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private handleStreamChunk(
|
|
496
|
+
chunk: ChatChunk,
|
|
497
|
+
state: { assistantText: string; toolCalls: any[] }
|
|
498
|
+
): void {
|
|
499
|
+
if (chunk.type === "text" && chunk.text) {
|
|
500
|
+
state.assistantText += chunk.text;
|
|
501
|
+
this.messages.push({ role: "assistant", content: chunk.text });
|
|
502
|
+
} else if (chunk.type === "tool_call" && chunk.tool_call) {
|
|
503
|
+
state.toolCalls.push(chunk.tool_call);
|
|
504
|
+
const name = chunk.tool_call.function.name;
|
|
505
|
+
this.messages.push({
|
|
506
|
+
role: "tool",
|
|
507
|
+
content: `[Tool: ${name}]`,
|
|
508
|
+
});
|
|
509
|
+
} else if (chunk.type === "error" && chunk.error) {
|
|
510
|
+
this.messages.push({ role: "system", content: `[error] ${chunk.error}` });
|
|
511
|
+
}
|
|
512
|
+
this.render();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private async runArena(text: string): Promise<void> {
|
|
516
|
+
this.messages.push({ role: "user", content: text });
|
|
517
|
+
this.session.add({ role: "user", content: text });
|
|
518
|
+
this.scrollOffset = 0;
|
|
519
|
+
this.render();
|
|
520
|
+
this.startSpinner();
|
|
521
|
+
|
|
522
|
+
const history = this.session.messages.filter(
|
|
523
|
+
(m): m is Message => m.role !== "system"
|
|
524
|
+
);
|
|
525
|
+
const modelIds = this.allModelIds();
|
|
526
|
+
if (modelIds.length < 2) {
|
|
527
|
+
this.stopSpinner();
|
|
528
|
+
this.showSystem(
|
|
529
|
+
`Arena needs at least 2 models. Found: ${modelIds.join(", ") || "(none)"}`
|
|
530
|
+
);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const results: ArenaResult[] = await this.arena.compare(
|
|
535
|
+
text,
|
|
536
|
+
history,
|
|
537
|
+
modelIds,
|
|
538
|
+
(modelId, chunk) => {
|
|
539
|
+
if (chunk.type === "text" && chunk.text) {
|
|
540
|
+
this.messages.push({
|
|
541
|
+
role: "system",
|
|
542
|
+
content: `${FG.brightCyan}[${modelId}]${RESET} ${chunk.text}`,
|
|
543
|
+
});
|
|
544
|
+
this.render();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
this.stopSpinner();
|
|
549
|
+
|
|
550
|
+
this.messages.push({ role: "system", content: this.arena.render(results) });
|
|
551
|
+
this.render();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private allModelIds(): string[] {
|
|
555
|
+
const ids = new Set<string>();
|
|
556
|
+
for (const cfg of this.router.configs) {
|
|
557
|
+
if (!cfg.enabled) continue;
|
|
558
|
+
for (const m of cfg.models) ids.add(m);
|
|
559
|
+
}
|
|
560
|
+
return Array.from(ids);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async handleCommand(text: string): Promise<void> {
|
|
564
|
+
const { command, args } = parseCommand(text);
|
|
565
|
+
if (!command) return;
|
|
566
|
+
|
|
567
|
+
const ctx: CommandContext = {
|
|
568
|
+
tui: this,
|
|
569
|
+
agent: this.agent,
|
|
570
|
+
router: this.router,
|
|
571
|
+
session: this.session,
|
|
572
|
+
arena: this.arena,
|
|
573
|
+
settings: this.settings,
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const handler = CommandHandler[command];
|
|
577
|
+
if (!handler) {
|
|
578
|
+
this.showSystem(`Unknown command: /${command}. Type /help for available commands.`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
const result = await handler.run(args, ctx);
|
|
584
|
+
if (typeof result === "string") {
|
|
585
|
+
this.showSystem(result);
|
|
586
|
+
}
|
|
587
|
+
} catch (err) {
|
|
588
|
+
this.showSystem(`Command error: ${(err as Error).message}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
private async handleLine(line: string): Promise<void> {
|
|
592
|
+
const text = line.trim();
|
|
593
|
+
if (!text) return;
|
|
594
|
+
if (text.startsWith("/")) {
|
|
595
|
+
await this.handleCommand(text);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (this.arenaMode) {
|
|
599
|
+
await this.runArena(text);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
this.messages.push({ role: "user", content: text });
|
|
604
|
+
this.session.add({ role: "user", content: text });
|
|
605
|
+
|
|
606
|
+
let assistantText = "";
|
|
607
|
+
const history = this.session.messages.filter(
|
|
608
|
+
(m): m is Message => m.role !== "system"
|
|
609
|
+
);
|
|
610
|
+
try {
|
|
611
|
+
for await (const chunk of this.agent.run(text, history)) {
|
|
612
|
+
if (chunk.type === "text" && chunk.text) {
|
|
613
|
+
assistantText += chunk.text;
|
|
614
|
+
process.stdout.write(chunk.text);
|
|
615
|
+
} else if (chunk.type === "tool_call" && chunk.tool_call) {
|
|
616
|
+
process.stdout.write(`\n[Tool: ${chunk.tool_call.function.name}]\n`);
|
|
617
|
+
} else if (chunk.type === "error" && chunk.error) {
|
|
618
|
+
process.stderr.write(`[error] ${chunk.error}\n`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
} catch (err) {
|
|
622
|
+
process.stderr.write(`error: ${(err as Error).message}\n`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (assistantText.trim()) {
|
|
626
|
+
this.messages.push({ role: "assistant", content: assistantText });
|
|
627
|
+
this.session.add({ role: "assistant", content: assistantText });
|
|
628
|
+
}
|
|
629
|
+
process.stdout.write("\n");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export function createTUI(opts: {
|
|
634
|
+
agent: Agent;
|
|
635
|
+
router: Router;
|
|
636
|
+
session: Session;
|
|
637
|
+
arena: Arena;
|
|
638
|
+
costTracker?: CostTracker;
|
|
639
|
+
skills?: Skills;
|
|
640
|
+
checkpoint?: Checkpoint;
|
|
641
|
+
settings?: Settings;
|
|
642
|
+
}): TUI {
|
|
643
|
+
return new TUI(opts);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export async function runTUI(): Promise<void> {
|
|
647
|
+
const cfg = getConfig();
|
|
648
|
+
const router = new Router(cfg.providers, new HealthTracker());
|
|
649
|
+
const registry = new ToolRegistry();
|
|
650
|
+
for (const make of [makeReadFileTool, makeWriteFileTool, makeEditFileTool, makeListDirTool, makeBashTool]) {
|
|
651
|
+
const tool = make(process.cwd());
|
|
652
|
+
registry.register(tool.def, tool.execute);
|
|
653
|
+
}
|
|
654
|
+
const agent = new Agent(router, registry);
|
|
655
|
+
const session = new Session();
|
|
656
|
+
const arena = new Arena(router);
|
|
657
|
+
const tui = new TUI({ agent, router, session, arena });
|
|
658
|
+
tui.start();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
|