zames_pro 2.50.0 → 2.50.1

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');
@@ -91,9 +100,11 @@ export function runConfigMenu(opts) {
91
100
  // (accounting for wrapping by terminal width), otherwise on narrow
92
101
  // terminals you get "garbage" from unerased tails.
93
102
  const cols = output.columns || 80;
103
+ // Reserve the last column like the editor does: a row that fills the
104
+ // terminal exactly can autowrap, which desyncs the block height.
105
+ const usable = Math.max(1, cols - 1);
94
106
  const rowsOf = (s) => {
95
- const len = stripAnsi(s).length;
96
- return Math.max(1, Math.ceil(len / Math.max(1, cols)));
107
+ return Math.max(1, Math.ceil(visCols(s) / usable));
97
108
  };
98
109
  if (lastRows > 0) {
99
110
  output.write(ESC + '[' + lastRows + 'A');
package/dist/input.js CHANGED
@@ -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));
@@ -324,6 +333,13 @@ export class LineEditor {
324
333
  // keeping the status untouched removes that flicker. Cleared whenever the
325
334
  // status genuinely changes so the cache can never go stale.
326
335
  _lastStatusBlock;
336
+ // Rows occupied by the WHOLE block (status + input + hints) at the last
337
+ // write. When the block SHRINKS (the pause status disappears, the input
338
+ // unwraps) the erase leaves the cursor at the old block top; printing a
339
+ // shorter block then ends above the screen bottom, so the footer floated with
340
+ // blank rows below it. Padding the redraw with (prev - new) newlines pushes
341
+ // it back down to the bottom (see _padShrink).
342
+ _blockRows;
327
343
  _wasRaw;
328
344
  _onData;
329
345
  // On SIGWINCH we redraw the whole block so the layout follows the new
@@ -411,6 +427,7 @@ export class LineEditor {
411
427
  this._thinkBase = '';
412
428
  this._sendState = '';
413
429
  this._lastStatusBlock = '';
430
+ this._blockRows = 0;
414
431
  this._wasRaw = false;
415
432
  this._onData = (b) => this._handle(b);
416
433
  this._resizeTimer = null;
@@ -665,8 +682,20 @@ export class LineEditor {
665
682
  stdin.resume();
666
683
  process.stdout.write(ESC + '[?2004h');
667
684
  stdin.on('data', this._onData);
685
+ // `pause()` erased our block and handed the terminal to a foreign UI (the
686
+ // /config menu), which may leave the cursor ANYWHERE — after Esc the menu
687
+ // clears its block and parks the cursor at the TOP of the screen. Without
688
+ // re-pinning, the block would be redrawn up there and every later repaint
689
+ // would keep it glued to the top. Re-fill the viewport and forget the
690
+ // stale block height so the editor lands at the bottom again.
691
+ this._lastStatusBlock = '';
692
+ this._blockRows = 0;
693
+ // Start from a KNOWN row: the foreign UI may have parked the cursor
694
+ // anywhere (the /config menu leaves it near the top). From home, padding
695
+ // lands exactly on the last screen row.
696
+ process.stdout.write(ESC + '[H');
697
+ this._padToBottom();
668
698
  this._writeBlock();
669
- this._render();
670
699
  }
671
700
  // Lock input while a long operation runs (chat resume/open, /new,
672
701
  // self-review). The line stays visible but text input and Enter are ignored,
@@ -865,6 +894,12 @@ export class LineEditor {
865
894
  NL +
866
895
  theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
867
896
  }
897
+ // The input may have UNWRAPPED (fewer rows than before) — push the block
898
+ // back to the bottom BEFORE the new content is printed (the padding is
899
+ // plain newlines at the top of the block, which scroll the screen).
900
+ const blockRowsOnly = top + layOnly.rows.length + linesBelowOnly;
901
+ this._padShrink(blockRowsOnly, this._blockRows);
902
+ this._blockRows = blockRowsOnly;
868
903
  process.stdout.write(outOnly);
869
904
  const upOnly = layOnly.rows.length - 1 - layOnly.cursorRow + linesBelowOnly;
870
905
  if (upOnly > 0)
@@ -899,13 +934,20 @@ export class LineEditor {
899
934
  NL +
900
935
  theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
901
936
  }
937
+ const linesBelow = shown.length
938
+ ? shown.length + (sugg.length > shown.length ? 1 : 0)
939
+ : 0;
940
+ // If the block SHRANK (the pause status went away, the input unwrapped),
941
+ // the erase left the cursor at the old block top; printing a shorter block
942
+ // would end above the screen bottom and the footer would float with blank
943
+ // rows below it. Padding with the difference pushes it back to the bottom.
944
+ const blockRows = top + lay.rows.length + linesBelow;
945
+ this._padShrink(blockRows, this._blockRows);
946
+ this._blockRows = blockRows;
902
947
  process.stdout.write(out);
903
948
  const lastRow = lay.rows.length - 1;
904
949
  // Move the cursor up: first to the input line within lay, then further by
905
950
  // 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
951
  const up = lastRow - lay.cursorRow + linesBelow;
910
952
  if (up > 0)
911
953
  process.stdout.write(ESC + '[' + up + 'A');
@@ -915,6 +957,19 @@ export class LineEditor {
915
957
  this.rendered = true;
916
958
  this.cursorRowFromTop = top + lay.cursorRow;
917
959
  }
960
+ // Push the redrawn block back down to the bottom when it got SHORTER than
961
+ // the previous one. `_eraseBlock`/`_eraseInputOnly` put the cursor at the old
962
+ // block top, so emitting (prev - next) newlines scrolls the screen just
963
+ // enough for the new, shorter block to end at the screen bottom again. See
964
+ // _blockRows.
965
+ _padShrink(nextRows, prevRows) {
966
+ if (prevRows <= nextRows)
967
+ return;
968
+ const rows = process.stdout.rows || 24;
969
+ const pad = Math.min(prevRows - nextRows, Math.max(0, rows - 1));
970
+ if (pad > 0)
971
+ process.stdout.write(NL.repeat(pad));
972
+ }
918
973
  // Public repaint hook: called when an external state that the status line
919
974
  // reads (e.g. the toggle icons) changed, so it can be redrawn immediately.
920
975
  refreshStatus() {
@@ -933,6 +988,10 @@ export class LineEditor {
933
988
  process.stdout.write(ESC + '[2J' + ESC + '[H');
934
989
  this.rendered = false;
935
990
  this._lastStatusBlock = '';
991
+ // We are about to re-pin the block to the bottom ourselves, so forget the
992
+ // previous block height — otherwise the shrink padding would add extra
993
+ // rows on top of the padding below.
994
+ this._blockRows = 0;
936
995
  // Re-pin the block to the bottom after the screen was cleared.
937
996
  this._padToBottom();
938
997
  this._writeBlock();
@@ -973,6 +1032,13 @@ export class LineEditor {
973
1032
  NL +
974
1033
  theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
975
1034
  }
1035
+ // A buffer change can UNWRAP the input (backspace on a wrapped line), so
1036
+ // the block can get shorter here too — push it back to the bottom BEFORE
1037
+ // the new content is printed (the padding is plain newlines at the top of
1038
+ // the block, which scroll the screen).
1039
+ const blockRows = this._statusTop + lay.rows.length + linesBelow;
1040
+ this._padShrink(blockRows, this._blockRows);
1041
+ this._blockRows = blockRows;
976
1042
  process.stdout.write(out);
977
1043
  const up = lay.rows.length - 1 - lay.cursorRow + linesBelow;
978
1044
  if (up > 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.50.0",
3
+ "version": "2.50.1",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {