@quatcode/agent 0.2.5 → 0.2.7
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/input.d.ts +6 -2
- package/dist/input.d.ts.map +1 -1
- package/dist/input.js +87 -31
- package/dist/input.js.map +1 -1
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +20 -2
- package/dist/keyboard.js.map +1 -1
- package/dist/theme.d.ts +1 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +2 -0
- package/dist/theme.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +282 -45
- package/dist/tui.js.map +1 -1
- package/package.json +2 -2
package/dist/tui.js
CHANGED
|
@@ -10,7 +10,7 @@ import { Picker } from "./picker.js";
|
|
|
10
10
|
import { Screen } from "./screen.js";
|
|
11
11
|
import { TurnController, fmtCost, fmtDuration, fmtTokens, toolArg, toolResultBlock } from "./turn.js";
|
|
12
12
|
import { renderBanner } from "./logo.js";
|
|
13
|
-
import { renderMarkdown } from "./markdown.js";
|
|
13
|
+
import { renderMarkdown, wrapSegments } from "./markdown.js";
|
|
14
14
|
import { ADD_PROVIDER, allProviders, buildProvider, defaultModelFor, effortsForModel, hasKey, keyEnvFor, probeProviderModels, providerEnvKey, providerHasModel, providerMeta, providerSlug, registerCustomProvider, removeCustomProvider, resolveProviderForModel, } from "./models.js";
|
|
15
15
|
import { activeThemeId, bg, bold, buildColorRemap, clipAnsi, fg, getThemes, glyphs, padEndTo, palette, remapColors, setTheme, stringWidth, stripAnsi, tone, toolGlyph, } from "./theme.js";
|
|
16
16
|
import { listSessionsForDir, loadConfig, makeSessionId, saveConfig, saveSession, deleteSession } from "./store.js";
|
|
@@ -24,6 +24,7 @@ const COMMAND_INFO = [
|
|
|
24
24
|
{ name: "/new", desc: "new conversation (clears the screen)" },
|
|
25
25
|
{ name: "/model", args: "[id]", desc: "pick or set the model" },
|
|
26
26
|
{ name: "/provider", args: "[add|list|remove]", desc: "connect a provider by API key" },
|
|
27
|
+
{ name: "/btw", args: "<question>", desc: "ask a side question without interrupting the task" },
|
|
27
28
|
{ name: "/effort", args: "[level]", desc: "reasoning effort (minimal…high)" },
|
|
28
29
|
{ name: "/key", desc: "enter the API key for the current provider" },
|
|
29
30
|
{ name: "/dir", args: "[path]", desc: "change the project directory" },
|
|
@@ -136,6 +137,14 @@ class TuiApp {
|
|
|
136
137
|
modeBadgeWidth = 0;
|
|
137
138
|
modeBadgeHover = false;
|
|
138
139
|
versionNotice = null;
|
|
140
|
+
bottomHintStart = -1;
|
|
141
|
+
bottomHintWidth = 0;
|
|
142
|
+
bottomHintRow = -1;
|
|
143
|
+
queue = [];
|
|
144
|
+
inputScroll = 0;
|
|
145
|
+
lastInputTotalRows = 0;
|
|
146
|
+
hoverRow = -1;
|
|
147
|
+
hoverCol = -1;
|
|
139
148
|
constructor(options, input, output) {
|
|
140
149
|
this.opts = options;
|
|
141
150
|
this.dir = path.resolve(options.dir);
|
|
@@ -237,8 +246,10 @@ class TuiApp {
|
|
|
237
246
|
const prompt = this.opts.initialPrompt;
|
|
238
247
|
this.buffer.addHistory(prompt);
|
|
239
248
|
this.clearSplash();
|
|
240
|
-
this.
|
|
249
|
+
this.appendUserBlock(prompt, 0);
|
|
241
250
|
await this.turn(prompt);
|
|
251
|
+
if (await this.drainQueue())
|
|
252
|
+
return this.finish();
|
|
242
253
|
}
|
|
243
254
|
for (;;) {
|
|
244
255
|
const result = await this.readLine();
|
|
@@ -264,7 +275,7 @@ class TuiApp {
|
|
|
264
275
|
transcriptLength: transcriptAt,
|
|
265
276
|
};
|
|
266
277
|
this.messageCursor = -1;
|
|
267
|
-
this.
|
|
278
|
+
const blockStart = this.appendUserBlock(input, images.length);
|
|
268
279
|
if (isCommand) {
|
|
269
280
|
this.turnUndo = null;
|
|
270
281
|
const shouldExit = await this.command(input);
|
|
@@ -272,9 +283,14 @@ class TuiApp {
|
|
|
272
283
|
break;
|
|
273
284
|
continue;
|
|
274
285
|
}
|
|
275
|
-
this.userMarkers.push({ line:
|
|
286
|
+
this.userMarkers.push({ line: blockStart, text: input });
|
|
276
287
|
await this.turn(input, images);
|
|
288
|
+
if (await this.drainQueue())
|
|
289
|
+
break;
|
|
277
290
|
}
|
|
291
|
+
return this.finish();
|
|
292
|
+
}
|
|
293
|
+
async finish() {
|
|
278
294
|
this.printGoodbye();
|
|
279
295
|
this.renderNow();
|
|
280
296
|
await this.pause(120);
|
|
@@ -374,6 +390,12 @@ class TuiApp {
|
|
|
374
390
|
this.moveSuggestion(key.kind === "wheelup" ? -1 : 1);
|
|
375
391
|
continue;
|
|
376
392
|
}
|
|
393
|
+
if ((this.mode === "editing" || this.mode === "running") &&
|
|
394
|
+
this.inputOverflow() &&
|
|
395
|
+
this.pointerOverInput()) {
|
|
396
|
+
this.scrollInput(key.kind === "wheelup" ? 3 : -3);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
377
399
|
const delta = key.kind === "wheelup" ? 3 : -3;
|
|
378
400
|
if (this.mode === "picker" && this.picker) {
|
|
379
401
|
const dir = key.kind === "wheelup" ? "up" : "down";
|
|
@@ -433,10 +455,21 @@ class TuiApp {
|
|
|
433
455
|
this.selection = null;
|
|
434
456
|
this.scheduleRender();
|
|
435
457
|
}
|
|
458
|
+
if (key.type === "nav" && key.dir === "down" && key.ctrl) {
|
|
459
|
+
this.scrollOffset = 0;
|
|
460
|
+
this.scheduleRender();
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
436
463
|
if (this.mode === "editing")
|
|
437
464
|
this.handleEditKey(key);
|
|
438
|
-
else if (this.mode === "running")
|
|
439
|
-
|
|
465
|
+
else if (this.mode === "running") {
|
|
466
|
+
const abortKey = key.type === "escape" || (key.type === "control" && key.ch === "c");
|
|
467
|
+
const undoKey = key.type === "control" && key.ch === "z";
|
|
468
|
+
if (abortKey || undoKey)
|
|
469
|
+
this.handleRunKey(key);
|
|
470
|
+
else
|
|
471
|
+
this.handleEditKey(key);
|
|
472
|
+
}
|
|
440
473
|
else if (this.mode === "permission")
|
|
441
474
|
this.handleConfirmKey(key);
|
|
442
475
|
else if (this.mode === "picker")
|
|
@@ -466,6 +499,10 @@ class TuiApp {
|
|
|
466
499
|
if (key.type === "text" || key.type === "backspace" || key.type === "delete") {
|
|
467
500
|
this.suggestionIndex = 0;
|
|
468
501
|
}
|
|
502
|
+
if (key.type === "text" || key.type === "backspace" || key.type === "delete") {
|
|
503
|
+
this.suggestionIndex = 0;
|
|
504
|
+
this.inputScroll = 0;
|
|
505
|
+
}
|
|
469
506
|
switch (key.type) {
|
|
470
507
|
case "text":
|
|
471
508
|
this.buffer.insert(key.text);
|
|
@@ -473,14 +510,16 @@ class TuiApp {
|
|
|
473
510
|
case "control":
|
|
474
511
|
switch (key.ch) {
|
|
475
512
|
case "c":
|
|
476
|
-
if (this.buffer.isEmpty())
|
|
513
|
+
if (this.buffer.isEmpty() && this.mode !== "running")
|
|
477
514
|
this.finishLine({ kind: "interrupt" });
|
|
478
515
|
else
|
|
479
516
|
this.buffer.clearLine();
|
|
480
517
|
break;
|
|
481
518
|
case "d":
|
|
482
|
-
if (this.buffer.isEmpty())
|
|
483
|
-
this.
|
|
519
|
+
if (this.buffer.isEmpty()) {
|
|
520
|
+
if (this.mode !== "running")
|
|
521
|
+
this.finishLine({ kind: "eof" });
|
|
522
|
+
}
|
|
484
523
|
else
|
|
485
524
|
this.buffer.deleteForward();
|
|
486
525
|
break;
|
|
@@ -543,7 +582,7 @@ class TuiApp {
|
|
|
543
582
|
if (!this.deleteImageTokenAfter())
|
|
544
583
|
this.buffer.deleteWordAfter();
|
|
545
584
|
}
|
|
546
|
-
else if (key.ch === "v" || key.ch === "V") {
|
|
585
|
+
else if (key.ch === "v" || key.ch === "V" || key.ch === "м" || key.ch === "М") {
|
|
547
586
|
void this.pasteFromClipboard();
|
|
548
587
|
return;
|
|
549
588
|
}
|
|
@@ -554,6 +593,7 @@ class TuiApp {
|
|
|
554
593
|
this.moveSuggestion(key.dir === "up" ? -1 : 1);
|
|
555
594
|
return;
|
|
556
595
|
}
|
|
596
|
+
this.inputScroll = 0;
|
|
557
597
|
if (key.dir === "up") {
|
|
558
598
|
if (!this.buffer.moveVertical(-1))
|
|
559
599
|
this.buffer.historyPrev();
|
|
@@ -588,6 +628,7 @@ class TuiApp {
|
|
|
588
628
|
}
|
|
589
629
|
break;
|
|
590
630
|
case "newline":
|
|
631
|
+
this.inputScroll = 0;
|
|
591
632
|
this.buffer.insert("\n");
|
|
592
633
|
break;
|
|
593
634
|
case "enter": {
|
|
@@ -599,7 +640,7 @@ class TuiApp {
|
|
|
599
640
|
const index = Math.max(0, Math.min(this.suggestionIndex, matches.length - 1));
|
|
600
641
|
const selected = matches[index];
|
|
601
642
|
if (selected) {
|
|
602
|
-
this.
|
|
643
|
+
this.submitText(selected.name);
|
|
603
644
|
return;
|
|
604
645
|
}
|
|
605
646
|
}
|
|
@@ -612,13 +653,13 @@ class TuiApp {
|
|
|
612
653
|
if (unique.length === 1)
|
|
613
654
|
text = unique[0].name;
|
|
614
655
|
}
|
|
615
|
-
this.
|
|
656
|
+
this.submitText(text);
|
|
616
657
|
return;
|
|
617
658
|
}
|
|
618
659
|
case "tab":
|
|
619
660
|
if (this.suggestionMatches().length > 0)
|
|
620
661
|
this.completeCommand();
|
|
621
|
-
else
|
|
662
|
+
else if (this.mode !== "running")
|
|
622
663
|
this.toggleMode();
|
|
623
664
|
break;
|
|
624
665
|
case "backspace":
|
|
@@ -644,13 +685,104 @@ class TuiApp {
|
|
|
644
685
|
this.scheduleRender();
|
|
645
686
|
resolve?.(result);
|
|
646
687
|
}
|
|
688
|
+
submitText(raw) {
|
|
689
|
+
if (this.mode !== "running") {
|
|
690
|
+
this.buffer.reset();
|
|
691
|
+
this.inputScroll = 0;
|
|
692
|
+
this.finishLine({ kind: "submit", text: raw });
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const extracted = extractInputImages(raw, this.pendingImages);
|
|
696
|
+
this.pendingImages = new Map();
|
|
697
|
+
const text = extracted.text;
|
|
698
|
+
const images = extracted.images;
|
|
699
|
+
if (text)
|
|
700
|
+
this.buffer.addHistory(text);
|
|
701
|
+
this.buffer.reset();
|
|
702
|
+
this.inputScroll = 0;
|
|
703
|
+
if (/^\/btw\b/.test(text)) {
|
|
704
|
+
void this.runBtw(text.replace(/^\/btw\s*/, ""));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
this.queue.push({ text, images });
|
|
708
|
+
this.appendUserBlock(text, images.length, [
|
|
709
|
+
` ${tone.mist("// QUEUED — will be sent when the current task finishes")}`,
|
|
710
|
+
]);
|
|
711
|
+
this.scheduleRender();
|
|
712
|
+
}
|
|
713
|
+
async drainQueue() {
|
|
714
|
+
while (this.queue.length > 0) {
|
|
715
|
+
const next = this.queue.shift();
|
|
716
|
+
if (!next.text.trim())
|
|
717
|
+
continue;
|
|
718
|
+
if (next.text.startsWith("/")) {
|
|
719
|
+
const shouldExit = await this.command(next.text);
|
|
720
|
+
if (shouldExit)
|
|
721
|
+
return true;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
this.append([` ${tone.mist("// SENDING QUEUED MESSAGE")}`]);
|
|
725
|
+
this.clearSplash();
|
|
726
|
+
this.userMarkers.push({ line: this.transcript.length, text: next.text });
|
|
727
|
+
await this.turn(next.text, next.images);
|
|
728
|
+
}
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
async runBtw(question) {
|
|
732
|
+
const text = question.trim();
|
|
733
|
+
if (!text) {
|
|
734
|
+
this.append([` ${tone.mist("// usage: /btw <question>")}`]);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const provider = this.ensureProvider();
|
|
738
|
+
const inner = Math.min(Math.max(44, this.contentWidth() - 8), 66);
|
|
739
|
+
if (!provider) {
|
|
740
|
+
this.append([
|
|
741
|
+
` ${tone.red(glyphs.cross)} ${tone.mist("// BTW needs a configured provider")}`,
|
|
742
|
+
]);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const pending = box("BTW", [tone.silver(clipAnsi(text, inner - 2)), "", tone.mist("// thinking…")], inner, palette.steel);
|
|
746
|
+
this.append([...pending, ""]);
|
|
747
|
+
const start = Math.max(0, this.transcript.length - pending.length - 1);
|
|
748
|
+
try {
|
|
749
|
+
const response = await provider.complete({
|
|
750
|
+
model: this.model,
|
|
751
|
+
system: `${this.system ? `${this.system}\n\n` : ""}# side channel\nThe user is asking a quick side question while the main task runs. Answer briefly and directly. Do not use tools, do not change the main task, and do not ask to run anything.`,
|
|
752
|
+
messages: [
|
|
753
|
+
...this.session.getMessages(),
|
|
754
|
+
{ role: "user", content: [{ type: "text", text }] },
|
|
755
|
+
],
|
|
756
|
+
maxTokens: 2048,
|
|
757
|
+
});
|
|
758
|
+
const answer = response.content
|
|
759
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
760
|
+
.join("")
|
|
761
|
+
.trim();
|
|
762
|
+
const answerLines = answer
|
|
763
|
+
? renderMarkdown(answer, { width: inner - 2, indent: 2 })
|
|
764
|
+
: [tone.mist("// the model returned no answer")];
|
|
765
|
+
const card = box("BTW", [tone.silver(clipAnsi(text, inner - 2)), "", ...answerLines], inner, palette.steel);
|
|
766
|
+
this.transcript.splice(start, pending.length + 1, ...card, "");
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
const card = box("BTW", [
|
|
770
|
+
tone.silver(clipAnsi(text, inner - 2)),
|
|
771
|
+
"",
|
|
772
|
+
tone.red(`// ${errorMessage(err)}`),
|
|
773
|
+
], inner, palette.red);
|
|
774
|
+
this.transcript.splice(start, pending.length + 1, ...card, "");
|
|
775
|
+
}
|
|
776
|
+
this.scheduleRender();
|
|
777
|
+
}
|
|
647
778
|
readLine(overrides = {}) {
|
|
648
779
|
const initial = overrides.initial ?? this.pendingInitial ?? "";
|
|
649
780
|
this.pendingInitial = null;
|
|
650
781
|
if (initial)
|
|
651
782
|
this.buffer.setText(initial);
|
|
652
|
-
else
|
|
783
|
+
else if (this.buffer.text().trim() === "")
|
|
653
784
|
this.buffer.reset();
|
|
785
|
+
this.inputScroll = 0;
|
|
654
786
|
this.inputPrompt = overrides.prompt ?? defaultPrompt();
|
|
655
787
|
this.inputMask = overrides.mask ?? false;
|
|
656
788
|
this.inputPlaceholder = overrides.placeholder ?? "ask anything, or /help";
|
|
@@ -735,17 +867,58 @@ class TuiApp {
|
|
|
735
867
|
if (!rendered)
|
|
736
868
|
return false;
|
|
737
869
|
const top = this.lastHeaderRows + this.lastViewport + this.lastInputOffset;
|
|
738
|
-
const
|
|
739
|
-
if (
|
|
870
|
+
const rowIndex = row - top;
|
|
871
|
+
if (rowIndex < 0 || rowIndex >= rendered.lines.length)
|
|
872
|
+
return false;
|
|
873
|
+
const visual = rendered.rows[rowIndex];
|
|
874
|
+
if (!visual)
|
|
740
875
|
return false;
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
const start = rendered.lineStarts[lineIndex] ?? 0;
|
|
744
|
-
this.buffer.setCursorFromDisplay(lineIndex, Math.max(0, textCol), start);
|
|
876
|
+
const textCol = col - this.padX - visual.prefixWidth;
|
|
877
|
+
this.buffer.setCursorFromDisplay(visual.line, Math.max(0, textCol), visual.start);
|
|
745
878
|
this.scheduleRender();
|
|
746
879
|
return true;
|
|
747
880
|
}
|
|
881
|
+
inputWindowSize() {
|
|
882
|
+
return Math.max(3, Math.floor(this.screen.rows / 2));
|
|
883
|
+
}
|
|
884
|
+
inputOverflow() {
|
|
885
|
+
return this.lastInputTotalRows > this.inputWindowSize();
|
|
886
|
+
}
|
|
887
|
+
pointerOverInput() {
|
|
888
|
+
if (!this.lastInputRender || this.hoverRow < 0)
|
|
889
|
+
return false;
|
|
890
|
+
const top = this.lastHeaderRows + this.lastViewport + this.lastInputOffset;
|
|
891
|
+
return (this.hoverRow >= top && this.hoverRow < top + this.lastInputRender.lines.length);
|
|
892
|
+
}
|
|
893
|
+
scrollInput(delta) {
|
|
894
|
+
const maxOffset = Math.max(0, this.lastInputTotalRows - this.inputWindowSize());
|
|
895
|
+
this.inputScroll = Math.max(0, Math.min(this.inputScroll + delta, maxOffset));
|
|
896
|
+
this.scheduleRender();
|
|
897
|
+
}
|
|
898
|
+
bottomHintHit(row, col) {
|
|
899
|
+
if (this.bottomHintRow < 0 || this.bottomHintStart < 0)
|
|
900
|
+
return false;
|
|
901
|
+
if (row !== this.bottomHintRow)
|
|
902
|
+
return false;
|
|
903
|
+
const start = this.padX + this.bottomHintStart;
|
|
904
|
+
return col >= start && col < start + this.bottomHintWidth;
|
|
905
|
+
}
|
|
906
|
+
bottomRuleLine() {
|
|
907
|
+
const width = this.contentWidth();
|
|
908
|
+
const label = `${glyphs.arrowDown} bottom · ctrl+${glyphs.arrowDown}`;
|
|
909
|
+
this.bottomHintStart = -1;
|
|
910
|
+
this.bottomHintWidth = 0;
|
|
911
|
+
if (this.scrollOffset > 0 && this.mode !== "picker" && width > 36) {
|
|
912
|
+
const dashes = Math.max(8, width - 2 - stringWidth(label) - 2);
|
|
913
|
+
this.bottomHintStart = 2 + dashes + 2;
|
|
914
|
+
this.bottomHintWidth = stringWidth(label);
|
|
915
|
+
return ` ${tone.mist("─".repeat(dashes))} ${tone.silver(label)}`;
|
|
916
|
+
}
|
|
917
|
+
return ` ${tone.mist("─".repeat(Math.max(8, width - 2)))}`;
|
|
918
|
+
}
|
|
748
919
|
handleHover(row, col) {
|
|
920
|
+
this.hoverRow = row;
|
|
921
|
+
this.hoverCol = col;
|
|
749
922
|
if (row === 1) {
|
|
750
923
|
const hover = this.badgeHit(col);
|
|
751
924
|
if (hover !== this.modeBadgeHover) {
|
|
@@ -767,11 +940,11 @@ class TuiApp {
|
|
|
767
940
|
}
|
|
768
941
|
return;
|
|
769
942
|
}
|
|
770
|
-
if (this.mode === "editing") {
|
|
943
|
+
if (this.mode === "editing" || this.mode === "running") {
|
|
771
944
|
const matches = this.suggestionMatches();
|
|
772
945
|
if (matches.length === 0)
|
|
773
946
|
return;
|
|
774
|
-
const top = this.lastHeaderRows + this.lastViewport + 1;
|
|
947
|
+
const top = this.lastHeaderRows + this.lastViewport + 1 + (this.status ? 1 : 0);
|
|
775
948
|
const index = row - top;
|
|
776
949
|
if (index >= 0 && index < matches.length && index !== this.suggestionIndex) {
|
|
777
950
|
this.suggestionIndex = index;
|
|
@@ -791,6 +964,11 @@ class TuiApp {
|
|
|
791
964
|
this.toggleMode();
|
|
792
965
|
return;
|
|
793
966
|
}
|
|
967
|
+
if (this.bottomHintHit(row, col)) {
|
|
968
|
+
this.scrollOffset = 0;
|
|
969
|
+
this.scheduleRender();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
794
972
|
if (this.mode === "permission") {
|
|
795
973
|
const index = this.permissionOptionAt(row, col);
|
|
796
974
|
if (index !== null) {
|
|
@@ -802,16 +980,16 @@ class TuiApp {
|
|
|
802
980
|
}
|
|
803
981
|
return;
|
|
804
982
|
}
|
|
805
|
-
if (this.mode === "editing") {
|
|
983
|
+
if (this.mode === "editing" || this.mode === "running") {
|
|
806
984
|
if (this.placeInputCursor(row, col))
|
|
807
985
|
return;
|
|
808
986
|
const matches = this.suggestionMatches();
|
|
809
987
|
if (matches.length > 0) {
|
|
810
|
-
const top = this.lastHeaderRows + this.lastViewport + 1;
|
|
988
|
+
const top = this.lastHeaderRows + this.lastViewport + 1 + (this.status ? 1 : 0);
|
|
811
989
|
const index = row - top;
|
|
812
990
|
if (index >= 0 && index < matches.length) {
|
|
813
991
|
this.suggestionIndex = index;
|
|
814
|
-
this.
|
|
992
|
+
this.submitText(matches[index].name);
|
|
815
993
|
}
|
|
816
994
|
}
|
|
817
995
|
return;
|
|
@@ -1036,6 +1214,8 @@ class TuiApp {
|
|
|
1036
1214
|
const viewport = Math.max(3, rows - header.length - footer.length - this.padTop - this.padBottom);
|
|
1037
1215
|
this.lastHeaderRows = header.length + this.padTop;
|
|
1038
1216
|
this.lastViewport = viewport;
|
|
1217
|
+
this.bottomHintRow =
|
|
1218
|
+
this.bottomHintStart >= 0 ? this.lastHeaderRows + viewport + footer.length - 1 : -1;
|
|
1039
1219
|
let content;
|
|
1040
1220
|
if (this.mode === "picker" && this.picker) {
|
|
1041
1221
|
content = this.picker.render(this.contentWidth() - 2, viewport);
|
|
@@ -1051,7 +1231,10 @@ class TuiApp {
|
|
|
1051
1231
|
while (lines.length < rows)
|
|
1052
1232
|
lines.push("");
|
|
1053
1233
|
let cursor = null;
|
|
1054
|
-
if (this.mode === "editing"
|
|
1234
|
+
if ((this.mode === "editing" || this.mode === "running") &&
|
|
1235
|
+
this.lastInputRender &&
|
|
1236
|
+
this.lastInputRender.cursorRow >= 0 &&
|
|
1237
|
+
this.lastInputRender.cursorRow < this.lastInputRender.lines.length) {
|
|
1055
1238
|
const inputTop = this.padTop + header.length + viewport + this.lastInputOffset;
|
|
1056
1239
|
cursor = {
|
|
1057
1240
|
row: inputTop + this.lastInputRender.cursorRow,
|
|
@@ -1116,7 +1299,8 @@ class TuiApp {
|
|
|
1116
1299
|
footerLines() {
|
|
1117
1300
|
const width = this.contentWidth();
|
|
1118
1301
|
const rule = ` ${tone.mist("─".repeat(Math.max(8, width - 2)))}`;
|
|
1119
|
-
|
|
1302
|
+
const bottom = this.bottomRuleLine();
|
|
1303
|
+
if (this.mode === "editing" || this.mode === "running") {
|
|
1120
1304
|
const rendered = this.buffer.render({
|
|
1121
1305
|
width,
|
|
1122
1306
|
prompt: this.inputPrompt,
|
|
@@ -1124,28 +1308,55 @@ class TuiApp {
|
|
|
1124
1308
|
mask: this.inputMask,
|
|
1125
1309
|
style: styleInputLine,
|
|
1126
1310
|
});
|
|
1127
|
-
|
|
1311
|
+
const maxRows = this.inputWindowSize();
|
|
1312
|
+
const total = rendered.lines.length;
|
|
1313
|
+
this.lastInputTotalRows = total;
|
|
1314
|
+
if (total > maxRows) {
|
|
1315
|
+
const maxOffset = total - maxRows;
|
|
1316
|
+
const fromBottom = Math.max(0, Math.min(this.inputScroll, maxOffset));
|
|
1317
|
+
this.inputScroll = fromBottom;
|
|
1318
|
+
const start = maxOffset - fromBottom;
|
|
1319
|
+
this.lastInputRender = {
|
|
1320
|
+
lines: rendered.lines.slice(start, start + maxRows),
|
|
1321
|
+
rows: rendered.rows.slice(start, start + maxRows),
|
|
1322
|
+
cursorRow: rendered.cursorRow - start,
|
|
1323
|
+
cursorCol: rendered.cursorCol,
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
else {
|
|
1327
|
+
this.inputScroll = 0;
|
|
1328
|
+
this.lastInputRender = rendered;
|
|
1329
|
+
}
|
|
1128
1330
|
const suggestions = this.suggestions();
|
|
1129
|
-
|
|
1130
|
-
|
|
1331
|
+
const statusLines = this.status
|
|
1332
|
+
? [this.statusLine()]
|
|
1333
|
+
: this.mode === "running"
|
|
1334
|
+
? [` ${tone.mist("// esc interrupts · enter queues a message")}`]
|
|
1335
|
+
: [];
|
|
1336
|
+
this.lastInputOffset = 1 + statusLines.length + suggestions.length;
|
|
1337
|
+
return [rule, ...statusLines, ...suggestions, ...this.lastInputRender.lines, bottom];
|
|
1131
1338
|
}
|
|
1132
1339
|
this.lastInputRender = null;
|
|
1340
|
+
this.lastInputTotalRows = 0;
|
|
1133
1341
|
if (this.status)
|
|
1134
|
-
return [rule, this.statusLine(),
|
|
1135
|
-
if (this.mode === "running") {
|
|
1136
|
-
return [rule, ` ${tone.mist("// esc interrupts")}`, rule];
|
|
1137
|
-
}
|
|
1342
|
+
return [rule, this.statusLine(), bottom];
|
|
1138
1343
|
if (this.mode === "permission")
|
|
1139
|
-
return [rule, ` ${this.permissionHint}`,
|
|
1344
|
+
return [rule, ` ${this.permissionHint}`, bottom];
|
|
1140
1345
|
if (this.mode === "picker" && this.picker) {
|
|
1141
|
-
return [rule, this.picker.footer(this.picker.itemCount()),
|
|
1346
|
+
return [rule, this.picker.footer(this.picker.itemCount()), bottom];
|
|
1142
1347
|
}
|
|
1143
|
-
return [rule, "",
|
|
1348
|
+
return [rule, "", bottom];
|
|
1144
1349
|
}
|
|
1145
1350
|
statusLine() {
|
|
1146
1351
|
const spin = tone.accent(glyphs.spinner[this.spinnerFrame % glyphs.spinner.length] ?? "-");
|
|
1147
1352
|
const elapsed = ((performance.now() - this.statusStartedAt) / 1000).toFixed(1);
|
|
1148
|
-
const meta = [
|
|
1353
|
+
const meta = [
|
|
1354
|
+
this.status?.meta,
|
|
1355
|
+
`${elapsed}s`,
|
|
1356
|
+
this.queue.length > 0 ? `${this.queue.length} queued` : "",
|
|
1357
|
+
]
|
|
1358
|
+
.filter(Boolean)
|
|
1359
|
+
.join(" · ");
|
|
1149
1360
|
return ` ${spin} ${this.status?.label ?? ""} ${tone.mist(meta)}`;
|
|
1150
1361
|
}
|
|
1151
1362
|
visibleTranscript(viewport) {
|
|
@@ -1900,6 +2111,9 @@ class TuiApp {
|
|
|
1900
2111
|
case "help":
|
|
1901
2112
|
this.printHelp();
|
|
1902
2113
|
return false;
|
|
2114
|
+
case "btw":
|
|
2115
|
+
await this.runBtw(arg);
|
|
2116
|
+
return false;
|
|
1903
2117
|
case "new":
|
|
1904
2118
|
this.session = new Session(this.system);
|
|
1905
2119
|
this.conversationId = makeSessionId();
|
|
@@ -2144,8 +2358,12 @@ class TuiApp {
|
|
|
2144
2358
|
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
2145
2359
|
.join("\n");
|
|
2146
2360
|
const images = message.content.filter((part) => part.type === "image").length;
|
|
2147
|
-
if (text.trim() || images > 0)
|
|
2361
|
+
if (text.trim() || images > 0) {
|
|
2362
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "")
|
|
2363
|
+
lines.push("");
|
|
2148
2364
|
lines.push(...this.userBlockLines(text, images));
|
|
2365
|
+
lines.push("");
|
|
2366
|
+
}
|
|
2149
2367
|
continue;
|
|
2150
2368
|
}
|
|
2151
2369
|
if (message.role === "assistant") {
|
|
@@ -2687,6 +2905,8 @@ class TuiApp {
|
|
|
2687
2905
|
["sessions picker", "ctrl+r rename · ctrl+p pin · ctrl+d delete"],
|
|
2688
2906
|
["↑ ↓", "input history — previous / next message"],
|
|
2689
2907
|
["wheel", "scroll the conversation"],
|
|
2908
|
+
["ctrl+↓", "jump to the bottom of the chat (or click the ↓ button)"],
|
|
2909
|
+
["enter while running", "queue the message · /btw asks without interrupting"],
|
|
2690
2910
|
["ctrl+p/ctrl+n", "input history (same as ↑ ↓)"],
|
|
2691
2911
|
["tab / badge", "switch plan/build · click the header badge · complete commands"],
|
|
2692
2912
|
["ctrl+z", "undo the last message (works while thinking)"],
|
|
@@ -2704,14 +2924,31 @@ class TuiApp {
|
|
|
2704
2924
|
}
|
|
2705
2925
|
this.append(lines);
|
|
2706
2926
|
}
|
|
2927
|
+
appendUserBlock(input, imageCount, extra = []) {
|
|
2928
|
+
const lines = this.userBlockLines(input, imageCount);
|
|
2929
|
+
const last = this.transcript[this.transcript.length - 1];
|
|
2930
|
+
const leading = this.transcript.length > 0 && last !== "" ? [""] : [];
|
|
2931
|
+
this.append([...leading, ...lines, ...extra, ""]);
|
|
2932
|
+
return Math.max(0, this.transcript.length - (extra.length + 1) - lines.length);
|
|
2933
|
+
}
|
|
2707
2934
|
userBlockLines(input, imageCount = 0) {
|
|
2935
|
+
const width = Math.max(20, this.contentWidth() - 2);
|
|
2708
2936
|
const lines = input.split("\n");
|
|
2709
|
-
const
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2937
|
+
const out = [];
|
|
2938
|
+
for (let index = 0; index < lines.length; index++) {
|
|
2939
|
+
const line = lines[index];
|
|
2940
|
+
if (line === "") {
|
|
2941
|
+
out.push("");
|
|
2942
|
+
continue;
|
|
2943
|
+
}
|
|
2944
|
+
const prefix = index === 0 ? `${tone.accent(glyphs.prompt)} ` : " ";
|
|
2945
|
+
const kind = index === 0 ? "bold" : "plain";
|
|
2946
|
+
out.push(...wrapSegments([{ text: line, kind }], prefix, " ", width));
|
|
2947
|
+
}
|
|
2948
|
+
if (imageCount > 0) {
|
|
2949
|
+
out.push(` ${tone.mist(`[${imageCount} image${imageCount === 1 ? "" : "s"} attached]`)}`);
|
|
2950
|
+
}
|
|
2951
|
+
return out;
|
|
2715
2952
|
}
|
|
2716
2953
|
async saveTurn(input, result, durationMs) {
|
|
2717
2954
|
const cost = estimateCost(result.usage, this.model, this.providerId);
|