zames_pro 2.50.0 → 2.50.2

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.
@@ -9,11 +9,20 @@
9
9
  // types): the caller reads and writes values through callbacks. This makes the
10
10
  // menu easy to test and reuse.
11
11
  import { theme } from './theme.js';
12
+ import { visLen } from './input.js';
12
13
  const ESC = String.fromCharCode(27);
13
14
  // Strip ANSI sequences to compute the visible length of a line.
14
15
  function stripAnsi(s) {
15
16
  return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
16
17
  }
18
+ // Visible COLUMNS of a line (ANSI removed, wide chars counted as 2). Using the
19
+ // code-point count made the block height disagree with the real screen on lines
20
+ // with emoji/wide chars, so the erase moved to the wrong row and the config list
21
+ // duplicated. A row that fills the terminal exactly also triggers autowrap on
22
+ // some terminals, so we keep one column free here as the editor does.
23
+ function visCols(s) {
24
+ return visLen(stripAnsi(s));
25
+ }
17
26
  function displayValue(field, value, t) {
18
27
  if (value === undefined)
19
28
  return t('cfg.menu.default');
@@ -90,10 +99,17 @@ export function runConfigMenu(opts) {
90
99
  // Erase the previous block and print the new one. We count VISUAL lines
91
100
  // (accounting for wrapping by terminal width), otherwise on narrow
92
101
  // terminals you get "garbage" from unerased tails.
102
+ // Reserve the last column. A line whose visible width reaches exactly the
103
+ // terminal width hits the autowrap edge: depending on the terminal it
104
+ // either consumes two rows for the following `\n` or keeps a pending wrap,
105
+ // and in both cases a full-width divisor UNDERCOUNTS the block height — the
106
+ // erase then misses the tail and the config lines DUPLICATE on screen (the
107
+ // reported bug). Overcounting by one is a harmless blank row; undercounting
108
+ // is not, so we bias to the safe side here.
93
109
  const cols = output.columns || 80;
110
+ const usable = Math.max(1, cols - 1);
94
111
  const rowsOf = (s) => {
95
- const len = stripAnsi(s).length;
96
- return Math.max(1, Math.ceil(len / Math.max(1, cols)));
112
+ return Math.max(1, Math.ceil(visCols(s) / usable));
97
113
  };
98
114
  if (lastRows > 0) {
99
115
  output.write(ESC + '[' + lastRows + 'A');
package/dist/input.js CHANGED
@@ -1,4 +1,4 @@
1
- import { theme } from './theme.js';
1
+ import { theme, divider } from './theme.js';
2
2
  import { renderMarkdown } from './markdown.js';
3
3
  import { randomThinkingPhrase, stripEllipsis, DOTS, renderDots, } from './spinner.js';
4
4
  import { translate } from './i18n.js';
@@ -157,7 +157,16 @@ export function charWidth(cp) {
157
157
  return 1;
158
158
  }
159
159
  export function layoutInput(promptStr, buf, cursor, cols) {
160
- const width = Math.max(20, Number(cols) || 80);
160
+ // Reserve the LAST column. If a printed row reaches exactly `cols` columns,
161
+ // terminals with autowrap (Tabby, iTerm, Windows Terminal, xterm's default
162
+ // deferred wrap) either wrap the cursor to the next line or leave it in the
163
+ // last cell with a "pending wrap". In both cases the editor's row count
164
+ // disagrees with the real screen by one, so the cursor/erase arithmetic in
165
+ // `_renderInputOnly`/`_writeBlock` walks one row too far and the input line
166
+ // climbs. This was most visible in an EMPTY chat with a single long,
167
+ // space-less line (which fills the row exactly) and when moving the cursor
168
+ // with the arrow keys.
169
+ const width = Math.max(2, (Number(cols) || 80) - 1);
161
170
  const promptW = visLen(promptStr);
162
171
  const pad = ' '.repeat(promptW);
163
172
  const chars = Array.from(String(buf));
@@ -306,9 +315,6 @@ export class LineEditor {
306
315
  // True while the dot-animation timer is running. Lets sendPause() update
307
316
  // only the base text (remaining seconds) without restarting the timer.
308
317
  _animating;
309
- // Whether the animated status shows the trailing "Esc — стоп" hint. Off
310
- // while a tool runs (its process cannot be cancelled).
311
- _showHint;
312
318
  _dotPhase;
313
319
  _thinkBase;
314
320
  // Explicit lifecycle state of the send ('generating' | 'paused' |
@@ -324,6 +330,13 @@ export class LineEditor {
324
330
  // keeping the status untouched removes that flicker. Cleared whenever the
325
331
  // status genuinely changes so the cache can never go stale.
326
332
  _lastStatusBlock;
333
+ // Rows occupied by the WHOLE block (status + input + hints) at the last
334
+ // write. When the block SHRINKS (the pause status disappears, the input
335
+ // unwraps) the erase leaves the cursor at the old block top; printing a
336
+ // shorter block then ends above the screen bottom, so the footer floated with
337
+ // blank rows below it. Padding the redraw with (prev - new) newlines pushes
338
+ // it back down to the bottom (see _padShrink).
339
+ _blockRows;
327
340
  _wasRaw;
328
341
  _onData;
329
342
  // On SIGWINCH we redraw the whole block so the layout follows the new
@@ -406,11 +419,11 @@ export class LineEditor {
406
419
  this._inPaste = false;
407
420
  this._dotTimer = null;
408
421
  this._animating = false;
409
- this._showHint = true;
410
422
  this._dotPhase = 0;
411
423
  this._thinkBase = '';
412
424
  this._sendState = '';
413
425
  this._lastStatusBlock = '';
426
+ this._blockRows = 0;
414
427
  this._wasRaw = false;
415
428
  this._onData = (b) => this._handle(b);
416
429
  this._resizeTimer = null;
@@ -665,8 +678,20 @@ export class LineEditor {
665
678
  stdin.resume();
666
679
  process.stdout.write(ESC + '[?2004h');
667
680
  stdin.on('data', this._onData);
681
+ // `pause()` erased our block and handed the terminal to a foreign UI (the
682
+ // /config menu), which may leave the cursor ANYWHERE — after Esc the menu
683
+ // clears its block and parks the cursor at the TOP of the screen. Without
684
+ // re-pinning, the block would be redrawn up there and every later repaint
685
+ // would keep it glued to the top. Re-fill the viewport and forget the
686
+ // stale block height so the editor lands at the bottom again.
687
+ this._lastStatusBlock = '';
688
+ this._blockRows = 0;
689
+ // Start from a KNOWN row: the foreign UI may have parked the cursor
690
+ // anywhere (the /config menu leaves it near the top). From home, padding
691
+ // lands exactly on the last screen row.
692
+ process.stdout.write(ESC + '[H');
693
+ this._padToBottom();
668
694
  this._writeBlock();
669
- this._render();
670
695
  }
671
696
  // Lock input while a long operation runs (chat resume/open, /new,
672
697
  // self-review). The line stays visible but text input and Enter are ignored,
@@ -865,6 +890,12 @@ export class LineEditor {
865
890
  NL +
866
891
  theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
867
892
  }
893
+ // The input may have UNWRAPPED (fewer rows than before) — push the block
894
+ // back to the bottom BEFORE the new content is printed (the padding is
895
+ // plain newlines at the top of the block, which scroll the screen).
896
+ const blockRowsOnly = top + layOnly.rows.length + linesBelowOnly;
897
+ this._padShrink(blockRowsOnly, this._blockRows);
898
+ this._blockRows = blockRowsOnly;
868
899
  process.stdout.write(outOnly);
869
900
  const upOnly = layOnly.rows.length - 1 - layOnly.cursorRow + linesBelowOnly;
870
901
  if (upOnly > 0)
@@ -899,13 +930,20 @@ export class LineEditor {
899
930
  NL +
900
931
  theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
901
932
  }
933
+ const linesBelow = shown.length
934
+ ? shown.length + (sugg.length > shown.length ? 1 : 0)
935
+ : 0;
936
+ // If the block SHRANK (the pause status went away, the input unwrapped),
937
+ // the erase left the cursor at the old block top; printing a shorter block
938
+ // would end above the screen bottom and the footer would float with blank
939
+ // rows below it. Padding with the difference pushes it back to the bottom.
940
+ const blockRows = top + lay.rows.length + linesBelow;
941
+ this._padShrink(blockRows, this._blockRows);
942
+ this._blockRows = blockRows;
902
943
  process.stdout.write(out);
903
944
  const lastRow = lay.rows.length - 1;
904
945
  // Move the cursor up: first to the input line within lay, then further by
905
946
  // the hint lines (if any) — the cursor must sit on the input.
906
- const linesBelow = shown.length
907
- ? shown.length + (sugg.length > shown.length ? 1 : 0)
908
- : 0;
909
947
  const up = lastRow - lay.cursorRow + linesBelow;
910
948
  if (up > 0)
911
949
  process.stdout.write(ESC + '[' + up + 'A');
@@ -915,6 +953,19 @@ export class LineEditor {
915
953
  this.rendered = true;
916
954
  this.cursorRowFromTop = top + lay.cursorRow;
917
955
  }
956
+ // Push the redrawn block back down to the bottom when it got SHORTER than
957
+ // the previous one. `_eraseBlock`/`_eraseInputOnly` put the cursor at the old
958
+ // block top, so emitting (prev - next) newlines scrolls the screen just
959
+ // enough for the new, shorter block to end at the screen bottom again. See
960
+ // _blockRows.
961
+ _padShrink(nextRows, prevRows) {
962
+ if (prevRows <= nextRows)
963
+ return;
964
+ const rows = process.stdout.rows || 24;
965
+ const pad = Math.min(prevRows - nextRows, Math.max(0, rows - 1));
966
+ if (pad > 0)
967
+ process.stdout.write(NL.repeat(pad));
968
+ }
918
969
  // Public repaint hook: called when an external state that the status line
919
970
  // reads (e.g. the toggle icons) changed, so it can be redrawn immediately.
920
971
  refreshStatus() {
@@ -933,6 +984,10 @@ export class LineEditor {
933
984
  process.stdout.write(ESC + '[2J' + ESC + '[H');
934
985
  this.rendered = false;
935
986
  this._lastStatusBlock = '';
987
+ // We are about to re-pin the block to the bottom ourselves, so forget the
988
+ // previous block height — otherwise the shrink padding would add extra
989
+ // rows on top of the padding below.
990
+ this._blockRows = 0;
936
991
  // Re-pin the block to the bottom after the screen was cleared.
937
992
  this._padToBottom();
938
993
  this._writeBlock();
@@ -973,6 +1028,13 @@ export class LineEditor {
973
1028
  NL +
974
1029
  theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
975
1030
  }
1031
+ // A buffer change can UNWRAP the input (backspace on a wrapped line), so
1032
+ // the block can get shorter here too — push it back to the bottom BEFORE
1033
+ // the new content is printed (the padding is plain newlines at the top of
1034
+ // the block, which scroll the screen).
1035
+ const blockRows = this._statusTop + lay.rows.length + linesBelow;
1036
+ this._padShrink(blockRows, this._blockRows);
1037
+ this._blockRows = blockRows;
976
1038
  process.stdout.write(out);
977
1039
  const up = lay.rows.length - 1 - lay.cursorRow + linesBelow;
978
1040
  if (up > 0)
@@ -1005,10 +1067,10 @@ export class LineEditor {
1005
1067
  _dots(n) {
1006
1068
  return renderDots(n);
1007
1069
  }
1008
- // Status-line hint ("Esc — stop"), localized.
1070
+ // Status-line hint ("Esc — stop"), localized. Shown for every animated
1071
+ // state: Esc really aborts both a generation and a running tool (the Bash
1072
+ // child is killed via AbortSignal), so the promise is accurate.
1009
1073
  _hint() {
1010
- if (!this._showHint)
1011
- return '';
1012
1074
  return theme.dim(' · ' + translate(this.locale)('spinner.hint'));
1013
1075
  }
1014
1076
  // The lifecycle phase prefix for the status line, colored by meaning:
@@ -1044,13 +1106,8 @@ export class LineEditor {
1044
1106
  // Shared by the thinking spinner and the send-pause indicator, so the pause
1045
1107
  // is animated too (previously the pause was a static console line and the
1046
1108
  // dots stayed frozen).
1047
- //
1048
- // `showHint` (default true) controls the trailing "· Esc — стоп". While a
1049
- // TOOL runs we hide it: the tool's child process cannot be killed, so
1050
- // promising "Esc to stop" would be misleading.
1051
- _startAnimated(baseText, showHint = true) {
1109
+ _startAnimated(baseText) {
1052
1110
  this._thinkBase = theme.brown(stripEllipsis(baseText));
1053
- this._showHint = showHint;
1054
1111
  this._dotPhase = 0;
1055
1112
  this.setStatus(this._thinkBase + this._dots(0) + this._hint());
1056
1113
  this._stopDots();
@@ -1128,7 +1185,7 @@ export class LineEditor {
1128
1185
  NL +
1129
1186
  rendered +
1130
1187
  NL +
1131
- theme.dim('─'.repeat(60)));
1188
+ theme.dim(divider()));
1132
1189
  }
1133
1190
  // Warning to the operator (e.g. the agent stopped suspiciously).
1134
1191
  // Printed above the input line without overwriting it.
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs/promises';
3
- import { theme } from './theme.js';
3
+ import { theme, divider } from './theme.js';
4
4
  import { translate, DEFAULT_LOCALE } from './i18n.js';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { ZAMES_HOME } from './config.js';
@@ -116,7 +116,7 @@ export async function selfReview({ browser, focus, transcript, locale, }) {
116
116
  await fs.writeFile(reportPath, finalMessage || t('self.report_empty'), 'utf-8');
117
117
  // Compute what changed
118
118
  const changed = await diffFiles(SRC_DIR, snapDir);
119
- console.log(theme.system('─'.repeat(60)));
119
+ console.log(theme.system(divider()));
120
120
  console.log(theme.system(t('self.report_path', { v: reportPath })));
121
121
  console.log(theme.system(t('self.snapshot_path', { v: snapDir })));
122
122
  if (changed.length) {
package/dist/spinner.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import ora from 'ora';
2
- import { theme } from './theme.js';
2
+ import { theme, divider } from './theme.js';
3
3
  import { renderMarkdown } from './markdown.js';
4
4
  import { translate } from './i18n.js';
5
5
  // Phrases for the "agent is working" spinner. Chosen in random order.
@@ -59,14 +59,9 @@ export function createSpinner(locale = 'ru') {
59
59
  // "running" dot sequence. Shared by the thinking spinner and the send-pause
60
60
  // indicator, so the pause is animated too (before, it was a static line and
61
61
  // the dots did not move — the operator saw a frozen spinner).
62
- //
63
- // `showHint` (default true) controls the trailing "· Esc — стоп". While a
64
- // tool runs we hide it: its process cannot be cancelled, so promising a stop
65
- // would be misleading.
66
- const startAnimated = (baseText, showHint = true) => {
62
+ const startAnimated = (baseText) => {
67
63
  animBase = theme.brown(stripEllipsis(baseText));
68
- const hint = showHint ? HINT : '';
69
- start(animBase + renderDots(0) + hint);
64
+ start(animBase + renderDots(0) + HINT);
70
65
  dotPhase = 0;
71
66
  if (dotTimer)
72
67
  clearInterval(dotTimer);
@@ -74,7 +69,7 @@ export function createSpinner(locale = 'ru') {
74
69
  if (!spinner)
75
70
  return;
76
71
  dotPhase = (dotPhase + 1) % DOTS.length;
77
- spinner.text = animBase + renderDots(dotPhase) + hint;
72
+ spinner.text = animBase + renderDots(dotPhase) + HINT;
78
73
  }, 400);
79
74
  animating = true;
80
75
  if (dotTimer.unref)
@@ -158,7 +153,7 @@ export function createSpinner(locale = 'ru') {
158
153
  // (which is highlighted by the prompt with a golden arrow).
159
154
  console.log(NL + theme.assistant(translate(locale)('editor.answer')) + NL);
160
155
  console.log(rendered);
161
- console.log(theme.dim('─'.repeat(60)) + NL);
156
+ console.log(theme.dim(divider()) + NL);
162
157
  },
163
158
  warning: (msg) => {
164
159
  stop();
package/dist/theme.js CHANGED
@@ -29,7 +29,16 @@ export const theme = {
29
29
  success: chalk.hex('#a9c08c'), // soft sage
30
30
  brown: chalk.hex('#a1723f'), // brown (status/spinner)
31
31
  toggleOn: chalk.hex('#6fd0b0'), // teal-green — a toggle is ON
32
- toggleOff: chalk.hex('#4a4f5a'), // dim gray — a toggle is OFF
33
32
  bold: chalk.bold,
34
33
  };
34
+ // Horizontal rule that separates the model's answer from the next block. It
35
+ // spans the terminal width (capped at `max`) so it does not look stubby on a
36
+ // wide terminal. Falls back to 60 columns when the width is unknown (non-TTY).
37
+ // One column is kept free, like everywhere else in the status/input layout, so
38
+ // a full-width rule cannot trigger autowrap.
39
+ export function divider(max = 100) {
40
+ const cols = process.stdout.columns || 0;
41
+ const width = Math.max(20, Math.min(max, cols ? cols - 1 : 60));
42
+ return '─'.repeat(width);
43
+ }
35
44
  export default theme;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.50.0",
3
+ "version": "2.50.2",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {