@code4bug/jarvis-agent 1.3.1 → 1.3.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/components/MultilineInput.js +15 -1
- package/dist/core/WorkerBridge.d.ts +3 -0
- package/dist/core/WorkerBridge.js +75 -16
- package/dist/core/query.js +68 -10
- package/dist/hooks/useTerminalCursorSync.d.ts +4 -1
- package/dist/hooks/useTerminalCursorSync.js +6 -4
- package/dist/screens/repl.js +37 -7
- package/dist/terminal/cursor.d.ts +1 -1
- package/dist/terminal/cursor.js +4 -3
- package/package.json +1 -1
|
@@ -18,6 +18,8 @@ const IME_COMPOSE_WINDOW_MS = 80;
|
|
|
18
18
|
const isAsciiLowerAlpha = (s) => /^[a-z]+$/.test(s);
|
|
19
19
|
/** 判断字符串是否包含非 ASCII 字符(中文、日文等 CJK 字符) */
|
|
20
20
|
const hasNonAscii = (s) => /[^\x00-\x7F]/.test(s);
|
|
21
|
+
const charDisplayWidth = (ch) => /[^\u0000-\u00ff]/.test(ch) ? 2 : 1;
|
|
22
|
+
const textDisplayWidth = (text) => Array.from(text).reduce((sum, ch) => sum + charDisplayWidth(ch), 0);
|
|
21
23
|
/**
|
|
22
24
|
* 多行文本输入组件,支持光标移动和粘贴折叠。
|
|
23
25
|
*
|
|
@@ -289,7 +291,19 @@ export default function MultilineInput({ value, onChange, onSubmit, onUpArrow, o
|
|
|
289
291
|
clearImeBufferWithInsertedLenRollback,
|
|
290
292
|
});
|
|
291
293
|
useInput(() => { }, { isActive });
|
|
292
|
-
|
|
294
|
+
const lines = value.length > 0 ? value.split('\n') : [''];
|
|
295
|
+
const { row: cursorRow, col: cursorCol } = getCursorRowCol(value, cursor);
|
|
296
|
+
const activeLine = lines[cursorRow] ?? '';
|
|
297
|
+
const beforeCursor = activeLine.slice(0, cursorCol);
|
|
298
|
+
const cursorColumn = 3 + textDisplayWidth(beforeCursor);
|
|
299
|
+
useTerminalCursorSync({
|
|
300
|
+
showCursor,
|
|
301
|
+
isActive,
|
|
302
|
+
rowsBelow,
|
|
303
|
+
cursorRow,
|
|
304
|
+
rowsInInput: lines.length,
|
|
305
|
+
cursorColumn,
|
|
306
|
+
});
|
|
293
307
|
return (_jsx(InputTextView, { value: value, cursor: cursor, placeholder: placeholder, showCursor: showCursor, onResetPastedChunks: () => {
|
|
294
308
|
if (pasteCountRef.current > 0) {
|
|
295
309
|
pasteCountRef.current = 0;
|
|
@@ -2,6 +2,9 @@ import { TranscriptMessage } from '../types/index.js';
|
|
|
2
2
|
import { EngineCallbacks } from './QueryEngine.js';
|
|
3
3
|
export declare class WorkerBridge {
|
|
4
4
|
private worker;
|
|
5
|
+
private currentRun;
|
|
6
|
+
private finalizeRun;
|
|
7
|
+
private buildAbortedTranscript;
|
|
5
8
|
/** 在独立 Worker 线程中执行查询,返回更新后的 transcript */
|
|
6
9
|
run(userInput: string, transcript: TranscriptMessage[], callbacks: EngineCallbacks, options?: {
|
|
7
10
|
includeUserProfile?: boolean;
|
|
@@ -36,12 +36,61 @@ await tsImport(workerData.__workerFile, pathToFileURL(workerData.__workerFile).h
|
|
|
36
36
|
}
|
|
37
37
|
export class WorkerBridge {
|
|
38
38
|
worker = null;
|
|
39
|
+
currentRun = null;
|
|
40
|
+
finalizeRun(worker, action, payload) {
|
|
41
|
+
const run = this.currentRun;
|
|
42
|
+
if (!run || run.worker !== worker || run.settled)
|
|
43
|
+
return;
|
|
44
|
+
run.settled = true;
|
|
45
|
+
if (run.abortTimer) {
|
|
46
|
+
clearTimeout(run.abortTimer);
|
|
47
|
+
run.abortTimer = null;
|
|
48
|
+
}
|
|
49
|
+
this.currentRun = null;
|
|
50
|
+
this.worker = null;
|
|
51
|
+
worker.terminate().catch(() => { });
|
|
52
|
+
if (action === 'resolve') {
|
|
53
|
+
run.resolve(payload);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
run.reject(payload);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
buildAbortedTranscript(transcript, userInput) {
|
|
60
|
+
const abortNotice = '[系统提示] 用户中断了上一轮回复(按下 ESC)。上一条助手消息可能不完整,请在后续回复中注意这一点。';
|
|
61
|
+
const nextTranscript = [...transcript];
|
|
62
|
+
const lastMessage = nextTranscript[nextTranscript.length - 1];
|
|
63
|
+
if (userInput.trim()) {
|
|
64
|
+
const hasSameTrailingUserInput = lastMessage?.role === 'user' && lastMessage.content === userInput;
|
|
65
|
+
if (!hasSameTrailingUserInput) {
|
|
66
|
+
nextTranscript.push({ role: 'user', content: userInput });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
nextTranscript.push({ role: 'user', content: abortNotice });
|
|
70
|
+
return nextTranscript;
|
|
71
|
+
}
|
|
39
72
|
/** 在独立 Worker 线程中执行查询,返回更新后的 transcript */
|
|
40
73
|
run(userInput, transcript, callbacks, options) {
|
|
41
74
|
return new Promise((resolve, reject) => {
|
|
42
75
|
const workerTsPath = path.join(__dirname, 'queryWorker.ts');
|
|
43
76
|
const worker = createWorker(workerTsPath);
|
|
44
77
|
this.worker = worker;
|
|
78
|
+
this.currentRun = {
|
|
79
|
+
worker,
|
|
80
|
+
resolve,
|
|
81
|
+
reject,
|
|
82
|
+
transcript,
|
|
83
|
+
userInput,
|
|
84
|
+
callbacks,
|
|
85
|
+
settled: false,
|
|
86
|
+
abortTimer: null,
|
|
87
|
+
lastLoopState: {
|
|
88
|
+
iteration: 0,
|
|
89
|
+
maxIterations: 0,
|
|
90
|
+
isRunning: true,
|
|
91
|
+
aborted: false,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
45
94
|
logInfo('worker_bridge.run.start', {
|
|
46
95
|
inputLength: userInput.length,
|
|
47
96
|
transcriptLength: transcript.length,
|
|
@@ -61,6 +110,9 @@ export class WorkerBridge {
|
|
|
61
110
|
callbacks.onClearStreamText?.();
|
|
62
111
|
break;
|
|
63
112
|
case 'loop_state':
|
|
113
|
+
if (this.currentRun?.worker === worker) {
|
|
114
|
+
this.currentRun.lastLoopState = msg.state;
|
|
115
|
+
}
|
|
64
116
|
callbacks.onLoopStateChange(msg.state);
|
|
65
117
|
break;
|
|
66
118
|
case 'session_update':
|
|
@@ -146,31 +198,25 @@ export class WorkerBridge {
|
|
|
146
198
|
break;
|
|
147
199
|
}
|
|
148
200
|
case 'done':
|
|
149
|
-
this.worker = null;
|
|
150
|
-
worker.terminate();
|
|
151
201
|
logInfo('worker_bridge.run.done', {
|
|
152
202
|
transcriptLength: msg.transcript.length,
|
|
153
203
|
});
|
|
154
|
-
resolve
|
|
204
|
+
this.finalizeRun(worker, 'resolve', msg.transcript);
|
|
155
205
|
break;
|
|
156
206
|
case 'error':
|
|
157
|
-
this.worker = null;
|
|
158
|
-
worker.terminate();
|
|
159
207
|
logError('worker_bridge.run.error', msg.message);
|
|
160
|
-
reject
|
|
208
|
+
this.finalizeRun(worker, 'reject', new Error(msg.message));
|
|
161
209
|
break;
|
|
162
210
|
}
|
|
163
211
|
});
|
|
164
212
|
worker.on('error', (err) => {
|
|
165
|
-
this.worker = null;
|
|
166
213
|
logError('worker_bridge.worker_error', err);
|
|
167
|
-
reject
|
|
214
|
+
this.finalizeRun(worker, 'reject', err);
|
|
168
215
|
});
|
|
169
216
|
worker.on('exit', (code) => {
|
|
170
|
-
if (code !== 0 && this.worker) {
|
|
171
|
-
this.worker = null;
|
|
217
|
+
if (code !== 0 && this.currentRun?.worker === worker && !this.currentRun.settled) {
|
|
172
218
|
logError('worker_bridge.worker_exit_abnormal', undefined, { code });
|
|
173
|
-
reject
|
|
219
|
+
this.finalizeRun(worker, 'reject', new Error(`Worker 异常退出,code=${code}`));
|
|
174
220
|
}
|
|
175
221
|
});
|
|
176
222
|
// 启动执行
|
|
@@ -180,10 +226,23 @@ export class WorkerBridge {
|
|
|
180
226
|
}
|
|
181
227
|
/** 向 Worker 发送中断信号 */
|
|
182
228
|
abort() {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
229
|
+
const run = this.currentRun;
|
|
230
|
+
if (!run || run.settled)
|
|
231
|
+
return;
|
|
232
|
+
logWarn('worker_bridge.abort_forwarded');
|
|
233
|
+
const msg = { type: 'abort' };
|
|
234
|
+
run.worker.postMessage(msg);
|
|
235
|
+
run.callbacks.onClearStreamText?.();
|
|
236
|
+
run.callbacks.onLoopStateChange({
|
|
237
|
+
...run.lastLoopState,
|
|
238
|
+
isRunning: false,
|
|
239
|
+
aborted: true,
|
|
240
|
+
});
|
|
241
|
+
run.abortTimer = setTimeout(() => {
|
|
242
|
+
if (!this.currentRun || this.currentRun.worker !== run.worker || this.currentRun.settled)
|
|
243
|
+
return;
|
|
244
|
+
logWarn('worker_bridge.abort_force_resolve');
|
|
245
|
+
this.finalizeRun(run.worker, 'resolve', this.buildAbortedTranscript(run.transcript, run.userInput));
|
|
246
|
+
}, 120);
|
|
188
247
|
}
|
|
189
248
|
}
|
package/dist/core/query.js
CHANGED
|
@@ -352,21 +352,59 @@ function canRunInParallelDirect(calls) {
|
|
|
352
352
|
/** 在独立 Worker 线程中执行单个工具,返回结果字符串 */
|
|
353
353
|
function runToolInWorker(tc, abortSignal) {
|
|
354
354
|
return new Promise((resolve, reject) => {
|
|
355
|
+
let settled = false;
|
|
356
|
+
let abortForwarded = false;
|
|
357
|
+
let abortPollTimer = null;
|
|
358
|
+
let forceTerminateTimer = null;
|
|
359
|
+
const cleanup = () => {
|
|
360
|
+
if (abortPollTimer !== null) {
|
|
361
|
+
clearInterval(abortPollTimer);
|
|
362
|
+
abortPollTimer = null;
|
|
363
|
+
}
|
|
364
|
+
if (forceTerminateTimer !== null) {
|
|
365
|
+
clearTimeout(forceTerminateTimer);
|
|
366
|
+
forceTerminateTimer = null;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const safeResolve = (result) => {
|
|
370
|
+
if (settled)
|
|
371
|
+
return;
|
|
372
|
+
settled = true;
|
|
373
|
+
cleanup();
|
|
374
|
+
worker.terminate().catch(() => { });
|
|
375
|
+
resolve(result);
|
|
376
|
+
};
|
|
377
|
+
const safeReject = (err) => {
|
|
378
|
+
if (settled)
|
|
379
|
+
return;
|
|
380
|
+
settled = true;
|
|
381
|
+
cleanup();
|
|
382
|
+
worker.terminate().catch(() => { });
|
|
383
|
+
reject(err);
|
|
384
|
+
};
|
|
355
385
|
const isTsx = __filename.endsWith('.ts');
|
|
356
386
|
const workerScript = isTsx
|
|
357
387
|
? `
|
|
358
388
|
import { tsImport } from 'tsx/esm/api';
|
|
359
389
|
import { workerData, parentPort } from 'worker_threads';
|
|
360
390
|
import { pathToFileURL } from 'url';
|
|
391
|
+
const abortSignal = { aborted: Boolean(workerData.abortSignal?.aborted) };
|
|
392
|
+
parentPort.on('message', (msg) => {
|
|
393
|
+
if (msg?.type === 'abort') abortSignal.aborted = true;
|
|
394
|
+
});
|
|
361
395
|
const mod = await tsImport(workerData.__file, pathToFileURL(workerData.__file).href);
|
|
362
|
-
const result = await mod.runToolDirect(workerData.tc,
|
|
363
|
-
parentPort.postMessage({ result });
|
|
396
|
+
const result = await mod.runToolDirect(workerData.tc, abortSignal);
|
|
397
|
+
parentPort.postMessage({ type: 'result', result });
|
|
364
398
|
`
|
|
365
399
|
: `
|
|
366
400
|
import { runToolDirect } from '${__filename.replace(/\.ts$/, '.js')}';
|
|
367
401
|
import { workerData, parentPort } from 'worker_threads';
|
|
368
|
-
const
|
|
369
|
-
parentPort.
|
|
402
|
+
const abortSignal = { aborted: Boolean(workerData.abortSignal?.aborted) };
|
|
403
|
+
parentPort.on('message', (msg) => {
|
|
404
|
+
if (msg?.type === 'abort') abortSignal.aborted = true;
|
|
405
|
+
});
|
|
406
|
+
const result = await runToolDirect(workerData.tc, abortSignal);
|
|
407
|
+
parentPort.postMessage({ type: 'result', result });
|
|
370
408
|
`;
|
|
371
409
|
const worker = new Worker(workerScript, {
|
|
372
410
|
eval: true,
|
|
@@ -376,17 +414,37 @@ parentPort.postMessage({ result });
|
|
|
376
414
|
abortSignal: { aborted: abortSignal.aborted },
|
|
377
415
|
},
|
|
378
416
|
});
|
|
417
|
+
abortPollTimer = setInterval(() => {
|
|
418
|
+
if (!abortSignal.aborted || abortForwarded || settled)
|
|
419
|
+
return;
|
|
420
|
+
abortForwarded = true;
|
|
421
|
+
logWarn('tool.worker.abort_forwarded', { toolName: tc.name });
|
|
422
|
+
worker.postMessage({ type: 'abort' });
|
|
423
|
+
forceTerminateTimer = setTimeout(() => {
|
|
424
|
+
if (settled)
|
|
425
|
+
return;
|
|
426
|
+
logWarn('tool.worker.force_terminated', { toolName: tc.name });
|
|
427
|
+
safeResolve('(工具执行被中断)');
|
|
428
|
+
}, 1500);
|
|
429
|
+
}, 50);
|
|
379
430
|
worker.on('message', (msg) => {
|
|
380
|
-
|
|
381
|
-
|
|
431
|
+
if (msg.type === 'result') {
|
|
432
|
+
safeResolve(msg.result ?? '');
|
|
433
|
+
}
|
|
382
434
|
});
|
|
383
435
|
worker.on('error', (err) => {
|
|
384
|
-
|
|
385
|
-
reject(err);
|
|
436
|
+
safeReject(err);
|
|
386
437
|
});
|
|
387
438
|
worker.on('exit', (code) => {
|
|
388
|
-
if (
|
|
389
|
-
|
|
439
|
+
if (settled)
|
|
440
|
+
return;
|
|
441
|
+
if (abortSignal.aborted) {
|
|
442
|
+
safeResolve('(工具执行被中断)');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (code !== 0) {
|
|
446
|
+
safeReject(new Error(`工具 Worker 异常退出 code=${code}`));
|
|
447
|
+
}
|
|
390
448
|
});
|
|
391
449
|
});
|
|
392
450
|
}
|
|
@@ -2,7 +2,10 @@ interface UseTerminalCursorSyncOptions {
|
|
|
2
2
|
showCursor: boolean;
|
|
3
3
|
isActive: boolean;
|
|
4
4
|
rowsBelow: number;
|
|
5
|
+
cursorRow?: number;
|
|
6
|
+
rowsInInput?: number;
|
|
7
|
+
cursorColumn?: number;
|
|
5
8
|
delayMs?: number;
|
|
6
9
|
}
|
|
7
|
-
export declare function useTerminalCursorSync({ showCursor, isActive, rowsBelow, delayMs, }: UseTerminalCursorSyncOptions): void;
|
|
10
|
+
export declare function useTerminalCursorSync({ showCursor, isActive, rowsBelow, cursorRow, rowsInInput, cursorColumn, delayMs, }: UseTerminalCursorSyncOptions): void;
|
|
8
11
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { moveCursorToColumn, relocateCursorToInputLine } from '../terminal/cursor.js';
|
|
3
|
-
export function useTerminalCursorSync({ showCursor, isActive, rowsBelow, delayMs = 80, }) {
|
|
3
|
+
export function useTerminalCursorSync({ showCursor, isActive, rowsBelow, cursorRow = 0, rowsInInput = 1, cursorColumn = 3, delayMs = 80, }) {
|
|
4
4
|
const cursorRelocTimerRef = useRef(null);
|
|
5
5
|
const lastCursorCommandRef = useRef('');
|
|
6
6
|
useEffect(() => {
|
|
@@ -27,11 +27,13 @@ export function useTerminalCursorSync({ showCursor, isActive, rowsBelow, delayMs
|
|
|
27
27
|
else {
|
|
28
28
|
cursorRelocTimerRef.current = setTimeout(() => {
|
|
29
29
|
cursorRelocTimerRef.current = null;
|
|
30
|
-
const
|
|
30
|
+
const extraRowsUp = Math.max(rowsInInput - 1 - cursorRow, 0);
|
|
31
|
+
const safeColumn = Math.max(cursorColumn, 1);
|
|
32
|
+
const commandKey = `input:${rowsBelow}:${extraRowsUp}:${safeColumn}`;
|
|
31
33
|
if (lastCursorCommandRef.current === commandKey)
|
|
32
34
|
return;
|
|
33
35
|
lastCursorCommandRef.current = commandKey;
|
|
34
|
-
relocateCursorToInputLine(rowsBelow,
|
|
36
|
+
relocateCursorToInputLine(rowsBelow, safeColumn, extraRowsUp);
|
|
35
37
|
}, delayMs);
|
|
36
38
|
}
|
|
37
39
|
return () => {
|
|
@@ -40,5 +42,5 @@ export function useTerminalCursorSync({ showCursor, isActive, rowsBelow, delayMs
|
|
|
40
42
|
cursorRelocTimerRef.current = null;
|
|
41
43
|
}
|
|
42
44
|
};
|
|
43
|
-
}, [showCursor, isActive, rowsBelow, delayMs]);
|
|
45
|
+
}, [showCursor, isActive, rowsBelow, cursorRow, rowsInInput, cursorColumn, delayMs]);
|
|
44
46
|
}
|
package/dist/screens/repl.js
CHANGED
|
@@ -39,6 +39,7 @@ export default function REPL() {
|
|
|
39
39
|
const [placeholder, setPlaceholder] = useState('');
|
|
40
40
|
const [activeAgents, setActiveAgents] = useState(getActiveAgentCount());
|
|
41
41
|
const lastEscRef = useRef(0);
|
|
42
|
+
const abortRequestedRef = useRef(false);
|
|
42
43
|
const lastAbortNoticeRef = useRef(0);
|
|
43
44
|
const sessionRef = useRef({
|
|
44
45
|
id: '', messages: [], createdAt: 0, updatedAt: 0, totalTokens: 0, totalCost: 0,
|
|
@@ -67,6 +68,34 @@ export default function REPL() {
|
|
|
67
68
|
abortHint: '推理已中断(ESC)',
|
|
68
69
|
}]);
|
|
69
70
|
}, []);
|
|
71
|
+
const markPendingMessagesAborted = useCallback(() => {
|
|
72
|
+
setMessages((prev) => prev
|
|
73
|
+
.filter((msg) => !(msg.type === 'thinking' && msg.status === 'pending'))
|
|
74
|
+
.map((msg) => {
|
|
75
|
+
if (msg.status !== 'pending')
|
|
76
|
+
return msg;
|
|
77
|
+
if (msg.type === 'tool_exec') {
|
|
78
|
+
return {
|
|
79
|
+
...msg,
|
|
80
|
+
status: 'aborted',
|
|
81
|
+
content: `${msg.toolName || '工具'} 已中断`,
|
|
82
|
+
abortHint: msg.abortHint ?? '命令已中断(ESC)',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return msg;
|
|
86
|
+
}));
|
|
87
|
+
finishThinking();
|
|
88
|
+
clearStream();
|
|
89
|
+
}, [clearStream, finishThinking]);
|
|
90
|
+
const requestAbort = useCallback(() => {
|
|
91
|
+
if (!isProcessing || !engineRef.current || abortRequestedRef.current)
|
|
92
|
+
return;
|
|
93
|
+
abortRequestedRef.current = true;
|
|
94
|
+
logWarn('ui.abort_by_escape');
|
|
95
|
+
markPendingMessagesAborted();
|
|
96
|
+
appendAbortNotice();
|
|
97
|
+
engineRef.current.abort();
|
|
98
|
+
}, [appendAbortNotice, isProcessing, markPendingMessagesAborted]);
|
|
70
99
|
// ===== 新会话逻辑 =====
|
|
71
100
|
const handleNewSession = useCallback(() => {
|
|
72
101
|
logInfo('ui.new_session');
|
|
@@ -78,6 +107,7 @@ export default function REPL() {
|
|
|
78
107
|
clearStream();
|
|
79
108
|
setLoopState(null);
|
|
80
109
|
setIsProcessing(false);
|
|
110
|
+
abortRequestedRef.current = false;
|
|
81
111
|
setShowWelcome(true);
|
|
82
112
|
resetTokens();
|
|
83
113
|
generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
|
|
@@ -145,6 +175,7 @@ export default function REPL() {
|
|
|
145
175
|
startStreamTimer();
|
|
146
176
|
}
|
|
147
177
|
else {
|
|
178
|
+
abortRequestedRef.current = false;
|
|
148
179
|
stopAll();
|
|
149
180
|
}
|
|
150
181
|
},
|
|
@@ -227,6 +258,7 @@ export default function REPL() {
|
|
|
227
258
|
setShowWelcome(false);
|
|
228
259
|
pushHistory(trimmed);
|
|
229
260
|
clearStream();
|
|
261
|
+
abortRequestedRef.current = false;
|
|
230
262
|
await engineRef.current.handleQuery(prompt, callbacks);
|
|
231
263
|
return;
|
|
232
264
|
}
|
|
@@ -285,6 +317,7 @@ export default function REPL() {
|
|
|
285
317
|
pushHistory(trimmed);
|
|
286
318
|
setInput('');
|
|
287
319
|
clearStream();
|
|
320
|
+
abortRequestedRef.current = false;
|
|
288
321
|
await engineRef.current.handleQuery(trimmed, callbacks);
|
|
289
322
|
}, [isProcessing, pushHistory, clearStream, slashMenu, handleNewSession]);
|
|
290
323
|
// ===== 输入处理 =====
|
|
@@ -380,9 +413,8 @@ export default function REPL() {
|
|
|
380
413
|
handleCtrlC();
|
|
381
414
|
return;
|
|
382
415
|
}
|
|
383
|
-
if (key.escape
|
|
384
|
-
|
|
385
|
-
engineRef.current.abort();
|
|
416
|
+
if (key.escape) {
|
|
417
|
+
requestAbort();
|
|
386
418
|
return;
|
|
387
419
|
}
|
|
388
420
|
if (key.ctrl && ch === 'o') {
|
|
@@ -418,10 +450,8 @@ export default function REPL() {
|
|
|
418
450
|
return;
|
|
419
451
|
}
|
|
420
452
|
if (key.escape) {
|
|
421
|
-
if (isProcessing
|
|
422
|
-
|
|
423
|
-
appendAbortNotice();
|
|
424
|
-
engineRef.current.abort();
|
|
453
|
+
if (isProcessing) {
|
|
454
|
+
requestAbort();
|
|
425
455
|
}
|
|
426
456
|
else if (input.length > 0) {
|
|
427
457
|
const now = Date.now();
|
|
@@ -3,4 +3,4 @@ export declare function showTerminalCursor(): void;
|
|
|
3
3
|
export declare function enableBracketedPaste(): void;
|
|
4
4
|
export declare function disableBracketedPaste(): void;
|
|
5
5
|
export declare function moveCursorToColumn(column: number): void;
|
|
6
|
-
export declare function relocateCursorToInputLine(rowsBelow: number, column: number): void;
|
|
6
|
+
export declare function relocateCursorToInputLine(rowsBelow: number, column: number, extraRowsUp?: number): void;
|
package/dist/terminal/cursor.js
CHANGED
|
@@ -14,8 +14,9 @@ export function disableBracketedPaste() {
|
|
|
14
14
|
export function moveCursorToColumn(column) {
|
|
15
15
|
process.stdout.write(`${ESC}${column}G`);
|
|
16
16
|
}
|
|
17
|
-
export function relocateCursorToInputLine(rowsBelow, column) {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
17
|
+
export function relocateCursorToInputLine(rowsBelow, column, extraRowsUp = 0) {
|
|
18
|
+
const totalRowsUp = Math.max(rowsBelow + extraRowsUp, 0);
|
|
19
|
+
const up = totalRowsUp > 0 ? `${ESC}${totalRowsUp}A` : '';
|
|
20
|
+
const down = totalRowsUp > 0 ? `${ESC}${totalRowsUp}B` : '';
|
|
20
21
|
process.stdout.write(`${up}${ESC}${column}G${down}`);
|
|
21
22
|
}
|