zames_pro 2.28.2 → 2.29.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.
- package/dist/browser.js +151 -1
- package/dist/config.js +25 -0
- package/dist/i18n.js +20 -0
- package/dist/index.js +10 -1
- package/dist/input.js +5 -0
- 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,45 @@ export class DeepSeekBrowser {
|
|
|
1017
1063
|
async _isGenerating() {
|
|
1018
1064
|
return await this._stopButtonVisible();
|
|
1019
1065
|
}
|
|
1066
|
+
// Click the DeepSeek "Continue" button when it is visible (reasoning/answer
|
|
1067
|
+
// paused by the server). Returns true when a click happened.
|
|
1068
|
+
//
|
|
1069
|
+
// With the "Deep thinking" toggle on, DeepSeek caps the THINK phase and
|
|
1070
|
+
// shows Continue; the model does NOT resume by itself. Without this, a long
|
|
1071
|
+
// reasoning turn sat idle until the operator pressed Continue by hand. We
|
|
1072
|
+
// only act on an EXACT short label so a random "Continue" in prose (a button
|
|
1073
|
+
// inside a rendered answer, etc.) is never clicked.
|
|
1074
|
+
async _clickContinueIfVisible() {
|
|
1075
|
+
try {
|
|
1076
|
+
const clicked = await this.page.evaluate(() => {
|
|
1077
|
+
// Normalize a label: textContent + aria-label, whitespace collapsed.
|
|
1078
|
+
const labelOf = (b) => ((b.textContent || '') + ' ' + (b.getAttribute('aria-label') || ''))
|
|
1079
|
+
.replace(/\s+/g, ' ')
|
|
1080
|
+
.trim();
|
|
1081
|
+
// The button must be a SHORT exact label, otherwise a paragraph or a
|
|
1082
|
+
// rendered "Continue" inside the answer could be clicked by mistake.
|
|
1083
|
+
const isExact = (t) => /^(continue|продолжить|продолжение)\s*[.!…]?$/i.test(t);
|
|
1084
|
+
const cands = Array.from(document.querySelectorAll('div[role="button"], button'));
|
|
1085
|
+
for (const e of cands) {
|
|
1086
|
+
const t = labelOf(e);
|
|
1087
|
+
if (!isExact(t))
|
|
1088
|
+
continue;
|
|
1089
|
+
const st = getComputedStyle(e);
|
|
1090
|
+
if (st.display === 'none' || st.visibility === 'hidden')
|
|
1091
|
+
continue;
|
|
1092
|
+
if (!e.offsetParent && st.position !== 'fixed')
|
|
1093
|
+
continue;
|
|
1094
|
+
e.click();
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
return false;
|
|
1098
|
+
});
|
|
1099
|
+
return !!clicked;
|
|
1100
|
+
}
|
|
1101
|
+
catch {
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1020
1105
|
// DeepSeek exposes two toggle buttons above the input: "Deep thinking"
|
|
1021
1106
|
// (reasoning) and "Smart search" (web search). Their labels are localized,
|
|
1022
1107
|
// so we match by a loose regex on the visible text and read the state from
|
|
@@ -1051,6 +1136,14 @@ export class DeepSeekBrowser {
|
|
|
1051
1136
|
getToggleStatesSync() {
|
|
1052
1137
|
return this._toggles;
|
|
1053
1138
|
}
|
|
1139
|
+
// Update the cached toggle states (used by the status icons) without a send.
|
|
1140
|
+
// Called from /config so the icons change immediately.
|
|
1141
|
+
setToggleState(s) {
|
|
1142
|
+
if (typeof s.deepThinking === 'boolean')
|
|
1143
|
+
this._toggles.deepThinking = s.deepThinking;
|
|
1144
|
+
if (typeof s.webSearch === 'boolean')
|
|
1145
|
+
this._toggles.webSearch = s.webSearch;
|
|
1146
|
+
}
|
|
1054
1147
|
// The CURRENT state of the two DeepSeek toggles, read from the DOM
|
|
1055
1148
|
// (aria-pressed) so the status line reflects what is actually on, not what
|
|
1056
1149
|
// the config asked for. Falls back to the configured values when the buttons
|
|
@@ -1117,6 +1210,11 @@ export class DeepSeekBrowser {
|
|
|
1117
1210
|
let attempt = 0;
|
|
1118
1211
|
let rateLimitRetries = 0;
|
|
1119
1212
|
let serverBusyRetries = 0;
|
|
1213
|
+
// Bumped when DeepSeek truncated the turn (`generation_err`/INCOMPLETE).
|
|
1214
|
+
// The retry does NOT consume a regular ask() attempt: it is a server-side
|
|
1215
|
+
// hiccup, not a failed send, and it is more likely in the reasoning
|
|
1216
|
+
// (deep thinking) mode, where the long THINK phase gets cut off.
|
|
1217
|
+
let incompleteRetries = 0;
|
|
1120
1218
|
while (attempt < this.askRetries) {
|
|
1121
1219
|
attempt++;
|
|
1122
1220
|
try {
|
|
@@ -1174,6 +1272,28 @@ export class DeepSeekBrowser {
|
|
|
1174
1272
|
return '(прервано пользователем)';
|
|
1175
1273
|
continue;
|
|
1176
1274
|
}
|
|
1275
|
+
// The turn was TRUNCATED by the server (`generation_err`, INCOMPLETE):
|
|
1276
|
+
// DeepSeek answered with a partial (often empty) answer and shows a
|
|
1277
|
+
// "Continue" button. In reasoning mode this is much more likely. We
|
|
1278
|
+
// resend the SAME prompt into the chat (that is what the Continue
|
|
1279
|
+
// button does) instead of waiting for an answer that will never come.
|
|
1280
|
+
if (e instanceof GenerationIncompleteError) {
|
|
1281
|
+
incompleteRetries++;
|
|
1282
|
+
if (incompleteRetries > this.maxIncompleteRetries) {
|
|
1283
|
+
console.error(theme.error(this._t('ds.incomplete_give_up', {
|
|
1284
|
+
attempt: incompleteRetries,
|
|
1285
|
+
})));
|
|
1286
|
+
throw e;
|
|
1287
|
+
}
|
|
1288
|
+
console.error(theme.warn(this._t('ds.incomplete_retry', {
|
|
1289
|
+
attempt: incompleteRetries,
|
|
1290
|
+
max: this.maxIncompleteRetries,
|
|
1291
|
+
})));
|
|
1292
|
+
const aborted = await this._sleepInterruptible(this.incompleteWaitMs);
|
|
1293
|
+
if (aborted)
|
|
1294
|
+
return '(прервано пользователем)';
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1177
1297
|
console.error('\n' +
|
|
1178
1298
|
theme.warn(this._t('ds.ask_retry', {
|
|
1179
1299
|
attempt,
|
|
@@ -1519,6 +1639,20 @@ export class DeepSeekBrowser {
|
|
|
1519
1639
|
if (isServerBusyText(pageText)) {
|
|
1520
1640
|
throw new ServerBusyError(pageText.slice(0, 300));
|
|
1521
1641
|
}
|
|
1642
|
+
// The turn was truncated by the server: the SSE body carries
|
|
1643
|
+
// finish_reason=generation_err / quasi_status=INCOMPLETE and the UI
|
|
1644
|
+
// shows "Continue". Retry instead of waiting out the whole timeout.
|
|
1645
|
+
if (this._netCapture &&
|
|
1646
|
+
this._netCaptureAt >= this._lastSentAt &&
|
|
1647
|
+
isGenerationIncompleteText(this._netCapture)) {
|
|
1648
|
+
throw new GenerationIncompleteError(this._netCapture.slice(-300));
|
|
1649
|
+
}
|
|
1650
|
+
// Reasoning-mode pause during the THINK phase: click Continue so the
|
|
1651
|
+
// generation resumes (the button appears before any RESPONSE text, so
|
|
1652
|
+
// the start-wait would otherwise spin until the deadline).
|
|
1653
|
+
if (this.autoContinue && (await this._clickContinueIfVisible())) {
|
|
1654
|
+
this._askDebug('CLICKED Continue (start-loop)');
|
|
1655
|
+
}
|
|
1522
1656
|
const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
|
|
1523
1657
|
const bodyLen = await this.page
|
|
1524
1658
|
.evaluate(() => document.body.innerText.length)
|
|
@@ -1652,6 +1786,22 @@ export class DeepSeekBrowser {
|
|
|
1652
1786
|
throw new ServerBusyError(pageText.slice(0, 300));
|
|
1653
1787
|
}
|
|
1654
1788
|
}
|
|
1789
|
+
// The server truncated the turn (generation_err / INCOMPLETE). The DOM
|
|
1790
|
+
// keeps the partial answer, so without this check the loop waits out the
|
|
1791
|
+
// whole timeout and throws ds.send_no_new_answer — the operator sees the
|
|
1792
|
+
// agent "stop" with a Continue button in the chat. Retry the send.
|
|
1793
|
+
if (this._netCapture &&
|
|
1794
|
+
this._netCaptureAt >= this._lastSentAt &&
|
|
1795
|
+
isGenerationIncompleteText(this._netCapture)) {
|
|
1796
|
+
throw new GenerationIncompleteError(this._netCapture.slice(-300));
|
|
1797
|
+
}
|
|
1798
|
+
// Reasoning-mode pause: DeepSeek caps the THINK phase and shows a
|
|
1799
|
+
// Continue button; the model does not resume by itself. Click it so the
|
|
1800
|
+
// answer keeps flowing instead of waiting for the operator.
|
|
1801
|
+
if (this.autoContinue && (await this._clickContinueIfVisible())) {
|
|
1802
|
+
this._askDebug('CLICKED Continue (finish-loop)');
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1655
1805
|
const netFresh = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
|
|
1656
1806
|
// The DOM may be empty while the answer is already in the network
|
|
1657
1807
|
// capture (a resumed chat re-renders its history, the answer selector
|
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/index.js
CHANGED
|
@@ -1407,13 +1407,22 @@ async function main() {
|
|
|
1407
1407
|
}
|
|
1408
1408
|
// Reflect browser toggles on the LIVE browser object. The browser is
|
|
1409
1409
|
// created once at startup, so without this a /config set browser.*
|
|
1410
|
-
// change would only apply after a restart.
|
|
1410
|
+
// change would only apply after a restart. We also update the cached
|
|
1411
|
+
// toggle states (_toggles) that the status icons read, and repaint the
|
|
1412
|
+
// editor, so the 🧠/🌐 icons change IMMEDIATELY (previously they only
|
|
1413
|
+
// updated after the next send, when _applyToggles() ran).
|
|
1411
1414
|
if (path === 'browser.deepThinking' || path === 'browser.webSearch') {
|
|
1412
1415
|
const b = browser;
|
|
1413
1416
|
if (path === 'browser.deepThinking')
|
|
1414
1417
|
b.deepThinking = !!value;
|
|
1415
1418
|
else
|
|
1416
1419
|
b.webSearch = !!value;
|
|
1420
|
+
b.setToggleState({
|
|
1421
|
+
deepThinking: b.deepThinking,
|
|
1422
|
+
webSearch: b.webSearch,
|
|
1423
|
+
});
|
|
1424
|
+
if (editor)
|
|
1425
|
+
editor.refreshStatus();
|
|
1417
1426
|
}
|
|
1418
1427
|
}
|
|
1419
1428
|
function configSetRaw(field, raw) {
|
package/dist/input.js
CHANGED
|
@@ -720,6 +720,11 @@ export class LineEditor {
|
|
|
720
720
|
this.rendered = true;
|
|
721
721
|
this.cursorRowFromTop = top + lay.cursorRow;
|
|
722
722
|
}
|
|
723
|
+
// Public repaint hook: called when an external state that the status line
|
|
724
|
+
// reads (e.g. the toggle icons) changed, so it can be redrawn immediately.
|
|
725
|
+
refreshStatus() {
|
|
726
|
+
this._render();
|
|
727
|
+
}
|
|
723
728
|
_render() {
|
|
724
729
|
this._eraseBlock();
|
|
725
730
|
this._writeBlock();
|