zames_pro 2.10.7 → 2.10.9
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/agent-loop.js +42 -5
- package/dist/browser.js +50 -2
- package/dist/i18n.js +1 -0
- package/dist/index.js +51 -2
- package/dist/input.js +42 -0
- package/package.json +1 -1
package/dist/agent-loop.js
CHANGED
|
@@ -20,6 +20,11 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
22
|
const safeThinking = safe(onThinking);
|
|
23
|
+
// Start the "working" indicator ONLY when a message is really sent (after
|
|
24
|
+
// the send-pause), not while the loop is still preparing the request.
|
|
25
|
+
// Previously safeThinking() fired before browser.ask(), so the spinner ran
|
|
26
|
+
// through the whole throttle pause / chat-open phase with nothing in flight.
|
|
27
|
+
browser.onSendStart = safeThinking;
|
|
23
28
|
const safeAssistantThought = safe(onAssistantThought);
|
|
24
29
|
const safeToolCall = safe(onToolCall);
|
|
25
30
|
const safeToolResult = safe(onToolResult);
|
|
@@ -117,6 +122,14 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
117
122
|
// instead get an EMPTY answer or the EXACT same answer as the previous turn
|
|
118
123
|
// (a sign the new message was not sent), we nudge instead of stopping.
|
|
119
124
|
let lastRaw = '';
|
|
125
|
+
// True when the PREVIOUS turn actually executed a tool. This is what
|
|
126
|
+
// makes a repeated answer "stale": the tool already ran, so running it
|
|
127
|
+
// again would duplicate side effects. If the previous answer looked like a
|
|
128
|
+
// call but was not recognized (e.g. a DSML/XML form the strict parser
|
|
129
|
+
// missed), the tool did NOT run and the echo is the only copy of the
|
|
130
|
+
// call — it must be executed, not discarded as stale.
|
|
131
|
+
// const ranToolLastTurn follows the previous turn's actual effect.
|
|
132
|
+
let lastTurnRanTool = false;
|
|
120
133
|
let justRanTool = false;
|
|
121
134
|
let watchdogRetries = 0;
|
|
122
135
|
const MAX_WATCHDOG_RETRIES = 3;
|
|
@@ -126,11 +139,18 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
126
139
|
let afterToolRetries = 0;
|
|
127
140
|
const MAX_AFTER_TOOL_RETRIES = 6;
|
|
128
141
|
for (let i = 0; i < maxIterations; i++) {
|
|
129
|
-
|
|
142
|
+
// NOTE: the spinner is NOT started here. browser.onSendStart fires it
|
|
143
|
+
// right when the message is actually typed/sent (after the send-pause),
|
|
144
|
+
// so no spinner runs during the pre-send phase.
|
|
130
145
|
// The first message (task) is user input: no throttle.
|
|
131
146
|
// Subsequent ones (tool-result and resend requests) are agent sends:
|
|
132
147
|
// throttled so we don't hit the rate limit.
|
|
133
148
|
const isFirst = i === 0;
|
|
149
|
+
// Did the PREVIOUS turn actually execute a tool? Used by the stale
|
|
150
|
+
// check below. We take a snapshot and immediately clear it: it will be
|
|
151
|
+
// set again at the end of this iteration only if a tool really runs.
|
|
152
|
+
const ranToolPrevTurn = lastTurnRanTool;
|
|
153
|
+
lastTurnRanTool = false;
|
|
134
154
|
// Safety net: browser.ask() has its own timeout, but a stuck send used to
|
|
135
155
|
// block the whole loop and look like a silent stop. We race it against a
|
|
136
156
|
// hard deadline and treat a timeout as a nudge (re-ask), never as a
|
|
@@ -194,18 +214,31 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
194
214
|
// previous answer with different markdown emphasis (`**done**` vs `done`),
|
|
195
215
|
// which defeated an exact match and made the loop run the SAME tool again —
|
|
196
216
|
// the "stopped after a tool call" signature with a duplicate call.
|
|
197
|
-
|
|
217
|
+
//
|
|
218
|
+
// AN ECHO IS NOT ALWAYS STALE: the previous answer may have been a
|
|
219
|
+
// DSML/XML call that the strict parser missed and the loop never actually
|
|
220
|
+
// ran. In that case the echo is the ONLY copy of the call we can get,
|
|
221
|
+
// and discarding it as "stale" burns the watchdog budget and stalls the
|
|
222
|
+
// agent. So we first check whether the echo itself parses as a tool
|
|
223
|
+
// call (including the XML/DSML form); if it does, we do NOT treat it
|
|
224
|
+
// as stale — the call gets run below like any other.
|
|
225
|
+
// Stale means: the PREVIOUS turn actually ran a tool (lastTurnRanTool)
|
|
226
|
+
// and this answer is a repeat of the previous one. If the previous
|
|
227
|
+
// answer never ran a tool (unrecognized DSML call), the repeat is NOT
|
|
228
|
+
// stale — it is the only copy of the call and must be executed.
|
|
229
|
+
const staleCandidate = ranToolPrevTurn &&
|
|
198
230
|
lastRaw.trim() !== '' &&
|
|
199
231
|
(rawResponse.trim() === lastRaw.trim() ||
|
|
200
232
|
normForStale(rawResponse) === normForStale(lastRaw));
|
|
233
|
+
const wdStale = staleCandidate;
|
|
201
234
|
const wdNoCall = justRanTool &&
|
|
202
235
|
!wdEmpty &&
|
|
203
236
|
!wdStale &&
|
|
204
237
|
parseToolCall(rawResponse) === null &&
|
|
205
238
|
!responseLooksLikeToolCall(rawResponse);
|
|
206
|
-
// A stale answer is discarded
|
|
207
|
-
//
|
|
208
|
-
//
|
|
239
|
+
// A stale answer is discarded only when it does NOT parse as a tool
|
|
240
|
+
// call (see wdStale above); an echo of a call that never ran is a real
|
|
241
|
+
// call and must be executed, not dropped.
|
|
209
242
|
if (!isFirst && (wdEmpty || wdStale || wdNoCall) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
|
|
210
243
|
watchdogRetries++;
|
|
211
244
|
transcript?.log('watchdog_nudge', {
|
|
@@ -464,6 +497,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
464
497
|
// A tool just ran — the next answer is expected to be a fresh tool call.
|
|
465
498
|
// Reset the watchdog so the next empty/repeated answer is nudged.
|
|
466
499
|
justRanTool = true;
|
|
500
|
+
// The tool really executed on this turn. The NEXT iteration will treat
|
|
501
|
+
// a repeat of this answer as stale only because of this flag (see NEXT,
|
|
502
|
+
// not the current, justRanTool check).
|
|
503
|
+
lastTurnRanTool = true;
|
|
467
504
|
watchdogRetries = 0;
|
|
468
505
|
// A fresh tool call just ran: reset the per-tool-result nudge budget so
|
|
469
506
|
// a long chain of tools is not cut off by an earlier bad turn.
|
package/dist/browser.js
CHANGED
|
@@ -139,6 +139,11 @@ export class DeepSeekBrowser {
|
|
|
139
139
|
_netSniff;
|
|
140
140
|
_netSniffLimit;
|
|
141
141
|
_netHookInstalled;
|
|
142
|
+
// Fired right when a message is actually typed/sent (AFTER the send-pause
|
|
143
|
+
// and attachments). Used to start the "agent is working" spinner only when
|
|
144
|
+
// a generation really begins, not during the pre-send phase (chat open,
|
|
145
|
+
// throttle wait), which used to show a spinner with no work in flight.
|
|
146
|
+
onSendStart;
|
|
142
147
|
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, } = {}) {
|
|
143
148
|
this.headless = headless;
|
|
144
149
|
this.debug = debug;
|
|
@@ -161,6 +166,7 @@ export class DeepSeekBrowser {
|
|
|
161
166
|
this._netSniff = [];
|
|
162
167
|
this._netSniffLimit = 5;
|
|
163
168
|
this._netHookInstalled = false;
|
|
169
|
+
this.onSendStart = null;
|
|
164
170
|
}
|
|
165
171
|
async launch() {
|
|
166
172
|
await killStaleChrome();
|
|
@@ -460,7 +466,11 @@ export class DeepSeekBrowser {
|
|
|
460
466
|
throw e;
|
|
461
467
|
}
|
|
462
468
|
console.error(theme.warn(`⏳ DeepSeek: «слишком часто». Жду ${Math.ceil(this.rateLimitWaitMs / 60000)} мин (${rateLimitRetries}/${this.maxRateLimitRetries}) и повторю...`));
|
|
463
|
-
|
|
469
|
+
// Interruptible: Esc/Ctrl+C must cancel this long wait too,
|
|
470
|
+
// otherwise "stop" stays frozen for up to 5 minutes.
|
|
471
|
+
const aborted = await this._sleepInterruptible(this.rateLimitWaitMs);
|
|
472
|
+
if (aborted)
|
|
473
|
+
return '(прервано пользователем)';
|
|
464
474
|
continue;
|
|
465
475
|
}
|
|
466
476
|
console.error(`\n⚠ ask() попытка ${attempt}/${this.askRetries} провалилась: ${e.message}`);
|
|
@@ -605,6 +615,12 @@ export class DeepSeekBrowser {
|
|
|
605
615
|
// Pause between sends. Applied ONLY to agent messages
|
|
606
616
|
// (tool-result, system-prompt) so we don't hit the rate limit.
|
|
607
617
|
// User input is sent without delay.
|
|
618
|
+
//
|
|
619
|
+
// The wait is INTERRUPTIBLE: Esc/Ctrl+C sets _abort, and we check it every
|
|
620
|
+
// 100ms instead of one long page.waitForTimeout(gap). Before, Esc pressed
|
|
621
|
+
// during this pause did nothing to the pause itself — the send was still
|
|
622
|
+
// delayed by the remaining seconds, and the stop request only took effect
|
|
623
|
+
// after the pause. This is the main "Esc does not cancel the pause" bug.
|
|
608
624
|
async _waitForSendSlot(agent) {
|
|
609
625
|
if (!agent)
|
|
610
626
|
return;
|
|
@@ -614,7 +630,21 @@ export class DeepSeekBrowser {
|
|
|
614
630
|
if (gap <= 0)
|
|
615
631
|
return;
|
|
616
632
|
console.error(theme.warn(`⏳ пауза ${Math.ceil(gap / 1000)}с перед отправкой`));
|
|
617
|
-
await this.
|
|
633
|
+
await this._sleepInterruptible(gap);
|
|
634
|
+
}
|
|
635
|
+
// Sleep in small slices so Esc/Ctrl+C can cancel the wait promptly.
|
|
636
|
+
// Returns true if the sleep was cut short by _abort.
|
|
637
|
+
async _sleepInterruptible(ms) {
|
|
638
|
+
const step = 100;
|
|
639
|
+
let left = ms;
|
|
640
|
+
while (left > 0) {
|
|
641
|
+
if (this._abort)
|
|
642
|
+
return true;
|
|
643
|
+
const chunk = Math.min(step, left);
|
|
644
|
+
await this.page.waitForTimeout(chunk);
|
|
645
|
+
left -= chunk;
|
|
646
|
+
}
|
|
647
|
+
return this._abort;
|
|
618
648
|
}
|
|
619
649
|
// DEBUG: append ask() phases to ~/.zames/ask-debug.log so a stall can be
|
|
620
650
|
// diagnosed from the field (what the DOM/network looked like at each step).
|
|
@@ -630,7 +660,12 @@ export class DeepSeekBrowser {
|
|
|
630
660
|
}
|
|
631
661
|
async _askOnce(prompt, { timeout, agent, attachments = [], }) {
|
|
632
662
|
// We reset the abort flag ONLY at the very start of the send.
|
|
663
|
+
// _stopped (Esc for the WHOLE batch) is NOT reset here: it may have been
|
|
664
|
+
// set while a previous send was waiting in the pause. If it is set, the
|
|
665
|
+
// user asked to stop and we must not start a new generation at all.
|
|
633
666
|
this._abort = false;
|
|
667
|
+
if (this._stopped)
|
|
668
|
+
return '(прервано пользователем)';
|
|
634
669
|
const input = await this._findVisible(INPUT_SELECTORS, 10_000);
|
|
635
670
|
if (!input) {
|
|
636
671
|
throw new Error('Не найдено поле ввода. Запустите /debug-dom и поправьте INPUT_SELECTORS.');
|
|
@@ -638,6 +673,19 @@ export class DeepSeekBrowser {
|
|
|
638
673
|
const beforeText = await this._readLastAnswerTextClean().catch(() => '');
|
|
639
674
|
this._askDebug('SEND agent=' + agent + ' len=' + prompt.length + ' beforeLen=' + beforeText.length + ' beforeHead=' + JSON.stringify(beforeText.slice(0, 60)));
|
|
640
675
|
await this._waitForSendSlot(agent);
|
|
676
|
+
// Esc/Ctrl+C pressed during the pause — do not send anything.
|
|
677
|
+
if (this._abort)
|
|
678
|
+
return '(прервано пользователем)';
|
|
679
|
+
// The pause is over and we are really about to type/send: only NOW start
|
|
680
|
+
// the "working" indicator. The spinner used to be started by the caller
|
|
681
|
+
// BEFORE ask(), so it ran for the whole throttle pause and chat-opening
|
|
682
|
+
// phase with no generation in flight.
|
|
683
|
+
if (this.onSendStart) {
|
|
684
|
+
try {
|
|
685
|
+
this.onSendStart();
|
|
686
|
+
}
|
|
687
|
+
catch { }
|
|
688
|
+
}
|
|
641
689
|
this._netCapture = '';
|
|
642
690
|
this._netCaptureAt = 0;
|
|
643
691
|
// Attach files/images FIRST (before the text): the DeepSeek upload widget
|
package/dist/i18n.js
CHANGED
|
@@ -128,6 +128,7 @@ const CATALOG = {
|
|
|
128
128
|
'msg.new_chat': { ru: 'Создаю новый чат...', en: 'Creating a new chat...' },
|
|
129
129
|
'msg.new_chat_ok': { ru: 'Новый чат.', en: 'New chat.' },
|
|
130
130
|
'msg.resuming': { ru: 'Восстанавливаю сессию {id}...', en: 'Restoring session {id}...' },
|
|
131
|
+
'msg.input_locked': { ru: 'Идёт операция, ввод временно заблокирован…', en: 'Operation in progress, input is temporarily locked…' },
|
|
131
132
|
'msg.opening_chat': { ru: 'Открываю чат {id}...', en: 'Opening chat {id}...' },
|
|
132
133
|
'msg.chat_opened': { ru: 'Чат открыт.', en: 'Chat opened.' },
|
|
133
134
|
'msg.interactive': { ru: 'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.', en: 'Interactive mode. Enter a task. Commands — /help. Exit — /exit.' },
|
package/dist/index.js
CHANGED
|
@@ -714,9 +714,15 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
714
714
|
};
|
|
715
715
|
// Execute the task, then everything the user managed to type while it
|
|
716
716
|
// ran. The queue may be replenished right during draining.
|
|
717
|
+
//
|
|
718
|
+
// NOTE: we do NOT call ui.thinking() here. runAgentLoop() fires
|
|
719
|
+
// onThinking() right before the actual browser send (after the send
|
|
720
|
+
// pause, system-prompt, etc.), so the spinner only appears when a
|
|
721
|
+
// generation really starts. Calling it here made the spinner run for
|
|
722
|
+
// the whole pre-send phase (chat creation, throttle wait) with no
|
|
723
|
+
// generation in flight.
|
|
717
724
|
while (true) {
|
|
718
|
-
|
|
719
|
-
await mod.runAgentLoop({
|
|
725
|
+
const outcome = await mod.runAgentLoop({
|
|
720
726
|
browser,
|
|
721
727
|
tools,
|
|
722
728
|
task: next.text,
|
|
@@ -735,6 +741,16 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
735
741
|
debugLog: debug,
|
|
736
742
|
locale: currentLocale,
|
|
737
743
|
});
|
|
744
|
+
// The loop may end WITHOUT a model answer: an exhausted iteration
|
|
745
|
+
// limit or an ask() watchdog (the model stopped responding). In that
|
|
746
|
+
// case no assistant message was shown, and the operator saw the run
|
|
747
|
+
// just "stop" after a tool call with no explanation. Surface it.
|
|
748
|
+
if (outcome &&
|
|
749
|
+
(outcome.startsWith('Достигнут лимит итераций') ||
|
|
750
|
+
outcome.startsWith('ask() watchdog'))) {
|
|
751
|
+
ui.warning(outcome);
|
|
752
|
+
transcript?.log('agent_no_answer', { outcome });
|
|
753
|
+
}
|
|
738
754
|
// Aborted (Esc/Ctrl+C) — we don't start the next tasks from the queue
|
|
739
755
|
// and clear it, so "stop" really stops everything.
|
|
740
756
|
if (browser._stopped) {
|
|
@@ -770,6 +786,9 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
770
786
|
finally {
|
|
771
787
|
stopWatching();
|
|
772
788
|
ui.stop();
|
|
789
|
+
// Detach the send hook so a later browser.ask() outside this task cannot
|
|
790
|
+
// start a stale spinner.
|
|
791
|
+
browser.onSendStart = null;
|
|
773
792
|
}
|
|
774
793
|
}
|
|
775
794
|
// ---------- main ----------
|
|
@@ -1339,6 +1358,8 @@ async function main() {
|
|
|
1339
1358
|
}
|
|
1340
1359
|
if (['/new', '/clear', 'new'].includes(lower)) {
|
|
1341
1360
|
console.log(theme.system(t('msg.new_chat')));
|
|
1361
|
+
if (editor)
|
|
1362
|
+
editor.lock(t('msg.input_locked'));
|
|
1342
1363
|
try {
|
|
1343
1364
|
await browser.newChat();
|
|
1344
1365
|
freshChatNext = false;
|
|
@@ -1351,6 +1372,10 @@ async function main() {
|
|
|
1351
1372
|
catch (e) {
|
|
1352
1373
|
console.error(theme.error(t('msg.new_chat_error', { v: e.message })), e.message);
|
|
1353
1374
|
}
|
|
1375
|
+
finally {
|
|
1376
|
+
if (editor)
|
|
1377
|
+
editor.unlock();
|
|
1378
|
+
}
|
|
1354
1379
|
continue;
|
|
1355
1380
|
}
|
|
1356
1381
|
// ---------- Self-review ----------
|
|
@@ -1360,6 +1385,8 @@ async function main() {
|
|
|
1360
1385
|
const originalWorkdir = reviewMode
|
|
1361
1386
|
? reviewMode.originalWorkdir
|
|
1362
1387
|
: currentWorkdir;
|
|
1388
|
+
if (editor)
|
|
1389
|
+
editor.lock(t('msg.input_locked'));
|
|
1363
1390
|
try {
|
|
1364
1391
|
const result = await mod.selfReview({
|
|
1365
1392
|
browser,
|
|
@@ -1388,6 +1415,10 @@ async function main() {
|
|
|
1388
1415
|
if (debug)
|
|
1389
1416
|
console.error(e.stack);
|
|
1390
1417
|
}
|
|
1418
|
+
finally {
|
|
1419
|
+
if (editor)
|
|
1420
|
+
editor.unlock();
|
|
1421
|
+
}
|
|
1391
1422
|
continue;
|
|
1392
1423
|
}
|
|
1393
1424
|
if (lower === '/self-fix' || lower.startsWith('/self-fix ')) {
|
|
@@ -1513,6 +1544,8 @@ async function main() {
|
|
|
1513
1544
|
// ---------- Regular commands ----------
|
|
1514
1545
|
if (lower === '/chats') {
|
|
1515
1546
|
const spin = editor || mod.createSpinner(currentLocale);
|
|
1547
|
+
if (editor)
|
|
1548
|
+
editor.lock(t('msg.input_locked'));
|
|
1516
1549
|
spin.thinking();
|
|
1517
1550
|
try {
|
|
1518
1551
|
lastChats = await browser.listChats(30);
|
|
@@ -1533,6 +1566,10 @@ async function main() {
|
|
|
1533
1566
|
spin.stop();
|
|
1534
1567
|
console.error(theme.error(t('chats.fetch_error')), e.message);
|
|
1535
1568
|
}
|
|
1569
|
+
finally {
|
|
1570
|
+
if (editor)
|
|
1571
|
+
editor.unlock();
|
|
1572
|
+
}
|
|
1536
1573
|
continue;
|
|
1537
1574
|
}
|
|
1538
1575
|
if (lower === '/resume' || lower.startsWith('/resume ')) {
|
|
@@ -1556,6 +1593,8 @@ async function main() {
|
|
|
1556
1593
|
continue;
|
|
1557
1594
|
}
|
|
1558
1595
|
console.log(theme.system(t('chats.opening', { v: pick.title })));
|
|
1596
|
+
if (editor)
|
|
1597
|
+
editor.lock(t('msg.input_locked'));
|
|
1559
1598
|
try {
|
|
1560
1599
|
await browser.openChat(pick.id);
|
|
1561
1600
|
currentChatId = pick.id;
|
|
@@ -1571,6 +1610,10 @@ async function main() {
|
|
|
1571
1610
|
catch (e) {
|
|
1572
1611
|
console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
|
|
1573
1612
|
}
|
|
1613
|
+
finally {
|
|
1614
|
+
if (editor)
|
|
1615
|
+
editor.unlock();
|
|
1616
|
+
}
|
|
1574
1617
|
continue;
|
|
1575
1618
|
}
|
|
1576
1619
|
if (lower === '/chat') {
|
|
@@ -1608,6 +1651,8 @@ async function main() {
|
|
|
1608
1651
|
console.error(theme.error(t('sessions.resume_id_usage')));
|
|
1609
1652
|
continue;
|
|
1610
1653
|
}
|
|
1654
|
+
if (editor)
|
|
1655
|
+
editor.lock(t('msg.input_locked'));
|
|
1611
1656
|
try {
|
|
1612
1657
|
console.log(theme.system(t('msg.opening_chat', { id })));
|
|
1613
1658
|
await browser.openChat(id);
|
|
@@ -1622,6 +1667,10 @@ async function main() {
|
|
|
1622
1667
|
catch (e) {
|
|
1623
1668
|
console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
|
|
1624
1669
|
}
|
|
1670
|
+
finally {
|
|
1671
|
+
if (editor)
|
|
1672
|
+
editor.unlock();
|
|
1673
|
+
}
|
|
1625
1674
|
continue;
|
|
1626
1675
|
}
|
|
1627
1676
|
if (lower === '/pwd') {
|
package/dist/input.js
CHANGED
|
@@ -207,6 +207,11 @@ export class LineEditor {
|
|
|
207
207
|
// Clipboard callback: tries to read an image from the OS clipboard when the
|
|
208
208
|
// terminal itself sends no usable data (Ctrl+V, right-click, empty paste).
|
|
209
209
|
onClipboard;
|
|
210
|
+
// When locked, the editor ignores text input and Enter (submit). Used while
|
|
211
|
+
// a long operation runs (chat resume/open, /new, self-review): otherwise the
|
|
212
|
+
// user could type and send a message mid-operation, and it would be queued
|
|
213
|
+
// and sent right after the operation, breaking the restored session.
|
|
214
|
+
locked;
|
|
210
215
|
constructor({ prompt = '> ', commands = [] } = {}) {
|
|
211
216
|
this.promptStr = prompt;
|
|
212
217
|
this.buf = '';
|
|
@@ -236,6 +241,7 @@ export class LineEditor {
|
|
|
236
241
|
this._tmpDir = '';
|
|
237
242
|
this.onAttach = null;
|
|
238
243
|
this.onClipboard = null;
|
|
244
|
+
this.locked = false;
|
|
239
245
|
}
|
|
240
246
|
// Read the OS clipboard for an image and insert its marker. Used when the
|
|
241
247
|
// terminal sends no usable paste data (Ctrl+V / right-click / empty paste).
|
|
@@ -333,6 +339,19 @@ export class LineEditor {
|
|
|
333
339
|
this._writeBlock();
|
|
334
340
|
this._render();
|
|
335
341
|
}
|
|
342
|
+
// Lock input while a long operation runs (chat resume/open, /new,
|
|
343
|
+
// self-review). The line stays visible but text input and Enter are ignored,
|
|
344
|
+
// so a message typed mid-operation is not queued and sent afterwards,
|
|
345
|
+
// breaking the restored session. Ctrl+C/Ctrl+D still work so the user can
|
|
346
|
+
// abort.
|
|
347
|
+
lock(status) {
|
|
348
|
+
this.locked = true;
|
|
349
|
+
if (status)
|
|
350
|
+
this.setStatus(theme.dim(status));
|
|
351
|
+
}
|
|
352
|
+
unlock() {
|
|
353
|
+
this.locked = false;
|
|
354
|
+
}
|
|
336
355
|
setPrompt(str) {
|
|
337
356
|
this.promptStr = str;
|
|
338
357
|
this._render();
|
|
@@ -481,6 +500,10 @@ export class LineEditor {
|
|
|
481
500
|
}
|
|
482
501
|
// ---------- input ----------
|
|
483
502
|
_submit() {
|
|
503
|
+
if (this.locked) {
|
|
504
|
+
this._render();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
484
507
|
const display = this.buf;
|
|
485
508
|
if (!display.trim()) {
|
|
486
509
|
this._render();
|
|
@@ -716,6 +739,25 @@ export class LineEditor {
|
|
|
716
739
|
}
|
|
717
740
|
_handle(data) {
|
|
718
741
|
let s = data.toString('utf-8');
|
|
742
|
+
// While locked (a long operation is running), swallow all input except
|
|
743
|
+
// Ctrl+C / Ctrl+D — so the user can still abort, but cannot type a message
|
|
744
|
+
// that would be queued and sent after the operation.
|
|
745
|
+
if (this.locked) {
|
|
746
|
+
if (s.length === 1) {
|
|
747
|
+
const c = s.charCodeAt(0);
|
|
748
|
+
if (c === 3) {
|
|
749
|
+
if (this.onCtrlC)
|
|
750
|
+
this.onCtrlC();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (c === 4) {
|
|
754
|
+
if (this.onCtrlC)
|
|
755
|
+
this.onCtrlC();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
719
761
|
if (!this._inPaste && s === ESC) {
|
|
720
762
|
if (this.onEscape)
|
|
721
763
|
this.onEscape();
|