zames_pro 2.29.0 → 2.29.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.
package/dist/browser.js CHANGED
@@ -160,6 +160,16 @@ const STOP_SELECTORS = [
160
160
  'button:has-text("Остановить")',
161
161
  'button[aria-label*="Stop" i]',
162
162
  ];
163
+ // DeepSeek's "Continue" button. With the reasoning ("Deep thinking") toggle
164
+ // on, the server caps the THINK phase: the reasoning stops mid-way and the UI
165
+ // offers a Continue button (this is NOT the same as a truncated turn with
166
+ // generation_err — there the answer failed; here the model simply paused).
167
+ // The agent must click it so the reasoning/answer keeps flowing, otherwise the
168
+ // turn sits idle until the operator presses Continue by hand.
169
+ //
170
+ // The button is found by scanning every div[role=button]/button and matching
171
+ // an EXACT short label ("Continue" / "Продолжить"), because the class names
172
+ // change and :has-text is not valid in a page-side querySelectorAll.
163
173
  // DeepSeek UI service statuses that are NOT the model's answer.
164
174
  // Otherwise the agent takes a status (Reading...) for an answer and breaks parsing.
165
175
  const STATUS_RE = /^(reading|thinking|searching|analyzing|generating|stop|остановить|читаю|думаю|поиск|анализ)[\s.…]*$/i;
@@ -177,6 +187,19 @@ const RATE_LIMIT_RE = /(messages? too frequent|too many requests|rate limit|сл
177
187
  // rate limit, these usually clear in seconds, so we retry quickly instead of
178
188
  // waiting minutes. "Server busy", 503, "temporarily unavailable", etc.
179
189
  const SERVER_BUSY_RE = /(server (is )?busy|server error|service (is )?unavailable|temporarily unavailable|internal server error|502|503|504|server overloaded|сервер занят|сервер перегружен|сервис недоступен|внутренняя ошибка|попробуйте позже)/i;
190
+ // DeepSeek turns a generation that FAILED mid-way into a truncated turn
191
+ // (`quasi_status: INCOMPLETE` + `finish_reason: generation_err`) and shows a
192
+ // "Continue" button in the UI. The failure is invisible to the answer text:
193
+ // the DOM keeps the partial answer (often the previous one), so the finish
194
+ // loop used to wait out the whole timeout and throw ds.send_no_new_answer.
195
+ // The SSE body IS available (net-capture keeps it as `_netCapture`), so we
196
+ // recognize the failed/truncated turn from it and retry instead of hanging.
197
+ const GENERATION_ERR_RE = /"finish_reason":\s*"generation_err"/i;
198
+ const INCOMPLETE_STATUS_RE = /"quasi_status","v":"INCOMPLETE"/i;
199
+ export function isGenerationIncompleteText(text) {
200
+ const t = String(text || '');
201
+ return GENERATION_ERR_RE.test(t) || INCOMPLETE_STATUS_RE.test(t);
202
+ }
180
203
  export function isRateLimitText(text) {
181
204
  return RATE_LIMIT_RE.test(String(text || ''));
182
205
  }
@@ -210,6 +233,20 @@ export class ServerBusyError extends Error {
210
233
  this.name = 'ServerBusyError';
211
234
  }
212
235
  }
236
+ // The generation was truncated by the server (`generation_err` / INCOMPLETE).
237
+ // Distinct from ServerBusyError: the DeepSeek backend answered, but the turn
238
+ // did not finish, and the UI offers a "Continue" button. The agent retries the
239
+ // same send (a new message into the chat) instead of waiting for an answer
240
+ // that will never arrive. This is what the operator hit with the reasoning
241
+ // (deep thinking) mode on: the long THINK phase makes the truncation far more
242
+ // likely, so with thinking off everything worked and with it on the turn kept
243
+ // stopping and showing "Continue".
244
+ export class GenerationIncompleteError extends Error {
245
+ constructor(detail) {
246
+ super('Generation incomplete (server truncated the answer). ' + detail);
247
+ this.name = 'GenerationIncompleteError';
248
+ }
249
+ }
213
250
  // ---------- profile cleanup ----------
