@quatcode/agent 0.2.4 → 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);
@@ -1099,7 +1136,14 @@ class TuiApp {
1099
1136
  this.modeBadgeWidth = stringWidth(badge);
1100
1137
  const left = `${before} ${badge}`;
1101
1138
  const scrolled = this.scrollOffset > 0 ? tone.mist("↑ ") : "";
1102
- const right = `${scrolled}${tone.mist(`v${VERSION}`)}`;
1139
+ const versionText = tone.mist(`v${VERSION}`);
1140
+ const dirLabel = shortenHome(this.dir);
1141
+ const reserve = stringWidth(left) + stringWidth(versionText) + stringWidth(scrolled) + 5;
1142
+ const maxDir = Math.max(12, Math.min(48, width - reserve));
1143
+ const dirText = stringWidth(dirLabel) > maxDir
1144
+ ? `…${dirLabel.slice(-(maxDir - 1))}`
1145
+ : dirLabel;
1146
+ const right = `${scrolled}${tone.mist(dirText)} ${versionText}`;
1103
1147
  const gap = Math.max(1, width - 2 - stringWidth(left) - stringWidth(right));
1104
1148
  return [
1105
1149
  ` ${left}${" ".repeat(gap)}${right}`,
@@ -1109,6 +1153,7 @@ class TuiApp {
1109
1153
  footerLines() {
1110
1154
  const width = this.contentWidth();
1111
1155
  const rule = ` ${tone.mist("─".repeat(Math.max(8, width - 2)))}`;
1156
+ const bottom = this.bottomRuleLine();
1112
1157
  if (this.mode === "editing") {
1113
1158
  const rendered = this.buffer.render({
1114
1159
  width,
@@ -1117,23 +1162,35 @@ class TuiApp {
1117
1162
  mask: this.inputMask,
1118
1163
  style: styleInputLine,
1119
1164
  });
1120
- 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
+ }
1121
1178
  const suggestions = this.suggestions();
1122
1179
  this.lastInputOffset = 1 + suggestions.length;
1123
- return [rule, ...suggestions, ...rendered.lines, rule];
1180
+ return [rule, ...suggestions, ...this.lastInputRender.lines, bottom];
1124
1181
  }
1125
1182
  this.lastInputRender = null;
1126
1183
  if (this.status)
1127
- return [rule, this.statusLine(), rule];
1184
+ return [rule, this.statusLine(), bottom];
1128
1185
  if (this.mode === "running") {
1129
- return [rule, ` ${tone.mist("// esc interrupts")}`, rule];
1186
+ return [rule, ` ${tone.mist("// esc interrupts")}`, bottom];
1130
1187
  }
1131
1188
  if (this.mode === "permission")
1132
- return [rule, ` ${this.permissionHint}`, rule];
1189
+ return [rule, ` ${this.permissionHint}`, bottom];
1133
1190
  if (this.mode === "picker" && this.picker) {
1134
- return [rule, this.picker.footer(this.picker.itemCount()), rule];
1191
+ return [rule, this.picker.footer(this.picker.itemCount()), bottom];
1135
1192
  }
1136
- return [rule, ` ${tone.mist(`// ${shortenHome(this.dir)}`)}`, rule];
1193
+ return [rule, "", bottom];
1137
1194
  }
1138
1195
  statusLine() {
1139
1196
  const spin = tone.accent(glyphs.spinner[this.spinnerFrame % glyphs.spinner.length] ?? "-");
@@ -1293,7 +1350,7 @@ class TuiApp {
1293
1350
  `${summary.fileCount} files`,
1294
1351
  summary.packageManager ?? "",
1295
1352
  ].filter(Boolean);
1296
- return tone.mist(parts.join(" · "));
1353
+ return `${tone.mist(parts.join(" · "))} ${tone.silver(shortenHome(this.dir))}`;
1297
1354
  }
1298
1355
  buildSystem(query) {
1299
1356
  if (!this.opts.context || !this.summary)
@@ -2680,6 +2737,7 @@ class TuiApp {
2680
2737
  ["sessions picker", "ctrl+r rename · ctrl+p pin · ctrl+d delete"],
2681
2738
  ["↑ ↓", "input history — previous / next message"],
2682
2739
  ["wheel", "scroll the conversation"],
2740
+ ["ctrl+↓", "jump to the bottom of the chat (or click the ↓ button)"],
2683
2741
  ["ctrl+p/ctrl+n", "input history (same as ↑ ↓)"],
2684
2742
  ["tab / badge", "switch plan/build · click the header badge · complete commands"],
2685
2743
  ["ctrl+z", "undo the last message (works while thinking)"],
@@ -2698,13 +2756,23 @@ class TuiApp {
2698
2756
  this.append(lines);
2699
2757
  }
2700
2758
  userBlockLines(input, imageCount = 0) {
2759
+ const width = Math.max(20, this.contentWidth() - 2);
2701
2760
  const lines = input.split("\n");
2702
- const head = ` ${tone.accent(glyphs.prompt)} ${bold(tone.bone(lines[0] ?? ""))}`;
2703
- const rest = lines.slice(1).map((line) => ` ${tone.bone(line)}`);
2704
- const images = imageCount > 0
2705
- ? [` ${tone.mist(`[${imageCount} image${imageCount === 1 ? "" : "s"} attached]`)}`]
2706
- : [];
2707
- 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;
2708
2776
  }
2709
2777
  async saveTurn(input, result, durationMs) {
2710
2778
  const cost = estimateCost(result.usage, this.model, this.providerId);