@sofer_agent/cli 0.3.11 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat.d.ts +0 -1
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +24 -46
- package/dist/chat.js.map +1 -1
- package/dist/tui.d.ts +3 -37
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +190 -337
- package/dist/tui.jsx +170 -0
- package/dist/tui.jsx.map +1 -0
- package/package.json +23 -5
- package/src/chat.ts +24 -57
- package/src/tui.tsx +227 -0
- package/src/tui.ts +0 -371
package/dist/tui.js
CHANGED
|
@@ -1,346 +1,199 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { template as _$template } from "solid-js/web";
|
|
2
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
3
|
+
import { setAttribute as _$setAttribute } from "solid-js/web";
|
|
4
|
+
import { effect as _$effect } from "solid-js/web";
|
|
5
|
+
import { insert as _$insert } from "solid-js/web";
|
|
6
|
+
var _tmpl$ = /*#__PURE__*/_$template(`<box flexdirection=row marginbottom=1><text flexShrink=0></text><text wrapMode=word flexGrow=1 fg=#e5e7eb>`),
|
|
7
|
+
_tmpl$2 = /*#__PURE__*/_$template(`<box flexdirection=column><scrollbox flexgrow=1 flexshrink=1 stickyscroll stickystart=bottom></scrollbox><box flexdirection=row flexshrink=0 paddingleft=3 paddingright=2 margintop=1><text fg=#67e8f9 flexGrow=1></text></box><box flexdirection=row flexshrink=0 minheight=3 maxheight=8 borderstyle=rounded bordercolor=#374151 paddingleft=1 paddingright=1 marginleft=2 marginright=2><text fg=#67e8f9 flexShrink=0>› </text><text wrapMode=word flexGrow=1 fg=#e5e7eb></text></box><box flexdirection=row flexshrink=0 paddingleft=2 paddingright=2><text fg=#9ca3af>`);
|
|
8
|
+
import { render, useKeyboard, useTerminalDimensions } from "@opentui/solid";
|
|
9
|
+
import { For, createEffect, createSignal, onCleanup } from "solid-js";
|
|
6
10
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
11
|
const SPINNER_MS = 80;
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
spinnerFrame = 0;
|
|
29
|
-
spinnerTimer = null;
|
|
30
|
-
turnStartedAt = 0;
|
|
31
|
-
// interrupt
|
|
32
|
-
abortCtrl = null;
|
|
33
|
-
// sidebar
|
|
34
|
-
info = { lastTurnIn: 0, lastTurnOut: 0 };
|
|
35
|
-
// callbacks
|
|
36
|
-
resolve = null;
|
|
37
|
-
onLine = null;
|
|
38
|
-
constructor(agent, config) {
|
|
39
|
-
this.agent = agent;
|
|
40
|
-
this.config = config;
|
|
41
|
-
}
|
|
42
|
-
measure() {
|
|
43
|
-
this.columns = Math.max(80, process.stdout.columns ?? 80);
|
|
44
|
-
this.rows = Math.max(24, process.stdout.rows ?? 24);
|
|
45
|
-
this.chatW = Math.max(MIN_CHAT_W, this.columns - SIDEBAR_W - 3);
|
|
46
|
-
this.sideW = this.columns - this.chatW - 3;
|
|
47
|
-
this.bodyH = this.rows - 5; // top border, input box(3), footer(1)
|
|
48
|
-
}
|
|
49
|
-
addMessage(role, text) {
|
|
50
|
-
this.state.push({ role, text });
|
|
51
|
-
this.scrollOff = Number.MAX_SAFE_INTEGER; // will be clamped to maxScroll in buildFrame
|
|
52
|
-
}
|
|
53
|
-
setInfo(info) {
|
|
54
|
-
Object.assign(this.info, info);
|
|
55
|
-
}
|
|
56
|
-
startSpinner() {
|
|
57
|
-
if (this.spinnerTimer)
|
|
58
|
-
return;
|
|
59
|
-
this.status = "thinking";
|
|
60
|
-
this.turnStartedAt = Date.now();
|
|
61
|
-
this.spinnerFrame = 0;
|
|
62
|
-
this.spinnerTimer = setInterval(() => {
|
|
63
|
-
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
64
|
-
this.render();
|
|
65
|
-
}, SPINNER_MS);
|
|
66
|
-
}
|
|
67
|
-
stopSpinner() {
|
|
68
|
-
this.status = "idle";
|
|
69
|
-
this.turnStartedAt = 0;
|
|
70
|
-
if (this.spinnerTimer) {
|
|
71
|
-
clearInterval(this.spinnerTimer);
|
|
72
|
-
this.spinnerTimer = null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
async run(onLine) {
|
|
76
|
-
this.onLine = onLine;
|
|
77
|
-
this.measure();
|
|
78
|
-
if (process.stdin.isTTY)
|
|
79
|
-
process.stdin.setRawMode?.(true);
|
|
80
|
-
process.stdin.on("data", this.handleInput);
|
|
81
|
-
process.stdout.on("resize", this.handleResize);
|
|
82
|
-
process.on("SIGWINCH", this.handleResize);
|
|
83
|
-
this.render();
|
|
84
|
-
return new Promise((resolve) => { this.resolve = resolve; });
|
|
85
|
-
}
|
|
86
|
-
shutdown() {
|
|
87
|
-
this.stopSpinner();
|
|
88
|
-
process.stdin.setRawMode?.(false);
|
|
89
|
-
process.stdin.removeAllListeners("data");
|
|
90
|
-
process.stdout.removeAllListeners("resize");
|
|
91
|
-
process.removeAllListeners("SIGWINCH");
|
|
92
|
-
this.render();
|
|
93
|
-
process.stdout.write("\n");
|
|
94
|
-
this.resolve?.();
|
|
95
|
-
}
|
|
96
|
-
// ── Input handling ──────────────────────────────────────────────────────
|
|
97
|
-
handleInput = (data) => {
|
|
98
|
-
const str = data.toString();
|
|
99
|
-
for (const ch of str) {
|
|
100
|
-
const code = ch.charCodeAt(0);
|
|
101
|
-
if (code === 3) {
|
|
102
|
-
this.shutdown();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (code === 4 && this.inputBuf.length === 0) {
|
|
106
|
-
this.shutdown();
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (code === 27) {
|
|
110
|
-
if (this.status === "thinking" && this.abortCtrl) {
|
|
111
|
-
this.abortCtrl.abort();
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
this.inputBuf = "";
|
|
115
|
-
this.cursor = 0;
|
|
116
|
-
this.render();
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (this.status === "thinking")
|
|
120
|
-
continue;
|
|
121
|
-
if (code === 13) {
|
|
122
|
-
this.submit();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (code === 127) {
|
|
126
|
-
if (this.cursor > 0) {
|
|
127
|
-
this.inputBuf = this.inputBuf.slice(0, this.cursor - 1) + this.inputBuf.slice(this.cursor);
|
|
128
|
-
this.cursor--;
|
|
129
|
-
}
|
|
130
|
-
this.render();
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (ch >= " ") {
|
|
134
|
-
this.inputBuf = this.inputBuf.slice(0, this.cursor) + ch + this.inputBuf.slice(this.cursor);
|
|
135
|
-
this.cursor++;
|
|
136
|
-
this.render();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
async submit() {
|
|
141
|
-
const line = this.inputBuf.trim();
|
|
142
|
-
this.inputBuf = "";
|
|
143
|
-
this.cursor = 0;
|
|
144
|
-
if (!line) {
|
|
145
|
-
this.render();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (line === "/exit" || line === "/quit") {
|
|
149
|
-
this.shutdown();
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
this.addMessage("you", line);
|
|
153
|
-
// scrollOff already set to MAX by addMessage (pinned to bottom)
|
|
154
|
-
this.abortCtrl = new AbortController();
|
|
155
|
-
this.startSpinner();
|
|
156
|
-
this.render();
|
|
157
|
-
await this.onLine?.(line, this.abortCtrl.signal);
|
|
158
|
-
this.abortCtrl = null;
|
|
159
|
-
this.stopSpinner();
|
|
160
|
-
this.render();
|
|
161
|
-
}
|
|
162
|
-
// ── Rendering ───────────────────────────────────────────────────────────
|
|
163
|
-
handleResize = () => { this.measure(); this.render(); };
|
|
164
|
-
render() {
|
|
165
|
-
this.measure();
|
|
166
|
-
process.stdout.write("\x1b[?25l\x1b[H" + this.buildFrame() + "\x1b[?25h");
|
|
167
|
-
}
|
|
168
|
-
buildFrame() {
|
|
169
|
-
const { columns, chatW, sideW, bodyH } = this;
|
|
170
|
-
const innerSideW = sideW - 2;
|
|
171
|
-
const sep = `${B.lt}${B.h.repeat(chatW)}${B.bt}${B.h.repeat(sideW)}${B.rt}`;
|
|
172
|
-
const inputW = columns - 2; // inside border
|
|
173
|
-
let buf = "";
|
|
174
|
-
// ── Top border ────────────────────────────────────────────────────────
|
|
175
|
-
buf += `${B.tl}${B.h.repeat(chatW)}${B.tt}${B.h.repeat(sideW)}${B.tr}\n`;
|
|
176
|
-
// ── Body ──────────────────────────────────────────────────────────────
|
|
177
|
-
const wrapped = this.state.flatMap((m) => this.wrapMessage(m, chatW));
|
|
178
|
-
const maxScroll = Math.max(0, wrapped.length - bodyH);
|
|
179
|
-
if (this.scrollOff > maxScroll)
|
|
180
|
-
this.scrollOff = maxScroll;
|
|
181
|
-
if (this.scrollOff < 0)
|
|
182
|
-
this.scrollOff = 0;
|
|
183
|
-
const visible = wrapped.slice(this.scrollOff, this.scrollOff + bodyH);
|
|
184
|
-
const sidebarLines = this.buildSidebar(innerSideW);
|
|
185
|
-
const sbTop = Math.max(0, Math.floor((bodyH - sidebarLines.length) / 2));
|
|
186
|
-
for (let r = 0; r < bodyH; r++) {
|
|
187
|
-
buf += B.v;
|
|
188
|
-
buf += visible[r] ? padTrunc(visible[r], chatW) : " ".repeat(chatW);
|
|
189
|
-
buf += B.v;
|
|
190
|
-
const sbIdx = r - sbTop;
|
|
191
|
-
const sbLine = (sbIdx >= 0 && sbIdx < sidebarLines.length) ? sidebarLines[sbIdx] : "";
|
|
192
|
-
buf += ` ${padRight(sbLine, innerSideW)} `;
|
|
193
|
-
buf += `${B.v}\n`;
|
|
194
|
-
}
|
|
195
|
-
// ── Separator ─────────────────────────────────────────────────────────
|
|
196
|
-
buf += `${sep}\n`;
|
|
197
|
-
// ── Spinner row ───────────────────────────────────────────────────────
|
|
198
|
-
if (this.status === "thinking") {
|
|
199
|
-
const frame = SPINNER_FRAMES[this.spinnerFrame];
|
|
200
|
-
const elapsed = this.turnStartedAt ? formatElapsed(Date.now() - this.turnStartedAt) : "";
|
|
201
|
-
const spinnerText = `${frame} thinking\u2026${elapsed ? ` ${elapsed}` : ""} (esc to interrupt)`;
|
|
202
|
-
buf += `${B.v} \x1b[36m${spinnerText}\x1b[0m${" ".repeat(Math.max(0, inputW - 1 - spinnerText.length))}${B.v}\n`;
|
|
203
|
-
}
|
|
204
|
-
// ── Input bar (bordered box) ─────────────────────────────────────────
|
|
205
|
-
buf += `${B.tl}${B.h.repeat(inputW)}${B.tr}\n`;
|
|
206
|
-
const cursorCh = this.status === "idle" ? "\x1b[5m▋\x1b[25m" : "";
|
|
207
|
-
const inputMaxW = inputW - 3; // "> " prefix + 1 padding
|
|
208
|
-
const visibleInput = this.inputBuf.length > inputMaxW
|
|
209
|
-
? this.inputBuf.slice(this.inputBuf.length - inputMaxW)
|
|
210
|
-
: this.inputBuf;
|
|
211
|
-
const inputPad = inputMaxW - visibleInput.length;
|
|
212
|
-
buf += `${B.v} \x1b[36m>\x1b[0m ${visibleInput}${cursorCh}${" ".repeat(Math.max(0, inputPad))} ${B.v}\n`;
|
|
213
|
-
buf += `${B.bl}${B.h.repeat(inputW)}${B.br}\n`;
|
|
214
|
-
// ── Footer ────────────────────────────────────────────────────────────
|
|
215
|
-
const totalIn = this.agent.usage.tokens.inputTokens;
|
|
216
|
-
const totalOut = this.agent.usage.tokens.outputTokens;
|
|
217
|
-
const cost = this.agent.usage.costUsd.toFixed(4);
|
|
218
|
-
const ft = `\x1b[90m${this.agent.address.slice(0, 10)}… · in ${totalIn} out ${totalOut} $${cost} /exit\x1b[0m`;
|
|
219
|
-
buf += `${padTrunc(ft, columns)}\n`;
|
|
220
|
-
// ── Cursor position ───────────────────────────────────────────────────
|
|
221
|
-
// Row layout: 1(top border) + bodyH(body) + 1(sep) + [1 spinner] + 1(input top) + 1(input content) = bodyH+4|5
|
|
222
|
-
const cursorRow = bodyH + 3 + (this.status === "thinking" ? 1 : 0); // 1-indexed: body+sep+[spinner]+input-top+input-content
|
|
223
|
-
const cursorCol = 4 + Math.min(this.cursor, inputMaxW);
|
|
224
|
-
buf += `\x1b[${cursorRow};${cursorCol}H`;
|
|
225
|
-
return buf;
|
|
226
|
-
}
|
|
227
|
-
// ── Messages ────────────────────────────────────────────────────────────
|
|
228
|
-
wrapMessage(msg, width) {
|
|
229
|
-
const label = this.roleLabel(msg.role);
|
|
230
|
-
const bodyWidth = Math.max(10, width - GUTTER.length - LABEL_W - 1);
|
|
231
|
-
const lines = wrapText(msg.text, bodyWidth);
|
|
232
|
-
return lines.map((l, i) => i === 0 ? `${GUTTER}${label} ${l}` : `${INDENT}${l}`);
|
|
233
|
-
}
|
|
234
|
-
roleLabel(role) {
|
|
235
|
-
switch (role) {
|
|
236
|
-
case "you": return "\x1b[36myou \x1b[0m";
|
|
237
|
-
case "agent": return `\x1b[32m${this.agent.name.toLowerCase().padEnd(LABEL_W)}\x1b[0m`;
|
|
238
|
-
case "system": return "\x1b[90msys \x1b[0m";
|
|
239
|
-
case "error": return "\x1b[31merr \x1b[0m";
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
// ── Sidebar ─────────────────────────────────────────────────────────────
|
|
243
|
-
buildSidebar(width) {
|
|
244
|
-
const { agent, config, info } = this;
|
|
245
|
-
const addr = `${agent.address.slice(0, 6)}…${agent.address.slice(-4)}`;
|
|
246
|
-
const totalIn = agent.usage.tokens.inputTokens;
|
|
247
|
-
const totalOut = agent.usage.tokens.outputTokens;
|
|
248
|
-
const cost = agent.usage.costUsd.toFixed(6);
|
|
249
|
-
const rows = [
|
|
250
|
-
`\x1b[1;36m${agent.name}\x1b[0m`,
|
|
251
|
-
`\x1b[90m${addr}\x1b[0m`,
|
|
252
|
-
"",
|
|
253
|
-
`${B.h.repeat(width)}`,
|
|
254
|
-
`\x1b[90mnetwork\x1b[0m ${config.network}`,
|
|
255
|
-
`\x1b[90mmodel\x1b[0m ${shortModel(config.brain.model)}`,
|
|
256
|
-
];
|
|
257
|
-
if (info.balance) {
|
|
258
|
-
rows.push("", `\x1b[90mbalance\x1b[0m \x1b[33m${info.balance} SUI\x1b[0m`);
|
|
259
|
-
}
|
|
260
|
-
rows.push("", `\x1b[90m▸ last turn\x1b[0m`, ` in ${info.lastTurnIn}`, ` out ${info.lastTurnOut}`, "", `\x1b[90m▸ total\x1b[0m`, ` in ${totalIn}`, ` out ${totalOut}`, ` \x1b[33m$${cost}\x1b[0m`);
|
|
261
|
-
return rows;
|
|
262
|
-
}
|
|
12
|
+
// ── State signals (module-level so chat.ts can push into them) ──────────────
|
|
13
|
+
|
|
14
|
+
const [rows, setRows] = createSignal([]);
|
|
15
|
+
const [status, setStatus] = createSignal("idle");
|
|
16
|
+
const [input, setInput] = createSignal("");
|
|
17
|
+
const [info, setInfoSignal] = createSignal({
|
|
18
|
+
lastTurnIn: 0,
|
|
19
|
+
lastTurnOut: 0
|
|
20
|
+
});
|
|
21
|
+
const [turnStartedAt, setTurnStartedAt] = createSignal(null);
|
|
22
|
+
const [spinnerFrame, setSpinnerFrame] = createSignal(0);
|
|
23
|
+
const [activeAbort, setActiveAbort] = createSignal(null);
|
|
24
|
+
|
|
25
|
+
// ── Exported imperative API (used by chat.ts) ────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export function addMessage(role, text) {
|
|
28
|
+
setRows(prev => [...prev, {
|
|
29
|
+
role,
|
|
30
|
+
text
|
|
31
|
+
}]);
|
|
263
32
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// First split on hard newlines, then word-wrap each paragraph
|
|
270
|
-
for (const para of text.split("\n")) {
|
|
271
|
-
if (!para) {
|
|
272
|
-
lines.push("");
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
const words = para.split(" ");
|
|
276
|
-
let cur = "";
|
|
277
|
-
for (const w of words) {
|
|
278
|
-
// Break words longer than width
|
|
279
|
-
if (w.length > width) {
|
|
280
|
-
if (cur) {
|
|
281
|
-
lines.push(cur);
|
|
282
|
-
cur = "";
|
|
283
|
-
}
|
|
284
|
-
for (let i = 0; i < w.length; i += width)
|
|
285
|
-
lines.push(w.slice(i, i + width));
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
if (cur && cur.length + 1 + w.length > width) {
|
|
289
|
-
lines.push(cur);
|
|
290
|
-
cur = w;
|
|
291
|
-
}
|
|
292
|
-
else
|
|
293
|
-
cur = cur ? `${cur} ${w}` : w;
|
|
294
|
-
}
|
|
295
|
-
if (cur)
|
|
296
|
-
lines.push(cur);
|
|
297
|
-
}
|
|
298
|
-
return lines.length > 0 ? lines : [""];
|
|
33
|
+
export function setInfo(patch) {
|
|
34
|
+
setInfoSignal(prev => ({
|
|
35
|
+
...prev,
|
|
36
|
+
...patch
|
|
37
|
+
}));
|
|
299
38
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
39
|
+
|
|
40
|
+
// ── Helper ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function formatElapsed(startedAt) {
|
|
43
|
+
if (!startedAt) return "";
|
|
44
|
+
const sec = Math.floor((Date.now() - startedAt) / 1000);
|
|
45
|
+
if (sec < 60) return `${sec}s`;
|
|
46
|
+
const m = Math.floor(sec / 60);
|
|
47
|
+
return `${m}m${String(sec % 60).padStart(2, "0")}s`;
|
|
303
48
|
}
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
while (m !== null) {
|
|
311
|
-
len += m.index - last;
|
|
312
|
-
last = m.index + m[0].length;
|
|
313
|
-
m = re.exec(s);
|
|
314
|
-
}
|
|
315
|
-
return len + s.length - last;
|
|
316
|
-
}
|
|
317
|
-
function padRight(s, w) {
|
|
318
|
-
const vl = visibleLen(s);
|
|
319
|
-
return vl >= w ? s : s + " ".repeat(w - vl);
|
|
49
|
+
function shortModel(model) {
|
|
50
|
+
const m = model.toLowerCase();
|
|
51
|
+
if (m.includes("sonnet")) return "sonnet-4";
|
|
52
|
+
if (m.includes("haiku")) return "haiku";
|
|
53
|
+
if (m.includes("opus")) return "opus-4";
|
|
54
|
+
return model.length > 16 ? `${model.slice(0, 15)}…` : model;
|
|
320
55
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
56
|
+
const LABEL = {
|
|
57
|
+
you: "you",
|
|
58
|
+
agent: "agent",
|
|
59
|
+
system: "sys",
|
|
60
|
+
error: "err"
|
|
61
|
+
};
|
|
62
|
+
const COLOR = {
|
|
63
|
+
you: "#67e8f9",
|
|
64
|
+
agent: "#86efac",
|
|
65
|
+
system: "#9ca3af",
|
|
66
|
+
error: "#fca5a5"
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ── Components ────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function MessageRow(props) {
|
|
72
|
+
return (() => {
|
|
73
|
+
var _el$ = _tmpl$(),
|
|
74
|
+
_el$2 = _el$.firstChild,
|
|
75
|
+
_el$3 = _el$2.nextSibling;
|
|
76
|
+
_$insert(_el$2, () => ` ${LABEL[props.line.role].padEnd(5)} `);
|
|
77
|
+
_$insert(_el$3, () => props.line.text);
|
|
78
|
+
_$effect(() => _$setAttribute(_el$2, "fg", COLOR[props.line.role]));
|
|
79
|
+
return _el$;
|
|
80
|
+
})();
|
|
326
81
|
}
|
|
327
|
-
function
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
82
|
+
function App(props) {
|
|
83
|
+
const dims = useTerminalDimensions();
|
|
84
|
+
|
|
85
|
+
// Spinner tick — only while thinking
|
|
86
|
+
createEffect(() => {
|
|
87
|
+
if (status() !== "thinking") {
|
|
88
|
+
setSpinnerFrame(0);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const id = setInterval(() => setSpinnerFrame(f => (f + 1) % SPINNER_FRAMES.length), SPINNER_MS);
|
|
92
|
+
onCleanup(() => clearInterval(id));
|
|
93
|
+
});
|
|
94
|
+
useKeyboard(evt => {
|
|
95
|
+
if (evt.ctrl && evt.name === "c") {
|
|
96
|
+
evt.preventDefault();
|
|
97
|
+
props.onExit();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (evt.name === "escape") {
|
|
101
|
+
const ab = activeAbort();
|
|
102
|
+
if (ab && !ab.signal.aborted) ab.abort();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (status() === "thinking") return; // block input during turns
|
|
106
|
+
|
|
107
|
+
if (evt.name === "return") {
|
|
108
|
+
const text = input().trim();
|
|
109
|
+
if (!text) return;
|
|
110
|
+
if (text === "/exit" || text === "/quit") {
|
|
111
|
+
props.onExit();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
addMessage("you", text);
|
|
115
|
+
setInput("");
|
|
116
|
+
const ctrl = new AbortController();
|
|
117
|
+
setActiveAbort(ctrl);
|
|
118
|
+
setStatus("thinking");
|
|
119
|
+
setTurnStartedAt(Date.now());
|
|
120
|
+
props.onSubmit(text, ctrl.signal).finally(() => {
|
|
121
|
+
setStatus("idle");
|
|
122
|
+
setTurnStartedAt(null);
|
|
123
|
+
setActiveAbort(null);
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (evt.name === "backspace" || evt.name === "delete") {
|
|
128
|
+
setInput(p => p.slice(0, -1));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (evt.sequence && !evt.ctrl && !evt.meta && !evt.option) {
|
|
132
|
+
setInput(p => p + evt.sequence);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const {
|
|
136
|
+
agent,
|
|
137
|
+
config
|
|
138
|
+
} = props;
|
|
139
|
+
const addr = () => `${agent.address.slice(0, 6)}…${agent.address.slice(-4)}`;
|
|
140
|
+
return (() => {
|
|
141
|
+
var _el$4 = _tmpl$2(),
|
|
142
|
+
_el$5 = _el$4.firstChild,
|
|
143
|
+
_el$6 = _el$5.nextSibling,
|
|
144
|
+
_el$7 = _el$6.firstChild,
|
|
145
|
+
_el$8 = _el$6.nextSibling,
|
|
146
|
+
_el$9 = _el$8.firstChild,
|
|
147
|
+
_el$0 = _el$9.nextSibling,
|
|
148
|
+
_el$1 = _el$8.nextSibling,
|
|
149
|
+
_el$10 = _el$1.firstChild;
|
|
150
|
+
_$setAttribute(_el$5, "contentoptions", {
|
|
151
|
+
flexDirection: "column",
|
|
152
|
+
paddingLeft: 0,
|
|
153
|
+
paddingRight: 1,
|
|
154
|
+
paddingTop: 1,
|
|
155
|
+
paddingBottom: 1
|
|
156
|
+
});
|
|
157
|
+
_$insert(_el$5, _$createComponent(For, {
|
|
158
|
+
get each() {
|
|
159
|
+
return rows();
|
|
160
|
+
},
|
|
161
|
+
children: line => _$createComponent(MessageRow, {
|
|
162
|
+
line: line
|
|
163
|
+
})
|
|
164
|
+
}));
|
|
165
|
+
_$insert(_el$7, () => {
|
|
166
|
+
if (status() !== "thinking") return " ";
|
|
167
|
+
spinnerFrame(); // reactive dependency
|
|
168
|
+
const elapsed = formatElapsed(turnStartedAt());
|
|
169
|
+
const frame = SPINNER_FRAMES[spinnerFrame()];
|
|
170
|
+
return elapsed ? `${frame} thinking… ${elapsed} (esc to interrupt)` : `${frame} thinking… (esc to interrupt)`;
|
|
171
|
+
});
|
|
172
|
+
_$insert(_el$0, () => `${input()}${status() === "idle" ? "▋" : ""}`);
|
|
173
|
+
_$insert(_el$10, () => [addr(), `${config.network}`, shortModel(config.brain.model), info().balance ? `${info().balance} SUI` : null, `in ${agent.usage.tokens.inputTokens} out ${agent.usage.tokens.outputTokens} $${agent.usage.costUsd.toFixed(4)}`, "/exit"].filter(Boolean).join(" · "));
|
|
174
|
+
_$effect(_p$ => {
|
|
175
|
+
var _v$ = dims().width,
|
|
176
|
+
_v$2 = dims().height;
|
|
177
|
+
_v$ !== _p$.e && _$setAttribute(_el$4, "width", _p$.e = _v$);
|
|
178
|
+
_v$2 !== _p$.t && _$setAttribute(_el$4, "height", _p$.t = _v$2);
|
|
179
|
+
return _p$;
|
|
180
|
+
}, {
|
|
181
|
+
e: undefined,
|
|
182
|
+
t: undefined
|
|
183
|
+
});
|
|
184
|
+
return _el$4;
|
|
185
|
+
})();
|
|
333
186
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
187
|
+
|
|
188
|
+
// ── Main entry ────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export async function runTui(agent, config, onSubmit) {
|
|
191
|
+
return new Promise(resolve => {
|
|
192
|
+
render(() => _$createComponent(App, {
|
|
193
|
+
agent: agent,
|
|
194
|
+
config: config,
|
|
195
|
+
onSubmit: onSubmit,
|
|
196
|
+
onExit: resolve
|
|
197
|
+
}));
|
|
198
|
+
});
|
|
345
199
|
}
|
|
346
|
-
//# sourceMappingURL=tui.js.map
|