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 +212 -3
- package/dist/config.js +25 -0
- package/dist/i18n.js +20 -0
- package/dist/input.js +80 -28
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1321
|
+
this._renderInputOnly();
|
|
1270
1322
|
continue;
|
|
1271
1323
|
}
|
|
1272
1324
|
if (code === 5) {
|
|
1273
1325
|
this._end();
|
|
1274
|
-
this.
|
|
1326
|
+
this._renderInputOnly();
|
|
1275
1327
|
continue;
|
|
1276
1328
|
}
|
|
1277
1329
|
if (code === 9) {
|
|
1278
1330
|
this._completeCommand();
|
|
1279
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
1361
|
+
this._renderInputOnly();
|
|
1310
1362
|
continue;
|
|
1311
1363
|
}
|
|
1312
1364
|
if (code === 127 || code === 8) {
|
|
1313
1365
|
this._backspace();
|
|
1314
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1455
|
+
this._renderInputOnly();
|
|
1404
1456
|
}
|
|
1405
1457
|
}
|
|
1406
1458
|
}
|