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