zames_pro 2.49.1 → 2.49.3

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.
Files changed (2) hide show
  1. package/dist/input.js +83 -18
  2. package/package.json +1 -1
package/dist/input.js CHANGED
@@ -93,7 +93,12 @@ export function visRows(s, cols) {
93
93
  const len = visLen(s);
94
94
  return Math.max(1, Math.ceil(len / width));
95
95
  }
96
- // Visible length of a line without ANSI sequences.
96
+ // Visible length of a line without ANSI sequences. WIDE characters (emoji,
97
+ // CJK) count as 2 columns — a plain code-point count made the status-line
98
+ // arithmetic (right-align, wrapping, block erase) disagree with the real
99
+ // screen, so a status containing an emoji/CJK could wrap past the edge and
100
+ // make the block erase miss (status "stacking"). Combining marks and the
101
+ // zero-width joiner/space/variation selectors count as 0.
97
102
  export function visLen(s) {
98
103
  s = String(s);
99
104
  let n = 0;
@@ -109,11 +114,48 @@ export function visLen(s) {
109
114
  }
110
115
  continue;
111
116
  }
112
- n++;
113
- i++;
117
+ const cp = s.codePointAt(i);
118
+ n += charWidth(cp);
119
+ i += cp > 0xffff ? 2 : 1;
114
120
  }
115
121
  return n;
116
122
  }