214
251
  async function cleanSingletonFiles() {
215
252
  const names = ['SingletonLock', 'SingletonCookie', 'SingletonSocket'];
@@ -249,6 +286,12 @@ export class DeepSeekBrowser {
249
286
  maxRateLimitRetries;
250
287
  maxServerBusyRetries;
251
288
  serverBusyWaitMs;
289
+ // Retries for a turn the server truncated (generation_err / INCOMPLETE).
290
+ maxIncompleteRetries;
291
+ incompleteWaitMs;
292
+ // Click DeepSeek's "Continue" button automatically when the reasoning phase
293
+ // is paused by the server (deep thinking mode).
294
+ autoContinue;
252
295
  // Desired state of the DeepSeek chat toggles, applied before each send.
253
296
  // deepThinking: the "Deep thinking" toggle (reasoning; the reasoning text
254
297
  // is never read/shown). webSearch: the "Smart search" toggle.
@@ -307,7 +350,7 @@ export class DeepSeekBrowser {
307
350
  // must be stripped (DeepSeek's CDN answers 403 to it); computed in the
308
351
  // constructor, overridable by an explicit UA in the options/config.
309
352
  userAgent;
310
- constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, deepThinking = false, webSearch = true, auth, locale, onAuthSave, userAgent, } = {}) {
353
+ constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, autoContinue = true, deepThinking = false, webSearch = true, auth, locale, onAuthSave, userAgent, } = {}) {
311
354
  this.headless = headless;
312
355
  this.debug = debug;
313
356
  this.channel = channel;
@@ -320,6 +363,9 @@ export class DeepSeekBrowser {
320
363
  this.maxRateLimitRetries = maxRateLimitRetries;
321
364
  this.maxServerBusyRetries = maxServerBusyRetries;
322
365
  this.serverBusyWaitMs = serverBusyWaitMs;
366
+ this.maxIncompleteRetries = maxIncompleteRetries;
367
+ this.incompleteWaitMs = incompleteWaitMs;
368
+ this.autoContinue = autoContinue;
323
369
  this.deepThinking = deepThinking;
324
370
  this.webSearch = webSearch;
325
371
  this.auth = {
@@ -1017,6 +1063,95 @@ export class DeepSeekBrowser {
1017
1063
  async _isGenerating() {
1018
1064
  return await this._stopButtonVisible();
1019
1065
  }
1066
+ // Is DeepSeek's "Continue" button currently visible? Used to BLOCK the
1067
+ // "answer settled" early-return in the finish loop: while Continue is on
1068
+ // screen the generation is PAUSED, not finished, so returning the partial
1069
+ // text there is exactly the "agent stopped with a Continue button" bug.
1070
+ async _continueButtonVisible() {
1071
+ try {
1072
+ return await this.page.evaluate(() => {
1073
+ const labelOf = (b) => ((b.textContent || '') + ' ' + (b.getAttribute('aria-label') || ''))
1074
+ .replace(/\s+/g, ' ')
1075
+ .trim();
1076
+ const isExact = (t) => /^(continue|продолжить|продолжение)\s*[.!…]?$/i.test(t);
1077
+ const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
1078
+ for (const e of cands) {
1079
+ if (!isExact(labelOf(e)))
1080
+ continue;
1081
+ const st = getComputedStyle(e);
1082
+ if (st.display === 'none' || st.visibility === 'hidden')
1083
+ continue;
1084
+ if (!e.offsetParent && st.position !== 'fixed')
1085
+ continue;
1086
+ return true;
1087
+ }
1088
+ return false;
1089
+ });
1090
+ }
1091
+ catch {
1092
+ return false;
1093
+ }
1094
+ }
1095
+ // Click the DeepSeek "Continue" button when it is visible (reasoning/answer
1096
+ // paused by the server). Returns true when a click happened.
1097
+ //
1098
+ // With the "Deep thinking" toggle on, DeepSeek caps the THINK phase and
1099
+ // shows Continue; the model does NOT resume by itself. Without this, a long
1100
+ // reasoning turn sat idle until the operator pressed Continue by hand. We
1101
+ // only act on an EXACT short label so a random "Continue" in prose (a button
1102
+ // inside a rendered answer, etc.) is never clicked.
1103
+ async _clickContinueIfVisible() {
1104
+ try {
1105
+ const clicked = await this.page.evaluate(() => {
1106
+ // Normalize a label: textContent + aria-label, whitespace collapsed.
1107
+ const labelOf = (b) => ((b.textContent || '') + ' ' + (b.getAttribute('aria-label') || ''))
1108
+ .replace(/\s+/g, ' ')
1109
+ .trim();
1110
+ // The button must be a SHORT exact label, otherwise a paragraph or a
1111
+ // rendered "Continue" inside the answer could be clicked by mistake.
1112
+ const isExact = (t) => /^(continue|продолжить|продолжение)\s*[.!…]?$/i.test(t);
1113
+ const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
1114
+ for (const e of cands) {
1115
+ const t = labelOf(e);
1116
+ if (!isExact(t))
1117
+ continue;
1118
+ const st = getComputedStyle(e);
1119
+ if (st.display === 'none' || st.visibility === 'hidden')
1120
+ continue;
1121
+ if (!e.offsetParent && st.position !== 'fixed')
1122
+ continue;
1123
+ // DeepSeek's button is a React component listening for POINTER/mouse
1124
+ // events, not just a bare click(). A plain e.click() sometimes does
1125
+ // nothing; dispatching the full sequence is what a real press does.
1126
+ const rect = e.getBoundingClientRect();
1127
+ const cx = rect.left + rect.width / 2;
1128
+ const cy = rect.top + rect.height / 2;
1129
+ const opts = {
1130
+ bubbles: true,
1131
+ cancelable: true,
1132
+ composed: true,
1133
+ clientX: cx,
1134
+ clientY: cy,
1135
+ button: 0,
1136
+ };
1137
+ try {
1138
+ e.dispatchEvent(new PointerEvent('pointerdown', opts));
1139
+ e.dispatchEvent(new MouseEvent('mousedown', opts));
1140
+ e.dispatchEvent(new PointerEvent('pointerup', opts));
1141
+ e.dispatchEvent(new MouseEvent('mouseup', opts));
1142
+ }
1143
+ catch { }
1144
+ e.click();
1145
+ return true;
1146
+ }
1147
+ return false;
1148
+ });
1149
+ return !!clicked;
1150
+ }
1151
+ catch {
1152
+ return false;
1153
+ }
1154
+ }
1020
1155
  // DeepSeek exposes two toggle buttons above the input: "Deep thinking"
1021
1156
  // (reasoning) and "Smart search" (web search). Their labels are localized,
1022
1157
  // so we match by a loose regex on the visible text and read the state from
@@ -1125,6 +1260,11 @@ export class DeepSeekBrowser {
1125
1260
  let attempt = 0;
1126
1261
  let rateLimitRetries = 0;
1127
1262
  let serverBusyRetries = 0;
1263
+ // Bumped when DeepSeek truncated the turn (`generation_err`/INCOMPLETE).
1264
+ // The retry does NOT consume a regular ask() attempt: it is a server-side
1265
+ // hiccup, not a failed send, and it is more likely in the reasoning
1266
+ // (deep thinking) mode, where the long THINK phase gets cut off.
1267
+ let incompleteRetries = 0;
1128
1268
  while (attempt < this.askRetries) {
1129
1269
  attempt++;
1130
1270
  try {
@@ -1182,6 +1322,28 @@ export class DeepSeekBrowser {
1182
1322
  return '(прервано пользователем)';
1183
1323
  continue;
1184
1324
  }
1325
+ // The turn was TRUNCATED by the server (`generation_err`, INCOMPLETE):
1326
+ // DeepSeek answered with a partial (often empty) answer and shows a
1327
+ // "Continue" button. In reasoning mode this is much more likely. We
1328
+ // resend the SAME prompt into the chat (that is what the Continue
1329
+ // button does) instead of waiting for an answer that will never come.
1330
+ if (e instanceof GenerationIncompleteError) {
1331
+ incompleteRetries++;
1332
+ if (incompleteRetries > this.maxIncompleteRetries) {
1333
+ console.error(theme.error(this._t('ds.incomplete_give_up', {
1334
+ attempt: incompleteRetries,
1335
+ })));
1336
+ throw e;
1337
+ }
1338
+ console.error(theme.warn(this._t('ds.incomplete_retry', {
1339
+ attempt: incompleteRetries,
1340
+ max: this.maxIncompleteRetries,
1341
+ })));
1342
+ const aborted = await this._sleepInterruptible(this.incompleteWaitMs);
1343
+ if (aborted)
1344
+ return '(прервано пользователем)';
1345
+ continue;
1346
+ }
1185
1347
  console.error('\n' +
1186
1348
  theme.warn(this._t('ds.ask_retry', {
1187
1349
  attempt,
@@ -1527,6 +1689,20 @@ export class DeepSeekBrowser {
1527
1689
  if (isServerBusyText(pageText)) {
1528
1690
  throw new ServerBusyError(pageText.slice(0, 300));
1529
1691
  }
1692
+ // The turn was truncated by the server: the SSE body carries
1693
+ // finish_reason=generation_err / quasi_status=INCOMPLETE and the UI
1694
+ // shows "Continue". Retry instead of waiting out the whole timeout.
1695
+ if (this._netCapture &&
1696
+ this._netCaptureAt >= this._lastSentAt &&
1697
+ isGenerationIncompleteText(this._netCapture)) {
1698
+ throw new GenerationIncompleteError(this._netCapture.slice(-300));
1699
+ }
1700
+ // Reasoning-mode pause during the THINK phase: click Continue so the
1701
+ // generation resumes (the button appears before any RESPONSE text, so
1702
+ // the start-wait would otherwise spin until the deadline).
1703
+ if (this.autoContinue && (await this._clickContinueIfVisible())) {
1704
+ this._askDebug('CLICKED Continue (start-loop)');
1705
+ }
1530
1706
  const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
1531
1707
  const bodyLen = await this.page
1532
1708
  .evaluate(() => document.body.innerText.length)
@@ -1562,7 +1738,10 @@ export class DeepSeekBrowser {
1562
1738
  // capture proves a new answer exists. If it stays equal, keep waiting.
1563
1739
  const notGenerating = !(await this._isGenerating());
1564
1740
  const differs = !!cur && normText(cur) !== normText(beforeText);
1565
- if (differs && cur === lastStartCur && notGenerating) {
1741
+ // Do NOT accept a "settled" answer while Continue is on screen — that
1742
+ // is a paused generation, not a finished one.
1743
+ const paused = this.autoContinue && (await this._continueButtonVisible());
1744
+ if (differs && cur === lastStartCur && notGenerating && !paused) {
1566
1745
  settledTicks++;
1567
1746
  if (settledTicks >= 2) {
1568
1747
  this._askDebug('SETTLED-differs return len=' + cur.length);
@@ -1660,6 +1839,22 @@ export class DeepSeekBrowser {
1660
1839
  throw new ServerBusyError(pageText.slice(0, 300));
1661
1840
  }
1662
1841
  }
1842
+ // The server truncated the turn (generation_err / INCOMPLETE). The DOM
1843
+ // keeps the partial answer, so without this check the loop waits out the
1844
+ // whole timeout and throws ds.send_no_new_answer — the operator sees the
1845
+ // agent "stop" with a Continue button in the chat. Retry the send.
1846
+ if (this._netCapture &&
1847
+ this._netCaptureAt >= this._lastSentAt &&
1848
+ isGenerationIncompleteText(this._netCapture)) {
1849
+ throw new GenerationIncompleteError(this._netCapture.slice(-300));
1850
+ }
1851
+ // Reasoning-mode pause: DeepSeek caps the THINK phase and shows a
1852
+ // Continue button; the model does not resume by itself. Click it so the
1853
+ // answer keeps flowing instead of waiting for the operator.
1854
+ if (this.autoContinue && (await this._clickContinueIfVisible())) {
1855
+ this._askDebug('CLICKED Continue (finish-loop)');
1856
+ continue;
1857
+ }
1663
1858
  const netFresh = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
1664
1859
  // The DOM may be empty while the answer is already in the network
1665
1860
  // capture (a resumed chat re-renders its history, the answer selector
@@ -1687,7 +1882,17 @@ export class DeepSeekBrowser {
1687
1882
  // so the FIN-loop always cost ~1.6s per answer. Defaults are now
1688
1883
  // 2 checks x 400ms (~0.8s saved per turn) and the values are honored.
1689
1884
  if (stable >= Math.max(1, this.stabilityChecks - 1)) {
1690
- if (isNew || !(await this._isGenerating())) {
1885
+ // A PAUSED generation also looks "stable" (the text stops changing
1886
+ // while Continue is on screen), so the old check returned the
1887
+ // partial answer here — the "agent stopped with a Continue button"
1888
+ // symptom. Click Continue and keep waiting instead.
1889
+ if (this.autoContinue && (await this._continueButtonVisible())) {
1890
+ if (await this._clickContinueIfVisible()) {
1891
+ this._askDebug('CLICKED Continue (before early return)');
1892
+ }
1893
+ stable = 0;
1894
+ }
1895
+ else if (isNew || !(await this._isGenerating())) {
1691
1896
  this._askDebug('RETURN stable curLen=' + cur.length);
1692
1897
  return cur;
1693
1898
  }
@@ -1713,6 +1918,10 @@ export class DeepSeekBrowser {
1713
1918
  await this.page.waitForTimeout(Math.max(0, this.stabilityDelayMs));
1714
1919
  }
1715
1920
  if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
1921
+ // If Continue is STILL on screen, the turn is paused, not finished:
1922
+ // click once more and report what we have rather than silently stopping.
1923
+ if (this.autoContinue)
1924
+ await this._clickContinueIfVisible();
1716
1925
  this._askDebug('RETURN last len=' + last.length);
1717
1926
  return last;
1718
1927
  }
package/dist/config.js CHANGED
@@ -43,6 +43,9 @@ export const DEFAULTS = {
43
43
  minSendIntervalMs: 15000,
44
44
  rateLimitWaitMs: 300000,
45
45
  maxRateLimitRetries: 6,
46
+ maxIncompleteRetries: 4,
47
+ incompleteWaitMs: 2000,
48
+ autoContinue: true,
46
49
  deepThinking: false,
47
50
  webSearch: true,
48
51
  auth: {
@@ -230,6 +233,28 @@ export const CONFIG_SCHEMA = [
230
233
  labelKey: 'cfg.f.browser_maxRateLimitRetries',
231
234
  groupKey: 'cfg.group.browser',
232
235
  },
236
+ {
237
+ path: 'browser.maxIncompleteRetries',
238
+ type: 'number',
239
+ min: 0,
240
+ max: 100,
241
+ labelKey: 'cfg.f.browser_maxIncompleteRetries',
242
+ groupKey: 'cfg.group.browser',
243
+ },
244
+ {
245
+ path: 'browser.incompleteWaitMs',
246
+ type: 'number',
247
+ min: 0,
248
+ max: 600000,
249
+ labelKey: 'cfg.f.browser_incompleteWaitMs',
250
+ groupKey: 'cfg.group.browser',
251
+ },
252
+ {
253
+ path: 'browser.autoContinue',
254
+ type: 'boolean',
255
+ labelKey: 'cfg.f.browser_autoContinue',
256
+ groupKey: 'cfg.group.browser',
257
+ },
233
258
  {
234
259
  path: 'browser.deepThinking',
235
260
  type: 'boolean',
package/dist/i18n.js CHANGED
@@ -533,6 +533,14 @@ const CATALOG = {
533
533
  ru: '⚠ Ответ не получен ({attempt}/{max}): {error} — повторяю...',
534
534
  en: '⚠ No answer yet ({attempt}/{max}): {error} — retrying...',
535
535
  },
536
+ 'ds.incomplete_retry': {
537
+ ru: '⏳ DeepSeek оборвал ответ (кнопка Continue в чате) — повторяю отправку ({attempt}/{max})...',
538
+ en: '⏳ DeepSeek truncated the answer (the Continue button in the chat) — resending ({attempt}/{max})...',
539
+ },
540
+ 'ds.incomplete_give_up': {
541
+ ru: '✖ DeepSeek {attempt} раз подряд оборвал ответ, не дойдя до конца. Возможно, включён режим размышления — попробуй выключить его (browser.deepThinking) или повтори позже.',
542
+ en: '✖ DeepSeek truncated the answer {attempt} times in a row. The reasoning mode (deep thinking) may be on — try turning it off (browser.deepThinking) or retry later.',
543
+ },
536
544
  'ds.ask_restart_browser': {
537
545
  ru: '⚠ Браузер потерял страницу — перезапускаю и вхожу заново...',
538
546
  en: '⚠ The browser lost the page — restarting and signing in again...',
@@ -858,6 +866,18 @@ const CATALOG = {
858
866
  ru: 'Число повторов при лимите',
859
867
  en: 'Rate-limit retry count',
860
868
  },
869
+ 'cfg.f.browser_maxIncompleteRetries': {
870
+ ru: 'Повторы при обрыве ответа (Continue)',
871
+ en: 'Retries when the answer is truncated (Continue)',
872
+ },
873
+ 'cfg.f.browser_incompleteWaitMs': {
874
+ ru: 'Пауза перед повтором после обрыва, мс',
875
+ en: 'Wait before retrying a truncated turn, ms',
876
+ },
877
+ 'cfg.f.browser_autoContinue': {
878
+ ru: 'Автонажатие кнопки Continue (режим размышления)',
879
+ en: 'Auto-click Continue (reasoning mode)',
880
+ },
861
881
  'cfg.f.browser_deepThinking': {
862
882
  ru: 'Глубокое мышление (долго; размышления не выводятся)',
863
883
  en: 'Deep thinking (slow; reasoning is hidden)',
package/dist/input.js CHANGED
@@ -233,6 +233,10 @@ export class LineEditor {
233
233
  pendingText;
234
234
  rendered;
235
235
  cursorRowFromTop;
236
+ // Number of rows occupied by the status/spinner block above the input line,
237
+ // as computed by the last _writeBlock(). _renderInputOnly() uses it to keep
238
+ // cursorRowFromTop correct without repainting the status.
239
+ _statusTop;
236
240
  busy;
237
241
  onSubmit;
238
242
  onEscape;
@@ -309,6 +313,7 @@ export class LineEditor {
309
313
  this.pendingText = null;
310
314
  this.rendered = false;
311
315
  this.cursorRowFromTop = 0;
316
+ this._statusTop = 0;
312
317
  this.busy = false;
313
318
  this.onSubmit = null;
314
319
  this.onEscape = null;
@@ -642,6 +647,7 @@ export class LineEditor {
642
647
  // re-printing the whole block on every keystroke is what made the display
643
648
  // flicker on terminals like Tabby. `top` is recovered from the cache so the
644
649
  // cursor math stays correct.
650
+ this._statusTop = top;
645
651
  const statusBlock = statusOut;
646
652
  const statusUnchanged = this.rendered && statusBlock === this._lastStatusBlock;
647
653
  if (statusUnchanged) {
@@ -729,6 +735,52 @@ export class LineEditor {
729
735
  this._eraseBlock();
730
736
  this._writeBlock();
731
737
  }
738
+ // Repaint ONLY the input rows, leaving the status/spinner block untouched.
739
+ // Used for buffer/cursor changes (typing, arrows, backspace, …): the status
740
+ // line has its OWN animation timer, and repainting it on every keystroke
741
+ // made the whole block blink on terminals like Tabby. When nothing is drawn
742
+ // yet (rendered === false) fall back to a full render so the very first
743
+ // paint is correct.
744
+ _renderInputOnly() {
745
+ if (!this.rendered) {
746
+ this._render();
747
+ return;
748
+ }
749
+ const cols = process.stdout.columns || 80;
750
+ const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
751
+ const inputRows = lay.rows.length;
752
+ const sugg = this._suggestions();
753
+ const shown = sugg.slice(0, 8);
754
+ this._suggestCount = shown.length;
755
+ const linesBelow = shown.length
756
+ ? shown.length + (sugg.length > shown.length ? 1 : 0)
757
+ : 0;
758
+ this._eraseInputOnly(inputRows + linesBelow);
759
+ let out = lay.rows.map((r) => r.prefix + r.text).join(NL);
760
+ if (shown.length) {
761
+ const maxName = Math.max(...shown.map((c) => c.name.length));
762
+ const lines = shown.map((c) => {
763
+ const name = theme.prompt(c.name.padEnd(maxName));
764
+ const desc = theme.dim(' ' + c.description);
765
+ return ' ' + name + desc;
766
+ });
767
+ out += NL + lines.join(NL);
768
+ const hidden = sugg.length - shown.length;
769
+ if (hidden > 0)
770
+ out +=
771
+ NL +
772
+ theme.dim(' ' + translate(this.locale)('editor.more', { n: hidden }));
773
+ }
774
+ process.stdout.write(out);
775
+ const up = lay.rows.length - 1 - lay.cursorRow + linesBelow;
776
+ if (up > 0)
777
+ process.stdout.write(ESC + '[' + up + 'A');
778
+ process.stdout.write(CR);
779
+ if (lay.cursorCol > 0)
780
+ process.stdout.write(ESC + '[' + lay.cursorCol + 'C');
781
+ this.rendered = true;
782
+ this.cursorRowFromTop = this._statusTop + lay.cursorRow;
783
+ }
732
784
  // Print a block ABOVE the input line and put the input line back.
733
785
  printAbove(text) {
734
786
  this._stopDots();
@@ -1185,7 +1237,7 @@ export class LineEditor {
1185
1237
  }
1186
1238
  this._pasteBuf = '';
1187
1239
  }
1188
- this._render();
1240
+ this._renderInputOnly();
1189
1241
  continue;
1190
1242
  }
1191
1243
  const pasteAt = s.indexOf(PASTE_START);
@@ -1196,7 +1248,7 @@ export class LineEditor {
1196
1248
  this._pasteBuf = '';
1197
1249
  if (before) {
1198
1250
  this._insert(before);
1199
- this._render();
1251
+ this._renderInputOnly();
1200
1252
  }
1201
1253
  continue;
1202
1254
  }
@@ -1205,31 +1257,31 @@ export class LineEditor {
1205
1257
  if (s.startsWith(ESC + '[13;2u')) {
1206
1258
  this._insertNewline();
1207
1259
  s = s.slice(7);
1208
- this._render();
1260
+ this._renderInputOnly();
1209
1261
  continue;
1210
1262
  }
1211
1263
  if (s.startsWith(ESC + '[13;5u')) {
1212
1264
  this._insertNewline();
1213
1265
  s = s.slice(7);
1214
- this._render();
1266
+ this._renderInputOnly();
1215
1267
  continue;
1216
1268
  }
1217
1269
  if (s.startsWith(ESC + '[27;2;13~')) {
1218
1270
  this._insertNewline();
1219
1271
  s = s.slice(10);
1220
- this._render();
1272
+ this._renderInputOnly();
1221
1273
  continue;
1222
1274
  }
1223
1275
  if (s.startsWith(ESC + '[27;5;13~')) {
1224
1276
  this._insertNewline();
1225
1277
  s = s.slice(10);
1226
- this._render();
1278
+ this._renderInputOnly();
1227
1279
  continue;
1228
1280
  }
1229
1281
  if (s.startsWith(ESC + NL)) {
1230
1282
  this._insertNewline();
1231
1283
  s = s.slice(2);
1232
- this._render();
1284
+ this._renderInputOnly();
1233
1285
  continue;
1234
1286
  }
1235
1287
  const ch = s[0];
@@ -1243,7 +1295,7 @@ export class LineEditor {
1243
1295
  const before = this.cursor > 0 ? Array.from(this.buf)[this.cursor - 1] : '';
1244
1296
  if (before === '\\') {
1245
1297
  this._insertNewline();
1246
- this._render();
1298
+ this._renderInputOnly();
1247
1299
  continue;
1248
1300
  }
1249
1301
  this._submit();
@@ -1251,7 +1303,7 @@ export class LineEditor {
1251
1303
  }
1252
1304
  if (code === 10) {
1253
1305
  this._insertNewline();
1254
- this._render();
1306
+ this._renderInputOnly();
1255
1307
  continue;
1256
1308
  }
1257
1309
  if (code === 3) {
@@ -1266,24 +1318,24 @@ export class LineEditor {
1266
1318
  }
1267
1319
  if (code === 1) {
1268
1320
  this._home();
1269
- this._render();
1321
+ this._renderInputOnly();
1270
1322
  continue;
1271
1323
  }
1272
1324
  if (code === 5) {
1273
1325
  this._end();
1274
- this._render();
1326
+ this._renderInputOnly();
1275
1327
  continue;
1276
1328
  }
1277
1329
  if (code === 9) {
1278
1330
  this._completeCommand();
1279
- this._render();
1331
+ this._renderInputOnly();
1280
1332
  continue;
1281
1333
  }
1282
1334
  if (code === 21) {
1283
1335
  this.buf = '';
1284
1336
  this.cursor = 0;
1285
1337
  this.pastes = [];
1286
- this._render();
1338
+ this._renderInputOnly();
1287
1339
  continue;
1288
1340
  }
1289
1341
  if (code === 22) {
@@ -1295,7 +1347,7 @@ export class LineEditor {
1295
1347
  }
1296
1348
  if (code === 23) {
1297
1349
  this._deleteWordLeft();
1298
- this._render();
1350
+ this._renderInputOnly();
1299
1351
  continue;
1300
1352
  }
1301
1353
  if (code === 11) {
@@ -1306,12 +1358,12 @@ export class LineEditor {
1306
1358
  e++;
1307
1359
  arr.splice(this.cursor, e - this.cursor);
1308
1360
  this.buf = arr.join('');
1309
- this._render();
1361
+ this._renderInputOnly();
1310
1362
  continue;
1311
1363
  }
1312
1364
  if (code === 127 || code === 8) {
1313
1365
  this._backspace();
1314
- this._render();
1366
+ this._renderInputOnly();
1315
1367
  continue;
1316
1368
  }
1317
1369
  if (code === 27) {
@@ -1320,25 +1372,25 @@ export class LineEditor {
1320
1372
  if (s.startsWith('[1;5D') || s.startsWith('[5D')) {
1321
1373
  this._wordLeft();
1322
1374
  s = s.slice(s.startsWith('[1;5D') ? 5 : 3);
1323
- this._render();
1375
+ this._renderInputOnly();
1324
1376
  continue;
1325
1377
  }
1326
1378
  if (s.startsWith('[1;5C') || s.startsWith('[5C')) {
1327
1379
  this._wordRight();
1328
1380
  s = s.slice(s.startsWith('[1;5C') ? 5 : 3);
1329
- this._render();
1381
+ this._renderInputOnly();
1330
1382
  continue;
1331
1383
  }
1332
1384
  if (s.startsWith('[D')) {
1333
1385
  this._left();
1334
1386
  s = s.slice(2);
1335
- this._render();
1387
+ this._renderInputOnly();
1336
1388
  continue;
1337
1389
  }
1338
1390
  if (s.startsWith('[C')) {
1339
1391
  this._right();
1340
1392
  s = s.slice(2);
1341
- this._render();
1393
+ this._renderInputOnly();
1342
1394
  continue;
1343
1395
  }
1344
1396
  if (s.startsWith('[A')) {
@@ -1348,7 +1400,7 @@ export class LineEditor {
1348
1400
  else
1349
1401
  this._up();
1350
1402
  s = s.slice(2);
1351
- this._render();
1403
+ this._renderInputOnly();
1352
1404
  continue;
1353
1405
  }
1354
1406
  if (s.startsWith('[B')) {
@@ -1357,38 +1409,38 @@ export class LineEditor {
1357
1409
  else
1358
1410
  this._down();
1359
1411
  s = s.slice(2);
1360
- this._render();
1412
+ this._renderInputOnly();
1361
1413
  continue;
1362
1414
  }
1363
1415
  if (s.startsWith('[H') || s.startsWith('[1~')) {
1364
1416
  this._home();
1365
1417
  s = s.slice(s.startsWith('[1~') ? 3 : 2);
1366
- this._render();
1418
+ this._renderInputOnly();
1367
1419
  continue;
1368
1420
  }
1369
1421
  if (s.startsWith('[F') || s.startsWith('[4~')) {
1370
1422
  this._end();
1371
1423
  s = s.slice(s.startsWith('[4~') ? 3 : 2);
1372
- this._render();
1424
+ this._renderInputOnly();
1373
1425
  continue;
1374
1426
  }
1375
1427
  if (s.startsWith('[3~')) {
1376
1428
  this._delete();
1377
1429
  s = s.slice(3);
1378
- this._render();
1430
+ this._renderInputOnly();
1379
1431
  continue;
1380
1432
  }
1381
1433
  // Alt+B / Alt+F — word movement.
1382
1434
  if (s.startsWith('b') || s.startsWith('B')) {
1383
1435
  this._wordLeft();
1384
1436
  s = s.slice(1);
1385
- this._render();
1437
+ this._renderInputOnly();
1386
1438
  continue;
1387
1439
  }
1388
1440
  if (s.startsWith('f') || s.startsWith('F')) {
1389
1441
  this._wordRight();
1390
1442
  s = s.slice(1);
1391
- this._render();
1443
+ this._renderInputOnly();
1392
1444
  continue;
1393
1445
  }
1394
1446
  let j = 0;
@@ -1400,7 +1452,7 @@ export class LineEditor {
1400
1452
  if (code < 32)
1401
1453
  continue;
1402
1454
  this._insert(ch);
1403
- this._render();
1455
+ this._renderInputOnly();
1404
1456
  }
1405
1457
  }
1406
1458
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.29.0",
3
+ "version": "2.29.2",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {