@code4bug/jarvis-agent 1.2.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/README.md +1 -1
- package/dist/agents/jarvis.md +1 -1
- package/dist/components/AnimatedStatusText.d.ts +10 -0
- package/dist/components/AnimatedStatusText.js +17 -0
- package/dist/components/ComposerPane.d.ts +25 -0
- package/dist/components/ComposerPane.js +10 -0
- package/dist/components/FooterPane.d.ts +9 -0
- package/dist/components/FooterPane.js +22 -0
- package/dist/components/InputTextView.d.ts +11 -0
- package/dist/components/InputTextView.js +44 -0
- package/dist/components/MessageList.d.ts +9 -0
- package/dist/components/MessageList.js +8 -0
- package/dist/components/MessageViewport.d.ts +21 -0
- package/dist/components/MessageViewport.js +11 -0
- package/dist/components/MultilineInput.js +75 -343
- package/dist/components/StreamingDraft.d.ts +11 -0
- package/dist/components/StreamingDraft.js +14 -0
- package/dist/components/inputEditing.d.ts +20 -0
- package/dist/components/inputEditing.js +48 -0
- 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/useMultilineInputStream.d.ts +17 -0
- package/dist/hooks/useMultilineInputStream.js +141 -0
- package/dist/hooks/useTerminalCursorSync.d.ts +11 -0
- package/dist/hooks/useTerminalCursorSync.js +46 -0
- package/dist/hooks/useTerminalSize.d.ts +7 -0
- package/dist/hooks/useTerminalSize.js +21 -0
- package/dist/screens/repl.js +74 -33
- package/dist/services/api/llm.js +3 -1
- package/dist/skills/index.js +10 -3
- package/dist/terminal/cursor.d.ts +6 -0
- package/dist/terminal/cursor.js +22 -0
- package/dist/tools/writeFile.js +63 -2
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface UseMultilineInputStreamOptions {
|
|
2
|
+
stdin?: NodeJS.ReadStream;
|
|
3
|
+
isActive: boolean;
|
|
4
|
+
pasteDetectWindowMs: number;
|
|
5
|
+
imeComposeWindowMs: number;
|
|
6
|
+
isAsciiLowerAlpha: (text: string) => boolean;
|
|
7
|
+
hasNonAscii: (text: string) => boolean;
|
|
8
|
+
insertPaste: (text: string) => void;
|
|
9
|
+
handleNormalInput: (raw: string) => void;
|
|
10
|
+
commitImeBuffer: () => void;
|
|
11
|
+
flushImeBuffer: () => void;
|
|
12
|
+
appendImeBuffer: (text: string) => void;
|
|
13
|
+
getImeBuffer: () => string;
|
|
14
|
+
clearImeBufferWithInsertedLenRollback: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function useMultilineInputStream({ stdin, isActive, pasteDetectWindowMs, imeComposeWindowMs, isAsciiLowerAlpha, hasNonAscii, insertPaste, handleNormalInput, commitImeBuffer, flushImeBuffer, appendImeBuffer, getImeBuffer, clearImeBufferWithInsertedLenRollback, }: UseMultilineInputStreamOptions): void;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
export function useMultilineInputStream({ stdin, isActive, pasteDetectWindowMs, imeComposeWindowMs, isAsciiLowerAlpha, hasNonAscii, insertPaste, handleNormalInput, commitImeBuffer, flushImeBuffer, appendImeBuffer, getImeBuffer, clearImeBufferWithInsertedLenRollback, }) {
|
|
3
|
+
const pasteBufferRef = useRef(null);
|
|
4
|
+
const batchBufferRef = useRef('');
|
|
5
|
+
const batchTimerRef = useRef(null);
|
|
6
|
+
const imeTimerRef = useRef(null);
|
|
7
|
+
const escTimerRef = useRef(null);
|
|
8
|
+
const ESC_WAIT_MS = 50;
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!stdin || !isActive)
|
|
11
|
+
return;
|
|
12
|
+
const flushBatchBuffer = () => {
|
|
13
|
+
const buf = batchBufferRef.current;
|
|
14
|
+
batchBufferRef.current = '';
|
|
15
|
+
batchTimerRef.current = null;
|
|
16
|
+
if (!buf)
|
|
17
|
+
return;
|
|
18
|
+
const cleaned = buf.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
19
|
+
if (cleaned.includes('\n') && cleaned.length > 1) {
|
|
20
|
+
insertPaste(cleaned);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (isAsciiLowerAlpha(buf)) {
|
|
24
|
+
appendImeBuffer(buf);
|
|
25
|
+
if (imeTimerRef.current !== null) {
|
|
26
|
+
clearTimeout(imeTimerRef.current);
|
|
27
|
+
}
|
|
28
|
+
imeTimerRef.current = setTimeout(flushImeBuffer, imeComposeWindowMs);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (hasNonAscii(buf) && getImeBuffer().length > 0) {
|
|
32
|
+
if (imeTimerRef.current !== null) {
|
|
33
|
+
clearTimeout(imeTimerRef.current);
|
|
34
|
+
imeTimerRef.current = null;
|
|
35
|
+
}
|
|
36
|
+
clearImeBufferWithInsertedLenRollback();
|
|
37
|
+
handleNormalInput(buf);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (getImeBuffer().length > 0) {
|
|
41
|
+
commitImeBuffer();
|
|
42
|
+
}
|
|
43
|
+
handleNormalInput(buf);
|
|
44
|
+
};
|
|
45
|
+
const onData = (data) => {
|
|
46
|
+
const raw = data.toString('utf-8');
|
|
47
|
+
if (raw.includes('\x1B[200~')) {
|
|
48
|
+
const startIdx = raw.indexOf('\x1B[200~') + 6;
|
|
49
|
+
const endIdx = raw.indexOf('\x1B[201~');
|
|
50
|
+
if (endIdx !== -1) {
|
|
51
|
+
insertPaste(raw.slice(startIdx, endIdx));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
pasteBufferRef.current = raw.slice(startIdx);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (pasteBufferRef.current !== null) {
|
|
59
|
+
const endIdx = raw.indexOf('\x1B[201~');
|
|
60
|
+
if (endIdx !== -1) {
|
|
61
|
+
pasteBufferRef.current += raw.slice(0, endIdx);
|
|
62
|
+
insertPaste(pasteBufferRef.current);
|
|
63
|
+
pasteBufferRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
pasteBufferRef.current += raw;
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (escTimerRef.current !== null) {
|
|
71
|
+
clearTimeout(escTimerRef.current);
|
|
72
|
+
escTimerRef.current = null;
|
|
73
|
+
if (raw === '\r' || raw === '\n') {
|
|
74
|
+
handleNormalInput('\x1B\r');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
handleNormalInput('\x1B');
|
|
78
|
+
}
|
|
79
|
+
const isSingleControl = raw === '\r' || raw === '\n' ||
|
|
80
|
+
raw === '\x7F' || raw === '\x08' ||
|
|
81
|
+
raw === '\t' || raw === '\x1B' ||
|
|
82
|
+
raw === '\x1B\r' || raw === '\x1B\n' ||
|
|
83
|
+
raw.startsWith('\x1B[') || raw.startsWith('\x1B]') || raw.startsWith('\x1BO');
|
|
84
|
+
if (raw === '\x03' || raw === '\x0F' || raw === '\x0C')
|
|
85
|
+
return;
|
|
86
|
+
if (raw === '\x1B') {
|
|
87
|
+
escTimerRef.current = setTimeout(() => {
|
|
88
|
+
escTimerRef.current = null;
|
|
89
|
+
handleNormalInput('\x1B');
|
|
90
|
+
}, ESC_WAIT_MS);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (isSingleControl && batchBufferRef.current === '') {
|
|
94
|
+
if (getImeBuffer().length > 0) {
|
|
95
|
+
commitImeBuffer();
|
|
96
|
+
}
|
|
97
|
+
handleNormalInput(raw);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (isSingleControl && batchBufferRef.current !== '') {
|
|
101
|
+
batchBufferRef.current += raw;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
batchBufferRef.current += raw;
|
|
105
|
+
if (batchTimerRef.current !== null) {
|
|
106
|
+
clearTimeout(batchTimerRef.current);
|
|
107
|
+
}
|
|
108
|
+
batchTimerRef.current = setTimeout(flushBatchBuffer, pasteDetectWindowMs);
|
|
109
|
+
};
|
|
110
|
+
stdin.prependListener('data', onData);
|
|
111
|
+
return () => {
|
|
112
|
+
stdin.off('data', onData);
|
|
113
|
+
if (batchTimerRef.current !== null) {
|
|
114
|
+
clearTimeout(batchTimerRef.current);
|
|
115
|
+
batchTimerRef.current = null;
|
|
116
|
+
}
|
|
117
|
+
if (escTimerRef.current !== null) {
|
|
118
|
+
clearTimeout(escTimerRef.current);
|
|
119
|
+
escTimerRef.current = null;
|
|
120
|
+
}
|
|
121
|
+
if (imeTimerRef.current !== null) {
|
|
122
|
+
clearTimeout(imeTimerRef.current);
|
|
123
|
+
imeTimerRef.current = null;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}, [
|
|
127
|
+
stdin,
|
|
128
|
+
isActive,
|
|
129
|
+
pasteDetectWindowMs,
|
|
130
|
+
imeComposeWindowMs,
|
|
131
|
+
isAsciiLowerAlpha,
|
|
132
|
+
hasNonAscii,
|
|
133
|
+
insertPaste,
|
|
134
|
+
handleNormalInput,
|
|
135
|
+
commitImeBuffer,
|
|
136
|
+
flushImeBuffer,
|
|
137
|
+
appendImeBuffer,
|
|
138
|
+
getImeBuffer,
|
|
139
|
+
clearImeBufferWithInsertedLenRollback,
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface UseTerminalCursorSyncOptions {
|
|
2
|
+
showCursor: boolean;
|
|
3
|
+
isActive: boolean;
|
|
4
|
+
rowsBelow: number;
|
|
5
|
+
cursorRow?: number;
|
|
6
|
+
rowsInInput?: number;
|
|
7
|
+
cursorColumn?: number;
|
|
8
|
+
delayMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function useTerminalCursorSync({ showCursor, isActive, rowsBelow, cursorRow, rowsInInput, cursorColumn, delayMs, }: UseTerminalCursorSyncOptions): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { moveCursorToColumn, relocateCursorToInputLine } from '../terminal/cursor.js';
|
|
3
|
+
export function useTerminalCursorSync({ showCursor, isActive, rowsBelow, cursorRow = 0, rowsInInput = 1, cursorColumn = 3, delayMs = 80, }) {
|
|
4
|
+
const cursorRelocTimerRef = useRef(null);
|
|
5
|
+
const lastCursorCommandRef = useRef('');
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
return () => {
|
|
8
|
+
if (cursorRelocTimerRef.current !== null) {
|
|
9
|
+
clearTimeout(cursorRelocTimerRef.current);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}, []);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (cursorRelocTimerRef.current !== null) {
|
|
15
|
+
clearTimeout(cursorRelocTimerRef.current);
|
|
16
|
+
}
|
|
17
|
+
if (!showCursor || !isActive) {
|
|
18
|
+
cursorRelocTimerRef.current = setTimeout(() => {
|
|
19
|
+
cursorRelocTimerRef.current = null;
|
|
20
|
+
const commandKey = 'col:1';
|
|
21
|
+
if (lastCursorCommandRef.current === commandKey)
|
|
22
|
+
return;
|
|
23
|
+
lastCursorCommandRef.current = commandKey;
|
|
24
|
+
moveCursorToColumn(1);
|
|
25
|
+
}, delayMs);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
cursorRelocTimerRef.current = setTimeout(() => {
|
|
29
|
+
cursorRelocTimerRef.current = null;
|
|
30
|
+
const extraRowsUp = Math.max(rowsInInput - 1 - cursorRow, 0);
|
|
31
|
+
const safeColumn = Math.max(cursorColumn, 1);
|
|
32
|
+
const commandKey = `input:${rowsBelow}:${extraRowsUp}:${safeColumn}`;
|
|
33
|
+
if (lastCursorCommandRef.current === commandKey)
|
|
34
|
+
return;
|
|
35
|
+
lastCursorCommandRef.current = commandKey;
|
|
36
|
+
relocateCursorToInputLine(rowsBelow, safeColumn, extraRowsUp);
|
|
37
|
+
}, delayMs);
|
|
38
|
+
}
|
|
39
|
+
return () => {
|
|
40
|
+
if (cursorRelocTimerRef.current !== null) {
|
|
41
|
+
clearTimeout(cursorRelocTimerRef.current);
|
|
42
|
+
cursorRelocTimerRef.current = null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}, [showCursor, isActive, rowsBelow, cursorRow, rowsInInput, cursorColumn, delayMs]);
|
|
46
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
/** 响应式终端尺寸 */
|
|
3
|
+
export function useTerminalSize() {
|
|
4
|
+
const [size, setSize] = useState(() => ({
|
|
5
|
+
width: process.stdout.columns || 80,
|
|
6
|
+
height: process.stdout.rows || 24,
|
|
7
|
+
}));
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const onResize = () => {
|
|
10
|
+
setSize({
|
|
11
|
+
width: process.stdout.columns || 80,
|
|
12
|
+
height: process.stdout.rows || 24,
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
process.stdout.on('resize', onResize);
|
|
16
|
+
return () => {
|
|
17
|
+
process.stdout.off('resize', onResize);
|
|
18
|
+
};
|
|
19
|
+
}, []);
|
|
20
|
+
return size;
|
|
21
|
+
}
|