@quatcode/agent 0.2.5 → 0.2.6

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/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";
@@ -136,6 +136,9 @@ class TuiApp {
136
136
  modeBadgeWidth = 0;
137
137
  modeBadgeHover = false;
138
138
  versionNotice = null;
139
+ bottomHintStart = -1;
140
+ bottomHintWidth = 0;
141
+ bottomHintRow = -1;
139
142
  constructor(options, input, output) {
140
143
  this.opts = options;
141
144
  this.dir = path.resolve(options.dir);
@@ -433,6 +436,11 @@ class TuiApp {
433
436
  this.selection = null;
434
437
  this.scheduleRender();
435
438
  }
439
+ if (key.type === "nav" && key.dir === "down" && key.ctrl) {
440
+ this.scrollOffset = 0;
441
+ this.scheduleRender();
442
+ continue;
443
+ }
436
444
  if (this.mode === "editing")
437
445
  this.handleEditKey(key);
438
446
  else if (this.mode === "running")
@@ -735,16 +743,38 @@ class TuiApp {
735
743
  if (!rendered)
736
744
  return false;
737
745
  const top = this.lastHeaderRows + this.lastViewport + this.lastInputOffset;
738
- const lineIndex = row - top;
739
- if (lineIndex < 0 || lineIndex >= rendered.lines.length)
746
+ const rowIndex = row - top;
747
+ if (rowIndex < 0 || rowIndex >= rendered.lines.length)
748
+ return false;
749
+ const visual = rendered.rows[rowIndex];
750
+ if (!visual)
740
751
  return false;
741
- const prefixWidth = rendered.prefixWidths[lineIndex] ?? 0;
742
- const textCol = col - this.padX - prefixWidth;
743
- const start = rendered.lineStarts[lineIndex] ?? 0;
744
- this.buffer.setCursorFromDisplay(lineIndex, Math.max(0, textCol), start);
752
+ const textCol = col - this.padX - visual.prefixWidth;
753
+ this.buffer.setCursorFromDisplay(visual.line, Math.max(0, textCol), visual.start);
745
754
  this.scheduleRender();
746
755
  return true;
747
756
  }
757
+ bottomHintHit(row, col) {
758
+ if (this.bottomHintRow < 0 || this.bottomHintStart < 0)
759
+ return false;
760
+ if (row !== this.bottomHintRow)
761
+ return false;
762
+ const start = this.padX + this.bottomHintStart;
763
+ return col >= start && col < start + this.bottomHintWidth;
764
+ }
765
+ bottomRuleLine() {
766
+ const width = this.contentWidth();
767
+ const label = `${glyphs.arrowDown} bottom · ctrl+${glyphs.arrowDown}`;
768
+ this.bottomHintStart = -1;
769
+ this.bottomHintWidth = 0;
770
+ if (this.scrollOffset > 0 && this.mode !== "picker" && width > 36) {
771
+ const dashes = Math.max(8, width - 2 - stringWidth(label) - 2);
772
+ this.bottomHintStart = 2 + dashes + 2;
773
+ this.bottomHintWidth = stringWidth(label);
774
+ return ` ${tone.mist("─".repeat(dashes))} ${tone.silver(label)}`;
775
+ }
776
+ return ` ${tone.mist("─".repeat(Math.max(8, width - 2)))}`;
777
+ }
748
778
  handleHover(row, col) {
749
779
  if (row === 1) {
750
780
  const hover = this.badgeHit(col);
@@ -791,6 +821,11 @@ class TuiApp {
791
821
  this.toggleMode();
792
822
  return;
793
823
  }
824
+ if (this.bottomHintHit(row, col)) {
825
+ this.scrollOffset = 0;
826
+ this.scheduleRender();
827
+ return;
828
+ }
794
829
  if (this.mode === "permission") {
795
830
  const index = this.permissionOptionAt(row, col);
796
831
  if (index !== null) {
@@ -1036,6 +1071,8 @@ class TuiApp {
1036
1071
  const viewport = Math.max(3, rows - header.length - footer.length - this.padTop - this.padBottom);
1037
1072
  this.lastHeaderRows = header.length + this.padTop;
1038
1073
  this.lastViewport = viewport;
1074
+ this.bottomHintRow =
1075
+ this.bottomHintStart >= 0 ? this.lastHeaderRows + viewport + footer.length - 1 : -1;
1039
1076
  let content;
1040
1077
  if (this.mode === "picker" && this.picker) {
1041
1078
  content = this.picker.render(this.contentWidth() - 2, viewport);
@@ -1116,6 +1153,7 @@ class TuiApp {
1116
1153
  footerLines() {
1117
1154
  const width = this.contentWidth();
1118
1155
  const rule = ` ${tone.mist("─".repeat(Math.max(8, width - 2)))}`;
1156
+ const bottom = this.bottomRuleLine();
1119
1157
  if (this.mode === "editing") {
1120
1158
  const rendered = this.buffer.render({
1121
1159
  width,
@@ -1124,23 +1162,35 @@ class TuiApp {
1124
1162
  mask: this.inputMask,
1125
1163
  style: styleInputLine,
1126
1164
  });
1127
- this.lastInputRender = rendered;
1165
+ const maxRows = Math.max(3, Math.floor(this.screen.rows / 2));
1166
+ if (rendered.lines.length > maxRows) {
1167
+ const start = Math.max(0, Math.min(rendered.cursorRow - maxRows + 1, rendered.lines.length - maxRows));
1168
+ this.lastInputRender = {
1169
+ lines: rendered.lines.slice(start, start + maxRows),
1170
+ rows: rendered.rows.slice(start, start + maxRows),
1171
+ cursorRow: rendered.cursorRow - start,
1172
+ cursorCol: rendered.cursorCol,
1173
+ };
1174
+ }
1175
+ else {
1176
+ this.lastInputRender = rendered;
1177
+ }
1128
1178
  const suggestions = this.suggestions();
1129
1179
  this.lastInputOffset = 1 + suggestions.length;
1130
- return [rule, ...suggestions, ...rendered.lines, rule];
1180
+ return [rule, ...suggestions, ...this.lastInputRender.lines, bottom];
1131
1181
  }
1132
1182
  this.lastInputRender = null;
1133
1183
  if (this.status)
1134
- return [rule, this.statusLine(), rule];
1184
+ return [rule, this.statusLine(), bottom];
1135
1185
  if (this.mode === "running") {
1136
- return [rule, ` ${tone.mist("// esc interrupts")}`, rule];
1186
+ return [rule, ` ${tone.mist("// esc interrupts")}`, bottom];
1137
1187
  }
1138
1188
  if (this.mode === "permission")
1139
- return [rule, ` ${this.permissionHint}`, rule];
1189
+ return [rule, ` ${this.permissionHint}`, bottom];
1140
1190
  if (this.mode === "picker" && this.picker) {
1141
- return [rule, this.picker.footer(this.picker.itemCount()), rule];
1191
+ return [rule, this.picker.footer(this.picker.itemCount()), bottom];
1142
1192
  }
1143
- return [rule, "", rule];
1193
+ return [rule, "", bottom];
1144
1194
  }
1145
1195
  statusLine() {
1146
1196
  const spin = tone.accent(glyphs.spinner[this.spinnerFrame % glyphs.spinner.length] ?? "-");
@@ -2687,6 +2737,7 @@ class TuiApp {
2687
2737
  ["sessions picker", "ctrl+r rename · ctrl+p pin · ctrl+d delete"],
2688
2738
  ["↑ ↓", "input history — previous / next message"],
2689
2739
  ["wheel", "scroll the conversation"],
2740
+ ["ctrl+↓", "jump to the bottom of the chat (or click the ↓ button)"],
2690
2741
  ["ctrl+p/ctrl+n", "input history (same as ↑ ↓)"],
2691
2742
  ["tab / badge", "switch plan/build · click the header badge · complete commands"],
2692
2743
  ["ctrl+z", "undo the last message (works while thinking)"],
@@ -2705,13 +2756,23 @@ class TuiApp {
2705
2756
  this.append(lines);
2706
2757
  }
2707
2758
  userBlockLines(input, imageCount = 0) {
2759
+ const width = Math.max(20, this.contentWidth() - 2);
2708
2760
  const lines = input.split("\n");
2709
- const head = ` ${tone.accent(glyphs.prompt)} ${bold(tone.bone(lines[0] ?? ""))}`;
2710
- const rest = lines.slice(1).map((line) => ` ${tone.bone(line)}`);
2711
- const images = imageCount > 0
2712
- ? [` ${tone.mist(`[${imageCount} image${imageCount === 1 ? "" : "s"} attached]`)}`]
2713
- : [];
2714
- return [head, ...rest, ...images];
2761
+ const out = [];
2762
+ for (let index = 0; index < lines.length; index++) {
2763
+ const line = lines[index];
2764
+ if (line === "") {
2765
+ out.push("");
2766
+ continue;
2767
+ }
2768
+ const prefix = index === 0 ? ` ${tone.accent(glyphs.prompt)} ` : " ";
2769
+ const kind = index === 0 ? "bold" : "plain";
2770
+ out.push(...wrapSegments([{ text: line, kind }], prefix, " ", width));
2771
+ }
2772
+ if (imageCount > 0) {
2773
+ out.push(` ${tone.mist(`[${imageCount} image${imageCount === 1 ? "" : "s"} attached]`)}`);
2774
+ }
2775
+ return out;
2715
2776
  }
2716
2777
  async saveTurn(input, result, durationMs) {
2717
2778
  const cost = estimateCost(result.usage, this.model, this.providerId);