smolcoder 0.4.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/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/agent.js +411 -0
- package/dist/context.js +337 -0
- package/dist/detect.js +199 -0
- package/dist/events.js +24 -0
- package/dist/index.js +680 -0
- package/dist/plan.js +91 -0
- package/dist/prompt.js +85 -0
- package/dist/providers/lmstudio.js +326 -0
- package/dist/providers/ollama.js +264 -0
- package/dist/providers/types.js +60 -0
- package/dist/sandbox.js +179 -0
- package/dist/tools/check.js +193 -0
- package/dist/tools/fs-tools.js +417 -0
- package/dist/tools/index.js +236 -0
- package/dist/tools/shell.js +168 -0
- package/dist/tools/tasks.js +128 -0
- package/dist/tui/editor.js +134 -0
- package/dist/tui/keys.js +145 -0
- package/dist/tui/tui.js +632 -0
- package/dist/ui.js +226 -0
- package/dist/util.js +72 -0
- package/dist/web/page.js +381 -0
- package/dist/web/webui.js +262 -0
- package/package.json +48 -0
package/dist/tui/tui.js
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The inline TUI, styled after opencode: an accent-bar input block with the
|
|
3
|
+
// session status (mode · model · effort · ctx) inside it, a slash-command menu
|
|
4
|
+
// that opens above the input as you type "/", arrow-key pickers with
|
|
5
|
+
// type-to-filter, and shift+tab mode cycling. Hand-rolled ANSI, zero deps.
|
|
6
|
+
//
|
|
7
|
+
// The frame (input block + menus) exists only while waiting for input; while
|
|
8
|
+
// the agent runs, output streams plainly and scrolls naturally.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.Tui = void 0;
|
|
11
|
+
const ui_1 = require("../ui");
|
|
12
|
+
const util_1 = require("../util");
|
|
13
|
+
const editor_1 = require("./editor");
|
|
14
|
+
const keys_1 = require("./keys");
|
|
15
|
+
const ACCENT = "\x1b[36m"; // cyan accent bar
|
|
16
|
+
const RESET = "\x1b[0m";
|
|
17
|
+
const SEL = "\x1b[48;5;31m\x1b[38;5;231m"; // cyan selection bar, white text — one blue theme
|
|
18
|
+
const BAR = `${ACCENT}▌${RESET} `;
|
|
19
|
+
const BOX_BG = "\x1b[48;5;235m"; // subtle shading for the input block
|
|
20
|
+
function visLen(s) {
|
|
21
|
+
// eslint-disable-next-line no-control-regex
|
|
22
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
23
|
+
}
|
|
24
|
+
/** Cut a colored string to a visible length without splitting escape codes. */
|
|
25
|
+
function truncateVisible(s, max) {
|
|
26
|
+
let out = "";
|
|
27
|
+
let seen = 0;
|
|
28
|
+
for (let i = 0; i < s.length; i++) {
|
|
29
|
+
if (s[i] === "\x1b") {
|
|
30
|
+
const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i));
|
|
31
|
+
if (m) {
|
|
32
|
+
out += m[0];
|
|
33
|
+
i += m[0].length - 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (seen >= max)
|
|
38
|
+
continue;
|
|
39
|
+
out += s[i];
|
|
40
|
+
seen++;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** One shaded row of the input block: accent bar + content padded to width,
|
|
45
|
+
* with the block background re-applied after any color reset inside. */
|
|
46
|
+
function boxRow(content, w) {
|
|
47
|
+
const inner = Math.max(1, w - 2);
|
|
48
|
+
if (visLen(content) > inner)
|
|
49
|
+
content = truncateVisible(content, inner);
|
|
50
|
+
const pad = Math.max(0, inner - visLen(content));
|
|
51
|
+
const body = (content + " ".repeat(pad)).split(RESET).join(RESET + BOX_BG);
|
|
52
|
+
return `${BOX_BG}${ACCENT}▌\x1b[39m ${body}${RESET}`;
|
|
53
|
+
}
|
|
54
|
+
class Tui {
|
|
55
|
+
slashCommands = [];
|
|
56
|
+
getStatus = () => "";
|
|
57
|
+
onModeCycle = null;
|
|
58
|
+
onCancel = null;
|
|
59
|
+
onExit = null;
|
|
60
|
+
placeholder = 'Ask anything… "add a dark mode toggle"';
|
|
61
|
+
/** Shown dim on the left of the hint row (the workspace path). */
|
|
62
|
+
hintLeft = "";
|
|
63
|
+
ed = new editor_1.LineEditor();
|
|
64
|
+
decoder = new keys_1.KeyDecoder();
|
|
65
|
+
state = "hidden";
|
|
66
|
+
prevLines = 0;
|
|
67
|
+
offsetFromBottom = 0;
|
|
68
|
+
history = [];
|
|
69
|
+
histIdx = -1;
|
|
70
|
+
histStash = "";
|
|
71
|
+
menuIndex = 0;
|
|
72
|
+
lastMenuFilter = null;
|
|
73
|
+
notice = null;
|
|
74
|
+
lastCtrlC = 0;
|
|
75
|
+
submitResolve = null;
|
|
76
|
+
sel = null;
|
|
77
|
+
confirmState = null;
|
|
78
|
+
spinnerTimer = null;
|
|
79
|
+
spinnerActive = false;
|
|
80
|
+
atLineStart = true;
|
|
81
|
+
lastKind = null;
|
|
82
|
+
start() {
|
|
83
|
+
process.stdin.setRawMode?.(true);
|
|
84
|
+
process.stdin.resume();
|
|
85
|
+
process.stdin.setEncoding("utf8");
|
|
86
|
+
process.stdin.on("data", (d) => this.onData(d));
|
|
87
|
+
process.stdout.on("resize", () => {
|
|
88
|
+
if (this.state !== "hidden")
|
|
89
|
+
this.redraw();
|
|
90
|
+
});
|
|
91
|
+
process.stdout.write("\x1b[?2004h"); // bracketed paste on
|
|
92
|
+
}
|
|
93
|
+
close() {
|
|
94
|
+
this.stopSpinner();
|
|
95
|
+
this.hideFrame();
|
|
96
|
+
process.stdout.write("\x1b[?2004l\x1b[?25h");
|
|
97
|
+
process.stdin.setRawMode?.(false);
|
|
98
|
+
process.stdin.pause();
|
|
99
|
+
}
|
|
100
|
+
// ---- input ---------------------------------------------------------------
|
|
101
|
+
readInput() {
|
|
102
|
+
this.ed.clear();
|
|
103
|
+
this.histIdx = -1;
|
|
104
|
+
this.menuIndex = 0;
|
|
105
|
+
this.state = "idle";
|
|
106
|
+
this.redraw();
|
|
107
|
+
return new Promise((res) => (this.submitResolve = res));
|
|
108
|
+
}
|
|
109
|
+
select(title, options) {
|
|
110
|
+
this.stopSpinner();
|
|
111
|
+
this.hideFrame();
|
|
112
|
+
process.stdout.write("\x1b[?25l");
|
|
113
|
+
this.state = "select";
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
this.sel = { title, options, filter: "", index: 0, resolve };
|
|
116
|
+
this.redraw();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
confirmCommand(command, reason) {
|
|
120
|
+
this.stopSpinner();
|
|
121
|
+
this.hideFrame();
|
|
122
|
+
this.state = "confirm";
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
this.confirmState = { command, reason, resolve };
|
|
125
|
+
this.redraw();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// ---- key routing ---------------------------------------------------------
|
|
129
|
+
onData(data) {
|
|
130
|
+
for (const key of this.decoder.decode(data)) {
|
|
131
|
+
switch (this.state) {
|
|
132
|
+
case "idle":
|
|
133
|
+
this.keyIdle(key);
|
|
134
|
+
break;
|
|
135
|
+
case "select":
|
|
136
|
+
this.keySelect(key);
|
|
137
|
+
break;
|
|
138
|
+
case "confirm":
|
|
139
|
+
this.keyConfirm(key);
|
|
140
|
+
break;
|
|
141
|
+
case "hidden": // agent running
|
|
142
|
+
if (key.type === "esc" || key.type === "ctrlc")
|
|
143
|
+
this.onCancel?.();
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
keyIdle(key) {
|
|
149
|
+
this.notice = null;
|
|
150
|
+
const menu = this.menuEntries();
|
|
151
|
+
switch (key.type) {
|
|
152
|
+
case "char":
|
|
153
|
+
case "text":
|
|
154
|
+
this.ed.insert(key.text);
|
|
155
|
+
break;
|
|
156
|
+
case "enter":
|
|
157
|
+
this.submit(menu);
|
|
158
|
+
return;
|
|
159
|
+
case "tab":
|
|
160
|
+
if (menu.length > 0) {
|
|
161
|
+
this.ed.set("/" + menu[Math.min(this.menuIndex, menu.length - 1)].name + " ");
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case "shifttab":
|
|
165
|
+
this.onModeCycle?.();
|
|
166
|
+
break;
|
|
167
|
+
case "backspace":
|
|
168
|
+
this.ed.backspace();
|
|
169
|
+
break;
|
|
170
|
+
case "delete":
|
|
171
|
+
this.ed.del();
|
|
172
|
+
break;
|
|
173
|
+
case "left":
|
|
174
|
+
this.ed.left();
|
|
175
|
+
break;
|
|
176
|
+
case "right":
|
|
177
|
+
this.ed.right();
|
|
178
|
+
break;
|
|
179
|
+
case "home":
|
|
180
|
+
case "ctrla":
|
|
181
|
+
this.ed.home();
|
|
182
|
+
break;
|
|
183
|
+
case "end":
|
|
184
|
+
case "ctrle":
|
|
185
|
+
this.ed.end();
|
|
186
|
+
break;
|
|
187
|
+
case "ctrlu":
|
|
188
|
+
this.ed.killToLineStart();
|
|
189
|
+
break;
|
|
190
|
+
case "ctrlw":
|
|
191
|
+
this.ed.deleteWordBack();
|
|
192
|
+
break;
|
|
193
|
+
case "up":
|
|
194
|
+
if (menu.length > 0) {
|
|
195
|
+
this.menuIndex = (this.menuIndex - 1 + menu.length) % menu.length;
|
|
196
|
+
}
|
|
197
|
+
else if (!this.ed.upLine()) {
|
|
198
|
+
this.historyPrev();
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
case "down":
|
|
202
|
+
if (menu.length > 0) {
|
|
203
|
+
this.menuIndex = (this.menuIndex + 1) % menu.length;
|
|
204
|
+
}
|
|
205
|
+
else if (!this.ed.downLine()) {
|
|
206
|
+
this.historyNext();
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
case "esc":
|
|
210
|
+
this.ed.clear();
|
|
211
|
+
break;
|
|
212
|
+
case "ctrlc": {
|
|
213
|
+
if (this.ed.buffer.length > 0) {
|
|
214
|
+
this.ed.clear();
|
|
215
|
+
}
|
|
216
|
+
else if (Date.now() - this.lastCtrlC < 1500) {
|
|
217
|
+
this.onExit?.();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
this.lastCtrlC = Date.now();
|
|
222
|
+
this.notice = "press ctrl+c again to exit";
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case "ctrld":
|
|
227
|
+
if (this.ed.buffer.length === 0) {
|
|
228
|
+
this.onExit?.();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
this.redraw();
|
|
234
|
+
}
|
|
235
|
+
submit(menu) {
|
|
236
|
+
let text = this.ed.buffer;
|
|
237
|
+
if (menu.length > 0) {
|
|
238
|
+
text = "/" + menu[Math.min(this.menuIndex, menu.length - 1)].name;
|
|
239
|
+
}
|
|
240
|
+
text = text.trim();
|
|
241
|
+
if (!text)
|
|
242
|
+
return;
|
|
243
|
+
if (this.history[this.history.length - 1] !== text)
|
|
244
|
+
this.history.push(text);
|
|
245
|
+
this.hideFrame();
|
|
246
|
+
this.state = "hidden";
|
|
247
|
+
// Echo the user's message as an accent-barred block, opencode-style.
|
|
248
|
+
const block = text
|
|
249
|
+
.split("\n")
|
|
250
|
+
.map((l) => `${ACCENT}▌${RESET} ${util_1.c.bold(l)}`)
|
|
251
|
+
.join("\n");
|
|
252
|
+
process.stdout.write(`\n${block}\n\n`);
|
|
253
|
+
this.atLineStart = true;
|
|
254
|
+
this.lastKind = null;
|
|
255
|
+
const resolve = this.submitResolve;
|
|
256
|
+
this.submitResolve = null;
|
|
257
|
+
resolve?.(text);
|
|
258
|
+
}
|
|
259
|
+
historyPrev() {
|
|
260
|
+
if (this.history.length === 0)
|
|
261
|
+
return;
|
|
262
|
+
if (this.histIdx === -1) {
|
|
263
|
+
this.histStash = this.ed.buffer;
|
|
264
|
+
this.histIdx = this.history.length - 1;
|
|
265
|
+
}
|
|
266
|
+
else if (this.histIdx > 0) {
|
|
267
|
+
this.histIdx--;
|
|
268
|
+
}
|
|
269
|
+
else
|
|
270
|
+
return;
|
|
271
|
+
this.ed.set(this.history[this.histIdx]);
|
|
272
|
+
}
|
|
273
|
+
historyNext() {
|
|
274
|
+
if (this.histIdx === -1)
|
|
275
|
+
return;
|
|
276
|
+
if (this.histIdx < this.history.length - 1) {
|
|
277
|
+
this.histIdx++;
|
|
278
|
+
this.ed.set(this.history[this.histIdx]);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
this.histIdx = -1;
|
|
282
|
+
this.ed.set(this.histStash);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
keySelect(key) {
|
|
286
|
+
const s = this.sel;
|
|
287
|
+
const filtered = this.filteredOptions();
|
|
288
|
+
switch (key.type) {
|
|
289
|
+
case "up":
|
|
290
|
+
s.index = filtered.length ? (s.index - 1 + filtered.length) % filtered.length : 0;
|
|
291
|
+
break;
|
|
292
|
+
case "down":
|
|
293
|
+
case "tab":
|
|
294
|
+
s.index = filtered.length ? (s.index + 1) % filtered.length : 0;
|
|
295
|
+
break;
|
|
296
|
+
case "char":
|
|
297
|
+
case "text":
|
|
298
|
+
s.filter += key.text;
|
|
299
|
+
s.index = 0;
|
|
300
|
+
break;
|
|
301
|
+
case "backspace":
|
|
302
|
+
s.filter = s.filter.slice(0, -1);
|
|
303
|
+
s.index = 0;
|
|
304
|
+
break;
|
|
305
|
+
case "enter": {
|
|
306
|
+
if (!filtered.length)
|
|
307
|
+
break;
|
|
308
|
+
const original = s.options.indexOf(filtered[Math.min(s.index, filtered.length - 1)]);
|
|
309
|
+
this.endSelect(original);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
case "esc":
|
|
313
|
+
case "ctrlc":
|
|
314
|
+
this.endSelect(null);
|
|
315
|
+
return;
|
|
316
|
+
default:
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
this.redraw();
|
|
320
|
+
}
|
|
321
|
+
endSelect(result) {
|
|
322
|
+
const s = this.sel;
|
|
323
|
+
this.hideFrame();
|
|
324
|
+
process.stdout.write("\x1b[?25h");
|
|
325
|
+
this.sel = null;
|
|
326
|
+
this.state = "hidden";
|
|
327
|
+
s.resolve(result);
|
|
328
|
+
}
|
|
329
|
+
filteredOptions() {
|
|
330
|
+
const s = this.sel;
|
|
331
|
+
if (!s.filter)
|
|
332
|
+
return s.options;
|
|
333
|
+
const f = s.filter.toLowerCase();
|
|
334
|
+
return s.options.filter((o) => o.label.toLowerCase().includes(f));
|
|
335
|
+
}
|
|
336
|
+
keyConfirm(key) {
|
|
337
|
+
const cs = this.confirmState;
|
|
338
|
+
let result = null;
|
|
339
|
+
if (key.type === "char") {
|
|
340
|
+
const ch = key.text.toLowerCase();
|
|
341
|
+
if (ch === "y")
|
|
342
|
+
result = "yes";
|
|
343
|
+
else if (ch === "n")
|
|
344
|
+
result = "no";
|
|
345
|
+
else if (ch === "a")
|
|
346
|
+
result = "always";
|
|
347
|
+
}
|
|
348
|
+
else if (key.type === "enter")
|
|
349
|
+
result = "yes";
|
|
350
|
+
else if (key.type === "esc" || key.type === "ctrlc")
|
|
351
|
+
result = "no";
|
|
352
|
+
if (result === null)
|
|
353
|
+
return;
|
|
354
|
+
this.hideFrame();
|
|
355
|
+
this.confirmState = null;
|
|
356
|
+
this.state = "hidden";
|
|
357
|
+
process.stdout.write(util_1.c.dim(` ${result === "always" ? "always allowed" : result} — ${cs.command}\n`));
|
|
358
|
+
cs.resolve(result);
|
|
359
|
+
}
|
|
360
|
+
// ---- rendering -----------------------------------------------------------
|
|
361
|
+
menuEntries() {
|
|
362
|
+
const b = this.ed.buffer;
|
|
363
|
+
if (!b.startsWith("/") || b.includes(" ") || b.includes("\n"))
|
|
364
|
+
return [];
|
|
365
|
+
const filter = b.slice(1).toLowerCase();
|
|
366
|
+
const list = this.slashCommands.filter((cmd) => cmd.name.startsWith(filter)).slice(0, 8);
|
|
367
|
+
if (filter !== this.lastMenuFilter) {
|
|
368
|
+
this.menuIndex = 0;
|
|
369
|
+
this.lastMenuFilter = filter;
|
|
370
|
+
}
|
|
371
|
+
if (this.menuIndex >= list.length)
|
|
372
|
+
this.menuIndex = 0;
|
|
373
|
+
return list;
|
|
374
|
+
}
|
|
375
|
+
width() {
|
|
376
|
+
return Math.max(30, (process.stdout.columns || 80) - 1);
|
|
377
|
+
}
|
|
378
|
+
redraw() {
|
|
379
|
+
const lines = [];
|
|
380
|
+
let cursorRow = -1;
|
|
381
|
+
let cursorCol = 0;
|
|
382
|
+
if (this.state === "idle") {
|
|
383
|
+
const w = this.width();
|
|
384
|
+
const menu = this.menuEntries();
|
|
385
|
+
const menuW = Math.min(w, 64);
|
|
386
|
+
for (let i = 0; i < menu.length; i++) {
|
|
387
|
+
const row = ` /${menu[i].name.padEnd(12)} ${menu[i].desc}`.slice(0, menuW).padEnd(menuW);
|
|
388
|
+
lines.push(i === this.menuIndex
|
|
389
|
+
? `${SEL}${row}${RESET}`
|
|
390
|
+
: ` ${util_1.c.bold("/" + menu[i].name.padEnd(12))} ${util_1.c.dim(menu[i].desc)}`);
|
|
391
|
+
}
|
|
392
|
+
const inputW = w - 2;
|
|
393
|
+
lines.push(boxRow("", w)); // top padding
|
|
394
|
+
if (this.ed.buffer.length === 0) {
|
|
395
|
+
cursorRow = lines.length;
|
|
396
|
+
cursorCol = 2;
|
|
397
|
+
lines.push(boxRow(util_1.c.dim(this.placeholder.slice(0, inputW)), w));
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const lay = (0, editor_1.layoutBuffer)(this.ed.buffer, this.ed.cursor, inputW);
|
|
401
|
+
cursorRow = lines.length + lay.curRow;
|
|
402
|
+
cursorCol = 2 + lay.curCol;
|
|
403
|
+
for (const row of lay.rows)
|
|
404
|
+
lines.push(boxRow(row, w));
|
|
405
|
+
}
|
|
406
|
+
lines.push(boxRow("", w)); // spacer
|
|
407
|
+
lines.push(boxRow(this.getStatus(), w));
|
|
408
|
+
lines.push(boxRow("", w)); // bottom padding
|
|
409
|
+
if (this.notice) {
|
|
410
|
+
lines.push(" " + util_1.c.yellow(this.notice));
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const keys = "/ commands · shift+tab mode";
|
|
414
|
+
const left = this.hintLeft
|
|
415
|
+
? this.hintLeft.length + keys.length + 5 > w
|
|
416
|
+
? "…" + this.hintLeft.slice(-(w - keys.length - 6))
|
|
417
|
+
: this.hintLeft
|
|
418
|
+
: "";
|
|
419
|
+
lines.push(util_1.c.dim(` ${left}${left ? " " : ""}${keys}`));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else if (this.state === "select" && this.sel) {
|
|
423
|
+
const s = this.sel;
|
|
424
|
+
const w = Math.min(this.width(), 64);
|
|
425
|
+
lines.push(BAR + util_1.c.bold(s.title) + " " + util_1.c.dim("esc cancel"));
|
|
426
|
+
lines.push(BAR + (s.filter ? s.filter : util_1.c.dim("type to filter")));
|
|
427
|
+
const filtered = this.filteredOptions();
|
|
428
|
+
if (!filtered.length)
|
|
429
|
+
lines.push(util_1.c.dim(" no matches"));
|
|
430
|
+
for (let i = 0; i < Math.min(filtered.length, 10); i++) {
|
|
431
|
+
const o = filtered[i];
|
|
432
|
+
const marker = o.current ? "● " : " ";
|
|
433
|
+
const plain = ` ${marker}${o.label}${o.hint ? " " + o.hint : ""}`.slice(0, w).padEnd(w);
|
|
434
|
+
lines.push(i === s.index
|
|
435
|
+
? `${SEL}${plain}${RESET}`
|
|
436
|
+
: ` ${o.current ? util_1.c.green(marker) : marker}${o.label}${o.hint ? " " + util_1.c.dim(o.hint) : ""}`);
|
|
437
|
+
}
|
|
438
|
+
if (filtered.length > 10)
|
|
439
|
+
lines.push(util_1.c.dim(` … ${filtered.length - 10} more (type to filter)`));
|
|
440
|
+
}
|
|
441
|
+
else if (this.state === "confirm" && this.confirmState) {
|
|
442
|
+
lines.push(BAR + util_1.c.yellow("run? ") + util_1.c.bold(this.confirmState.command.slice(0, this.width() - 8)));
|
|
443
|
+
if (this.confirmState.reason)
|
|
444
|
+
lines.push(" " + util_1.c.dim(this.confirmState.reason));
|
|
445
|
+
const program = this.confirmState.command.trim().split(/\s+/)[0];
|
|
446
|
+
lines.push(" " + util_1.c.dim(`[y]es · [n]o · [a]lways allow '${program}' this session`));
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
this.writeFrame(lines, cursorRow, cursorCol);
|
|
452
|
+
}
|
|
453
|
+
writeFrame(lines, cursorRow, cursorCol) {
|
|
454
|
+
let seq = "";
|
|
455
|
+
if (this.prevLines > 0) {
|
|
456
|
+
if (this.offsetFromBottom > 0)
|
|
457
|
+
seq += `\x1b[${this.offsetFromBottom}B`;
|
|
458
|
+
seq += "\r";
|
|
459
|
+
if (this.prevLines > 1)
|
|
460
|
+
seq += `\x1b[${this.prevLines - 1}A`;
|
|
461
|
+
seq += "\x1b[J";
|
|
462
|
+
}
|
|
463
|
+
seq += lines.join("\n");
|
|
464
|
+
// park the cursor
|
|
465
|
+
if (cursorRow >= 0 && cursorRow < lines.length) {
|
|
466
|
+
const up = lines.length - 1 - cursorRow;
|
|
467
|
+
if (up > 0)
|
|
468
|
+
seq += `\x1b[${up}A`;
|
|
469
|
+
seq += "\r";
|
|
470
|
+
if (cursorCol > 0)
|
|
471
|
+
seq += `\x1b[${cursorCol}C`;
|
|
472
|
+
this.offsetFromBottom = up;
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
this.offsetFromBottom = 0;
|
|
476
|
+
}
|
|
477
|
+
process.stdout.write(seq);
|
|
478
|
+
this.prevLines = lines.length;
|
|
479
|
+
}
|
|
480
|
+
hideFrame() {
|
|
481
|
+
if (this.prevLines === 0)
|
|
482
|
+
return;
|
|
483
|
+
let seq = "";
|
|
484
|
+
if (this.offsetFromBottom > 0)
|
|
485
|
+
seq += `\x1b[${this.offsetFromBottom}B`;
|
|
486
|
+
seq += "\r";
|
|
487
|
+
if (this.prevLines > 1)
|
|
488
|
+
seq += `\x1b[${this.prevLines - 1}A`;
|
|
489
|
+
seq += "\x1b[J";
|
|
490
|
+
process.stdout.write(seq);
|
|
491
|
+
this.prevLines = 0;
|
|
492
|
+
this.offsetFromBottom = 0;
|
|
493
|
+
}
|
|
494
|
+
/** Redraw the frame if one is on screen (status bar refresh, etc.). */
|
|
495
|
+
refresh() {
|
|
496
|
+
if (this.state !== "hidden")
|
|
497
|
+
this.redraw();
|
|
498
|
+
}
|
|
499
|
+
// ---- AgentUI (output while the agent runs) -------------------------------
|
|
500
|
+
out(s) {
|
|
501
|
+
if (this.tickerOn)
|
|
502
|
+
this.endTicker();
|
|
503
|
+
if (this.state !== "hidden") {
|
|
504
|
+
this.hideFrame();
|
|
505
|
+
process.stdout.write(s);
|
|
506
|
+
this.redraw();
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
process.stdout.write(s);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
thinkBuf = "";
|
|
513
|
+
thinkStart = 0;
|
|
514
|
+
tickerOn = false;
|
|
515
|
+
ensureLine() {
|
|
516
|
+
if (this.tickerOn) {
|
|
517
|
+
this.endTicker();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!this.atLineStart) {
|
|
521
|
+
this.out("\n");
|
|
522
|
+
this.atLineStart = true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
token(text) {
|
|
526
|
+
this.stopSpinner();
|
|
527
|
+
this.lastKind = "content";
|
|
528
|
+
this.out(text);
|
|
529
|
+
this.atLineStart = text.endsWith("\n");
|
|
530
|
+
}
|
|
531
|
+
/** Reasoning streams as ONE grey line, cropped to the latest tail and
|
|
532
|
+
* overwritten in place — a live pulse, not a wall of text. It collapses to
|
|
533
|
+
* "✦ thought for Ns" the moment real output starts. */
|
|
534
|
+
thinking(text) {
|
|
535
|
+
this.stopSpinner();
|
|
536
|
+
if (this.state !== "hidden")
|
|
537
|
+
return;
|
|
538
|
+
if (!this.tickerOn) {
|
|
539
|
+
if (!this.atLineStart)
|
|
540
|
+
this.out("\n");
|
|
541
|
+
this.tickerOn = true;
|
|
542
|
+
this.thinkStart = Date.now();
|
|
543
|
+
this.thinkBuf = "";
|
|
544
|
+
}
|
|
545
|
+
this.thinkBuf += text;
|
|
546
|
+
if (this.thinkBuf.length > 4000)
|
|
547
|
+
this.thinkBuf = this.thinkBuf.slice(-2000);
|
|
548
|
+
const w = Math.max(20, (process.stdout.columns || 80) - 4);
|
|
549
|
+
const clean = this.thinkBuf.replace(/\s+/g, " ").trim();
|
|
550
|
+
const tail = clean.length > w ? "…" + clean.slice(-(w - 1)) : clean;
|
|
551
|
+
process.stdout.write("\r\x1b[2K" + util_1.c.gray("✦ " + tail));
|
|
552
|
+
this.atLineStart = false;
|
|
553
|
+
}
|
|
554
|
+
endTicker() {
|
|
555
|
+
if (!this.tickerOn)
|
|
556
|
+
return;
|
|
557
|
+
this.tickerOn = false;
|
|
558
|
+
const secs = ((Date.now() - this.thinkStart) / 1000).toFixed(1);
|
|
559
|
+
process.stdout.write("\r\x1b[2K" + util_1.c.gray(`✦ thought for ${secs}s`) + "\n");
|
|
560
|
+
this.thinkBuf = "";
|
|
561
|
+
this.atLineStart = true;
|
|
562
|
+
}
|
|
563
|
+
toolCall(name, args) {
|
|
564
|
+
this.stopSpinner();
|
|
565
|
+
this.ensureLine();
|
|
566
|
+
this.lastKind = null;
|
|
567
|
+
this.out(`${util_1.c.cyan("→")} ${util_1.c.bold(name)} ${util_1.c.dim((0, ui_1.summarizeArgs)(name, args))}\n`);
|
|
568
|
+
}
|
|
569
|
+
toolResult(result) {
|
|
570
|
+
this.ensureLine();
|
|
571
|
+
const firstLine = result.split("\n")[0] ?? "";
|
|
572
|
+
const isError = firstLine.startsWith("Error");
|
|
573
|
+
const lineCount = result.split("\n").length;
|
|
574
|
+
const label = isError
|
|
575
|
+
? util_1.c.red(firstLine.slice(0, 120))
|
|
576
|
+
: util_1.c.dim(firstLine.slice(0, 100) + (lineCount > 1 ? ` (+${lineCount - 1} lines)` : ""));
|
|
577
|
+
this.out(` ${isError ? util_1.c.red("✗") : util_1.c.green("✓")} ${label}\n`);
|
|
578
|
+
}
|
|
579
|
+
println(s = "") {
|
|
580
|
+
this.stopSpinner();
|
|
581
|
+
this.ensureLine();
|
|
582
|
+
this.lastKind = null;
|
|
583
|
+
this.out(s + "\n");
|
|
584
|
+
this.atLineStart = true;
|
|
585
|
+
}
|
|
586
|
+
status(s) {
|
|
587
|
+
this.println(util_1.c.gray(s));
|
|
588
|
+
}
|
|
589
|
+
turnEnd(label) {
|
|
590
|
+
this.ensureLine();
|
|
591
|
+
this.lastKind = null;
|
|
592
|
+
this.out(`${util_1.c.dim("■ " + label)}\n\n`);
|
|
593
|
+
this.atLineStart = true;
|
|
594
|
+
}
|
|
595
|
+
planUpdated(plan) {
|
|
596
|
+
this.stopSpinner();
|
|
597
|
+
this.ensureLine();
|
|
598
|
+
this.lastKind = null;
|
|
599
|
+
this.out((0, ui_1.renderPlan)(plan) + "\n");
|
|
600
|
+
this.atLineStart = true;
|
|
601
|
+
}
|
|
602
|
+
warn(s) {
|
|
603
|
+
this.println(util_1.c.yellow(s));
|
|
604
|
+
}
|
|
605
|
+
error(s) {
|
|
606
|
+
this.println(util_1.c.red(s));
|
|
607
|
+
}
|
|
608
|
+
startSpinner(label) {
|
|
609
|
+
if (!process.stdout.isTTY || this.state !== "hidden")
|
|
610
|
+
return;
|
|
611
|
+
this.stopSpinner();
|
|
612
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
613
|
+
let i = 0;
|
|
614
|
+
const started = Date.now();
|
|
615
|
+
this.spinnerActive = true;
|
|
616
|
+
this.spinnerTimer = setInterval(() => {
|
|
617
|
+
const secs = Math.floor((Date.now() - started) / 1000);
|
|
618
|
+
process.stdout.write(`\r${util_1.c.cyan(frames[i++ % frames.length])} ${util_1.c.dim(label + (secs > 2 ? ` ${secs}s` : "") + " · esc to cancel")} `);
|
|
619
|
+
}, 100);
|
|
620
|
+
}
|
|
621
|
+
stopSpinner() {
|
|
622
|
+
if (this.spinnerTimer) {
|
|
623
|
+
clearInterval(this.spinnerTimer);
|
|
624
|
+
this.spinnerTimer = null;
|
|
625
|
+
}
|
|
626
|
+
if (this.spinnerActive) {
|
|
627
|
+
process.stdout.write("\r" + " ".repeat(70) + "\r");
|
|
628
|
+
this.spinnerActive = false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
exports.Tui = Tui;
|