123
+ // Display width of one code point in columns (0, 1 or 2). Based on the
124
+ // conventional wcwidth ranges — good enough for terminal layout, and it makes
125
+ // the emoji used in the status line (🧠🌐) count as 2, while math symbols like
126
+ // ⧗ (U+29D7) stay 1, matching a real terminal.
127
+ export function charWidth(cp) {
128
+ // Zero width: combining marks, ZWJ/ZWNJ, variation selectors.
129
+ if (cp === 0x200b ||
130
+ cp === 0x200c ||
131
+ cp === 0x200d ||
132
+ (cp >= 0x0300 && cp <= 0x036f) ||
133
+ (cp >= 0x1ab0 && cp <= 0x1aff) ||
134
+ (cp >= 0x1dc0 && cp <= 0x1dff) ||
135
+ (cp >= 0x20d0 && cp <= 0x20ff) ||
136
+ (cp >= 0xfe00 && cp <= 0xfe0f) ||
137
+ (cp >= 0xfe20 && cp <= 0xfe2f)) {
138
+ return 0;
139
+ }
140
+ // Wide: CJK, Hangul, fullwidth forms, emoji.
141
+ if ((cp >= 0x1100 && cp <= 0x115f) ||
142
+ (cp >= 0x2e80 && cp <= 0x303e) ||
143
+ (cp >= 0x3041 && cp <= 0x33ff) ||
144
+ (cp >= 0x3400 && cp <= 0x4dbf) ||
145
+ (cp >= 0x4e00 && cp <= 0x9fff) ||
146
+ (cp >= 0xa000 && cp <= 0xa4cf) ||
147
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
148
+ (cp >= 0xf900 && cp <= 0xfaff) ||
149
+ (cp >= 0xfe30 && cp <= 0xfe4f) ||
150
+ (cp >= 0xff00 && cp <= 0xff60) ||
151
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
152
+ (cp >= 0x1f000 && cp <= 0x1f02f) ||
153
+ (cp >= 0x1f300 && cp <= 0x1faff) ||
154
+ (cp >= 0x20000 && cp <= 0x3fffd)) {
155
+ return 2;
156
+ }
157
+ return 1;
158
+ }
117
159
  export function layoutInput(promptStr, buf, cursor, cols) {
118
160
  const width = Math.max(20, Number(cols) || 80);
119
161
  const promptW = visLen(promptStr);
@@ -128,21 +170,29 @@ export function layoutInput(promptStr, buf, cursor, cols) {
128
170
  const avail = Math.max(1, width - promptW);
129
171
  const start = i;
130
172
  // Build the next visual line. First we take as many
131
- // characters as fit (count < avail). If we hit the width
173
+ // characters as fit (columns < avail). If we hit the width
132
174
  // and the next character is not the end of the line, we roll back to the
133
175
  // last space so we don't split a word in the middle (word-wrap).
176
+ // Wrapping and the cursor column are measured in COLUMNS (charWidth),
177
+ // not code points — otherwise a CJK/emoji input would overflow the row
178
+ // and put the cursor on the wrong cell.
134
179
  let text = '';
135
- let count = 0;
136
- let lastSpace = -1; // index of the space in text (by Array.from)
137
- while (i < chars.length && chars[i] !== NL && count < avail) {
180
+ let widthUsed = 0;
181
+ let charIdx = 0;
182
+ let lastSpace = -1; // char index of the space within text
183
+ while (i < chars.length && chars[i] !== NL) {
184
+ const w = charWidth(chars[i].codePointAt(0));
185
+ if (widthUsed + w > avail)
186
+ break;
138
187
  if (chars[i] === ' ')
139
- lastSpace = count;
188
+ lastSpace = charIdx;
140
189
  text += chars[i];
141
190
  i++;
142
- count++;
191
+ charIdx++;
192
+ widthUsed += w;
143
193
  }
144
194
  const atLineEnd = i >= chars.length || chars[i] === NL;
145
- if (!atLineEnd && count >= avail && lastSpace > 0) {
195
+ if (!atLineEnd && widthUsed >= avail && lastSpace > 0) {
146
196
  // Move the "tail" of the line to the next visual line.
147
197
  const textChars = Array.from(text);
148
198
  const tailLen = textChars.length - lastSpace;
@@ -162,7 +212,8 @@ export function layoutInput(promptStr, buf, cursor, cols) {
162
212
  let cursorCol = promptW;
163
213
  for (let r = 0; r < rows.length; r++) {
164
214
  const row = rows[r];
165
- const len = Array.from(row.text).length;
215
+ const rowChars = Array.from(row.text);
216
+ const len = rowChars.length;
166
217
  const from = row.start;
167
218
  const to = row.start + len;
168
219
  if (cur >= from && cur <= to) {
@@ -170,7 +221,12 @@ export function layoutInput(promptStr, buf, cursor, cols) {
170
221
  if (cur === to && next && next.start === to)
171
222
  continue;
172
223
  cursorRow = r;
173
- cursorCol = promptW + (cur - from);
224
+ // Cursor column in COLUMNS (wide chars count as 2).
225
+ let w = 0;
226
+ for (let k = 0; k < cur - from; k++) {
227
+ w += charWidth(rowChars[k].codePointAt(0));
228
+ }
229
+ cursorCol = promptW + w;
174
230
  break;
175
231
  }
176
232
  }
@@ -683,11 +739,19 @@ export class LineEditor {
683
739
  // status stays on screen, only the input area is redrawn. This removes the
684
740
  // full-screen flicker on terminals like Tabby, where erasing the whole block
685
741
  // on every character made the display blink.
686
- _eraseInputOnly(inputRows) {
742
+ //
743
+ // `cursorRowInInput` is the cursor's row WITHIN the input area (0-based) as
744
+ // it is currently on screen: we move up exactly that many rows to the input's
745
+ // TOP and clear downward (ESC[J), which also wipes the hint lines below the
746
+ // input. Passing the input's full HEIGHT here (an earlier version did)
747
+ // overshot by `cursorRow` rows whenever the cursor was not on the last visual
748
+ // line, so every keystroke redrew the block one row higher — the input line
749
+ // visibly climbed from the bottom to the top of the screen.
750
+ _eraseInputOnly(cursorRowInInput) {
687
751
  if (!this.rendered)
688
752
  return;
689
753
  const rows = process.stdout.rows || 24;
690
- const up = Math.min(Math.max(0, inputRows - 1), Math.max(0, rows - 1));
754
+ const up = Math.min(Math.max(0, cursorRowInInput), Math.max(0, rows - 1));
691
755
  if (up > 0)
692
756
  process.stdout.write(ESC + '[' + up + 'A');
693
757
  process.stdout.write(CR + ESC + '[J');
@@ -777,13 +841,14 @@ export class LineEditor {
777
841
  if (statusUnchanged) {
778
842
  // Count the real input rows via layout (wrapping aware).
779
843
  const layOnly = layoutInput(this.promptStr, this.buf, this.cursor, cols);
780
- const inputLineRows = layOnly.rows.length;
781
844
  const suggOnly = this._suggestions();
782
845
  const shownOnly = suggOnly.slice(0, 8);
783
846
  const linesBelowOnly = shownOnly.length
784
847
  ? shownOnly.length + (suggOnly.length > shownOnly.length ? 1 : 0)
785
848
  : 0;
786
- this._eraseInputOnly(inputLineRows + linesBelowOnly);
849
+ // Move up to the INPUT top using the OLD cursor row (the position that is
850
+ // on screen right now), never the input's height.
851
+ this._eraseInputOnly(this.cursorRowFromTop - this._statusTop);
787
852
  let outOnly = '';
788
853
  outOnly += layOnly.rows.map((r) => r.prefix + r.text).join(NL);
789
854
  if (shownOnly.length) {
@@ -885,14 +950,14 @@ export class LineEditor {
885
950
  }
886
951
  const cols = process.stdout.columns || 80;
887
952
  const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
888
- const inputRows = lay.rows.length;
889
953
  const sugg = this._suggestions();
890
954
  const shown = sugg.slice(0, 8);
891
955
  this._suggestCount = shown.length;
892
956
  const linesBelow = shown.length
893
957
  ? shown.length + (sugg.length > shown.length ? 1 : 0)
894
958
  : 0;
895
- this._eraseInputOnly(inputRows + linesBelow);
959
+ // Move up to the INPUT top using the OLD cursor row (on screen right now).
960
+ this._eraseInputOnly(this.cursorRowFromTop - this._statusTop);
896
961
  let out = lay.rows.map((r) => r.prefix + r.text).join(NL);
897
962
  if (shown.length) {
898
963
  const maxName = Math.max(...shown.map((c) => c.name.length));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.49.1",
3
+ "version": "2.49.3",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {