@adhdev/daemon-core 0.6.56 → 0.6.58
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/index.d.ts +23 -0
- package/dist/index.js +423 -95
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/providers/_builtin/cli/aider-cli/scripts/1.0/parse_output.js +51 -3
- package/providers/_builtin/cli/claude-cli/provider.json +18 -6
- package/providers/_builtin/cli/claude-cli/scripts/1.0/detect_status.js +68 -16
- package/providers/_builtin/cli/claude-cli/scripts/1.0/parse_approval.js +81 -22
- package/providers/_builtin/cli/claude-cli/scripts/1.0/parse_output.js +347 -94
- package/providers/_builtin/cli/codex-cli/provider.json +2 -0
- package/providers/_builtin/cli/codex-cli/scripts/1.0/detect_status.js +44 -10
- package/providers/_builtin/cli/codex-cli/scripts/1.0/parse_approval.js +83 -7
- package/providers/_builtin/cli/codex-cli/scripts/1.0/parse_output.js +501 -47
- package/providers/_builtin/cli/cursor-cli/scripts/1.0/parse_output.js +1 -1
- package/providers/_builtin/cli/github-copilot-cli/scripts/1.0/parse_output.js +1 -1
- package/providers/_builtin/cli/goose-cli/scripts/1.0/parse_output.js +1 -1
- package/providers/_builtin/cli/opencode-cli/scripts/1.0/parse_output.js +1 -1
- package/providers/_builtin/ide/vscode/provider.json +5 -1
- package/providers/_builtin/ide/vscode/scripts/1.0/focus_editor.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/list_models.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/list_sessions.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/new_session.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/open_panel.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/read_chat.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/resolve_action.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/scripts.js +25 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/send_message.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/set_model.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/switch_session.js +1 -0
- package/providers/_builtin/registry.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +410 -65
- package/src/commands/chat-commands.ts +7 -1
- package/src/config/chat-history.ts +53 -1
- package/src/daemon/dev-server.ts +7 -9
- package/src/providers/cli-provider-instance.ts +10 -23
- package/src/providers/provider-instance.ts +1 -0
- package/src/providers/version-archive.ts +4 -1
|
@@ -57,6 +57,7 @@ export interface CliSessionStatus {
|
|
|
57
57
|
messages: CliChatMessage[];
|
|
58
58
|
workingDir: string;
|
|
59
59
|
activeModal: { message: string; buttons: string[] } | null;
|
|
60
|
+
terminalHistory?: string;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
/**
|
|
@@ -82,10 +83,19 @@ export interface CliScriptInput {
|
|
|
82
83
|
rawBuffer: string; // Raw PTY output (with ANSI)
|
|
83
84
|
recentBuffer: string; // Recent 1000 chars (ANSI-stripped)
|
|
84
85
|
screenText: string; // Current visible screen snapshot
|
|
86
|
+
terminalHistory?: string; // Rolling append-only terminal transcript
|
|
85
87
|
messages: CliChatMessage[]; // Previously parsed messages
|
|
86
88
|
partialResponse: string; // Current partial response being generated
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
interface TurnParseScope {
|
|
92
|
+
prompt: string;
|
|
93
|
+
startedAt: number;
|
|
94
|
+
bufferStart: number;
|
|
95
|
+
rawBufferStart: number;
|
|
96
|
+
terminalHistoryStart: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
export interface CliProviderModule {
|
|
90
100
|
type: string;
|
|
91
101
|
name: string;
|
|
@@ -93,6 +103,7 @@ export interface CliProviderModule {
|
|
|
93
103
|
binary: string;
|
|
94
104
|
sendDelayMs?: number;
|
|
95
105
|
sendKey?: string;
|
|
106
|
+
submitStrategy?: 'wait_for_echo' | 'immediate';
|
|
96
107
|
spawn: {
|
|
97
108
|
command: string;
|
|
98
109
|
args: string[];
|
|
@@ -193,6 +204,94 @@ function shSingleQuote(arg: string): string {
|
|
|
193
204
|
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
194
205
|
}
|
|
195
206
|
|
|
207
|
+
function estimatePromptDisplayLines(text: string, cols = 100): number {
|
|
208
|
+
const normalized = String(text || '').replace(/\r/g, '');
|
|
209
|
+
if (!normalized) return 1;
|
|
210
|
+
return normalized
|
|
211
|
+
.split('\n')
|
|
212
|
+
.reduce((sum, line) => sum + Math.max(1, Math.ceil(Math.max(1, line.length) / cols)), 0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function extractPromptRetrySnippet(text: string): string {
|
|
216
|
+
const lines = String(text || '')
|
|
217
|
+
.replace(/\r/g, '')
|
|
218
|
+
.split('\n')
|
|
219
|
+
.map(line => line.trim())
|
|
220
|
+
.filter(Boolean);
|
|
221
|
+
const candidate = lines[lines.length - 1] || lines[0] || '';
|
|
222
|
+
return candidate.slice(-120);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizePromptText(text: string): string {
|
|
226
|
+
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function compactPromptText(text: string): string {
|
|
230
|
+
return String(text || '').replace(/\s+/g, '').trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function promptLikelyVisible(screenText: string, promptSnippet: string): boolean {
|
|
234
|
+
const snippet = normalizePromptText(promptSnippet);
|
|
235
|
+
if (!snippet) return false;
|
|
236
|
+
|
|
237
|
+
const normalizedScreen = normalizePromptText(screenText);
|
|
238
|
+
if (normalizedScreen.includes(snippet)) return true;
|
|
239
|
+
|
|
240
|
+
const compactScreen = compactPromptText(screenText);
|
|
241
|
+
const compactSnippet = compactPromptText(promptSnippet);
|
|
242
|
+
if (compactSnippet && compactScreen.includes(compactSnippet)) return true;
|
|
243
|
+
|
|
244
|
+
const tokens = snippet
|
|
245
|
+
.split(/[^A-Za-z0-9_.:/-]+/)
|
|
246
|
+
.map(token => token.trim())
|
|
247
|
+
.filter(token => token.length >= 4);
|
|
248
|
+
if (tokens.length === 0) return false;
|
|
249
|
+
|
|
250
|
+
const required = Math.min(tokens.length, 3);
|
|
251
|
+
const matched = tokens.filter(token =>
|
|
252
|
+
normalizedScreen.includes(token) || compactScreen.includes(compactPromptText(token)),
|
|
253
|
+
).length;
|
|
254
|
+
return matched >= required;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function splitHistoryLines(text: string): string[] {
|
|
258
|
+
return String(text || '')
|
|
259
|
+
.split('\n')
|
|
260
|
+
.map(line => line.replace(/\s+$/, ''));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function normalizeHistoryLine(line: string): string {
|
|
264
|
+
return String(line || '').replace(/\s+/g, ' ').trim();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function mergeTerminalHistory(existing: string, snapshot: string): string {
|
|
268
|
+
const next = String(snapshot || '').trim();
|
|
269
|
+
if (!next) return existing;
|
|
270
|
+
const prev = String(existing || '').trim();
|
|
271
|
+
if (!prev) return next;
|
|
272
|
+
if (prev === next || prev.endsWith(next)) return prev;
|
|
273
|
+
|
|
274
|
+
const prevLines = splitHistoryLines(prev);
|
|
275
|
+
const nextLines = splitHistoryLines(next);
|
|
276
|
+
const prevNorm = prevLines.map(normalizeHistoryLine);
|
|
277
|
+
const nextNorm = nextLines.map(normalizeHistoryLine);
|
|
278
|
+
|
|
279
|
+
const maxOverlap = Math.min(prevLines.length, nextLines.length);
|
|
280
|
+
for (let overlap = maxOverlap; overlap >= 1; overlap -= 1) {
|
|
281
|
+
const prevTail = prevNorm.slice(prevNorm.length - overlap);
|
|
282
|
+
const nextHead = nextNorm.slice(0, overlap);
|
|
283
|
+
if (prevTail.every((line, index) => line === nextHead[index])) {
|
|
284
|
+
return [...prevLines, ...nextLines.slice(overlap)].join('\n').trim();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const compactPrev = prevNorm.join('\n');
|
|
289
|
+
const compactNext = nextNorm.join('\n');
|
|
290
|
+
if (compactPrev.includes(compactNext)) return prev;
|
|
291
|
+
|
|
292
|
+
return `${prev}\n${next}`.trim();
|
|
293
|
+
}
|
|
294
|
+
|
|
196
295
|
/**
|
|
197
296
|
* Normalize provider.json for auto-implement approval detection.
|
|
198
297
|
* Kept for backward compat with dev-server auto-impl pipeline only.
|
|
@@ -238,6 +337,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
238
337
|
private provider: CliProviderModule;
|
|
239
338
|
private ptyProcess: any = null;
|
|
240
339
|
private messages: CliChatMessage[] = [];
|
|
340
|
+
private committedMessages: CliChatMessage[] = [];
|
|
241
341
|
private structuredMessages: CliChatMessage[] = [];
|
|
242
342
|
private currentStatus: CliSessionStatus['status'] = 'starting';
|
|
243
343
|
private onStatusChange: (() => void) | null = null;
|
|
@@ -273,6 +373,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
273
373
|
private settleTimer: NodeJS.Timeout | null = null;
|
|
274
374
|
private settledBuffer: string = '';
|
|
275
375
|
private submitPendingUntil = 0;
|
|
376
|
+
private responseSettleIgnoreUntil = 0;
|
|
377
|
+
private responseEpoch = 0;
|
|
378
|
+
private submitRetryTimer: NodeJS.Timeout | null = null;
|
|
379
|
+
private submitRetryUsed = false;
|
|
380
|
+
private submitRetryPromptSnippet = '';
|
|
276
381
|
|
|
277
382
|
// Resize redraw suppression
|
|
278
383
|
private resizeSuppressUntil: number = 0;
|
|
@@ -288,8 +393,47 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
288
393
|
private accumulatedRawBuffer: string = '';
|
|
289
394
|
/** Current visible terminal screen snapshot */
|
|
290
395
|
private terminalScreen = new TerminalScreen(40, 120);
|
|
396
|
+
/** Rolling append-only terminal transcript built from screen snapshots */
|
|
397
|
+
private terminalHistory: string = '';
|
|
291
398
|
/** Max accumulated buffer size (last 50KB) */
|
|
292
399
|
private static readonly MAX_ACCUMULATED_BUFFER = 50000;
|
|
400
|
+
private currentTurnScope: TurnParseScope | null = null;
|
|
401
|
+
|
|
402
|
+
private syncMessageViews(): void {
|
|
403
|
+
this.messages = [...this.committedMessages];
|
|
404
|
+
this.structuredMessages = [...this.committedMessages];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private sliceFromOffset(text: string, start: number): string {
|
|
408
|
+
if (!text) return '';
|
|
409
|
+
if (!Number.isFinite(start) || start <= 0) return text;
|
|
410
|
+
if (start >= text.length) return '';
|
|
411
|
+
return text.slice(start);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private buildParseInput(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null): CliScriptInput {
|
|
415
|
+
const buffer = scope
|
|
416
|
+
? (this.sliceFromOffset(this.terminalHistory, scope.terminalHistoryStart)
|
|
417
|
+
|| this.sliceFromOffset(this.accumulatedBuffer, scope.bufferStart)
|
|
418
|
+
|| this.accumulatedBuffer)
|
|
419
|
+
: this.accumulatedBuffer;
|
|
420
|
+
const rawBuffer = scope
|
|
421
|
+
? (this.sliceFromOffset(this.accumulatedRawBuffer, scope.rawBufferStart) || this.accumulatedRawBuffer)
|
|
422
|
+
: this.accumulatedRawBuffer;
|
|
423
|
+
const terminalHistory = scope
|
|
424
|
+
? (this.sliceFromOffset(this.terminalHistory, scope.terminalHistoryStart) || this.terminalHistory)
|
|
425
|
+
: this.terminalHistory;
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
buffer,
|
|
429
|
+
rawBuffer,
|
|
430
|
+
recentBuffer: buffer.slice(-1000) || this.recentOutputBuffer,
|
|
431
|
+
screenText: this.terminalScreen.getText(),
|
|
432
|
+
terminalHistory,
|
|
433
|
+
messages: [...baseMessages],
|
|
434
|
+
partialResponse,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
293
437
|
|
|
294
438
|
private setStatus(status: CliSessionStatus['status'], trigger?: string): void {
|
|
295
439
|
const prev = this.currentStatus;
|
|
@@ -307,6 +451,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
307
451
|
private readonly approvalKeys: Record<number, string>;
|
|
308
452
|
private readonly sendDelayMs: number;
|
|
309
453
|
private readonly sendKey: string;
|
|
454
|
+
private readonly submitStrategy: 'wait_for_echo' | 'immediate';
|
|
310
455
|
|
|
311
456
|
constructor(provider: CliProviderModule, workingDir: string, private extraArgs: string[] = []) {
|
|
312
457
|
this.provider = provider;
|
|
@@ -334,6 +479,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
334
479
|
this.sendKey = typeof (provider as any).sendKey === 'string' && (provider as any).sendKey.length > 0
|
|
335
480
|
? (provider as any).sendKey
|
|
336
481
|
: '\r';
|
|
482
|
+
this.submitStrategy = (provider as any).submitStrategy === 'immediate' ? 'immediate' : 'wait_for_echo';
|
|
337
483
|
|
|
338
484
|
// Scripts are required — loaded by ProviderLoader via compatibility array
|
|
339
485
|
this.cliScripts = (provider as any).scripts || {};
|
|
@@ -459,7 +605,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
459
605
|
this.startupParseGate = true;
|
|
460
606
|
this.startupBuffer = '';
|
|
461
607
|
this.terminalScreen.reset(40, 120);
|
|
462
|
-
this.
|
|
608
|
+
this.terminalHistory = '';
|
|
609
|
+
this.currentTurnScope = null;
|
|
610
|
+
this.ready = false;
|
|
463
611
|
this.setStatus('idle', 'pty_ready');
|
|
464
612
|
this.onStatusChange?.();
|
|
465
613
|
}
|
|
@@ -475,6 +623,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
475
623
|
}
|
|
476
624
|
|
|
477
625
|
this.terminalScreen.write(rawData);
|
|
626
|
+
this.terminalHistory = mergeTerminalHistory(this.terminalHistory, this.terminalScreen.getText());
|
|
478
627
|
const cleanData = stripAnsi(rawData);
|
|
479
628
|
|
|
480
629
|
if (this.isWaitingForResponse && cleanData) {
|
|
@@ -521,7 +670,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
521
670
|
|
|
522
671
|
if (isReady) {
|
|
523
672
|
this.startupParseGate = false;
|
|
673
|
+
this.ready = true;
|
|
524
674
|
LOG.info('CLI', `[${this.cliType}] Startup gate end (${elapsed}ms, scriptStatus=${scriptStatus})`);
|
|
675
|
+
this.onStatusChange?.();
|
|
525
676
|
} else {
|
|
526
677
|
return;
|
|
527
678
|
}
|
|
@@ -533,6 +684,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
533
684
|
|
|
534
685
|
private scheduleSettle(): void {
|
|
535
686
|
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
687
|
+
const settleEpoch = this.responseEpoch;
|
|
536
688
|
const delay = Math.max(
|
|
537
689
|
this.timeouts.outputSettle,
|
|
538
690
|
this.submitPendingUntil > Date.now()
|
|
@@ -541,14 +693,40 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
541
693
|
);
|
|
542
694
|
this.settleTimer = setTimeout(() => {
|
|
543
695
|
this.settleTimer = null;
|
|
696
|
+
if (settleEpoch !== this.responseEpoch) return;
|
|
544
697
|
this.settledBuffer = this.recentOutputBuffer;
|
|
545
698
|
this.evaluateSettled();
|
|
546
699
|
}, delay);
|
|
547
700
|
}
|
|
548
701
|
|
|
702
|
+
private armApprovalExitTimeout(): void {
|
|
703
|
+
if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
|
|
704
|
+
this.approvalExitTimeout = setTimeout(() => {
|
|
705
|
+
if (this.currentStatus !== 'waiting_approval') return;
|
|
706
|
+
const tail = this.recentOutputBuffer;
|
|
707
|
+
const modal = this.runParseApproval(tail);
|
|
708
|
+
const stillWaiting = this.runDetectStatus(tail) === 'waiting_approval' || !!modal;
|
|
709
|
+
if (stillWaiting) {
|
|
710
|
+
this.activeModal = modal || this.activeModal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
|
|
711
|
+
this.onStatusChange?.();
|
|
712
|
+
this.armApprovalExitTimeout();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
|
|
716
|
+
this.activeModal = null;
|
|
717
|
+
this.lastApprovalResolvedAt = Date.now();
|
|
718
|
+
this.setStatus('idle', 'approval_timeout');
|
|
719
|
+
this.onStatusChange?.();
|
|
720
|
+
}, 60000);
|
|
721
|
+
}
|
|
722
|
+
|
|
549
723
|
private evaluateSettled(): void {
|
|
724
|
+
if (this.submitPendingUntil > Date.now()) return;
|
|
725
|
+
if (this.responseSettleIgnoreUntil > Date.now()) return;
|
|
550
726
|
const tail = this.settledBuffer;
|
|
551
|
-
const
|
|
727
|
+
const modal = this.runParseApproval(tail);
|
|
728
|
+
const rawScriptStatus = this.runDetectStatus(tail);
|
|
729
|
+
const scriptStatus = rawScriptStatus === 'waiting_approval' || modal ? 'waiting_approval' : rawScriptStatus;
|
|
552
730
|
if (!scriptStatus) return;
|
|
553
731
|
|
|
554
732
|
const prevStatus = this.currentStatus;
|
|
@@ -560,20 +738,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
560
738
|
this.setStatus('waiting_approval', 'script_detect');
|
|
561
739
|
|
|
562
740
|
// Use parseApproval script for modal info
|
|
563
|
-
const modal = this.runParseApproval(tail);
|
|
564
741
|
this.activeModal = modal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
|
|
565
742
|
|
|
566
743
|
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
567
|
-
|
|
568
|
-
this.approvalExitTimeout = setTimeout(() => {
|
|
569
|
-
if (this.currentStatus === 'waiting_approval') {
|
|
570
|
-
LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
|
|
571
|
-
this.activeModal = null;
|
|
572
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
573
|
-
this.setStatus('idle', 'approval_timeout');
|
|
574
|
-
this.onStatusChange?.();
|
|
575
|
-
}
|
|
576
|
-
}, 60000);
|
|
744
|
+
this.armApprovalExitTimeout();
|
|
577
745
|
this.onStatusChange?.();
|
|
578
746
|
return;
|
|
579
747
|
}
|
|
@@ -607,7 +775,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
607
775
|
this.lastApprovalResolvedAt = Date.now();
|
|
608
776
|
}
|
|
609
777
|
if (this.isWaitingForResponse) {
|
|
610
|
-
this.
|
|
778
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
779
|
+
this.idleTimeout = setTimeout(() => {
|
|
780
|
+
if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
|
|
781
|
+
this.finishResponse();
|
|
782
|
+
}
|
|
783
|
+
}, this.timeouts.idleFinish);
|
|
611
784
|
} else if (prevStatus !== 'idle') {
|
|
612
785
|
this.setStatus('idle', 'script_detect');
|
|
613
786
|
this.onStatusChange?.();
|
|
@@ -616,17 +789,78 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
616
789
|
}
|
|
617
790
|
|
|
618
791
|
private finishResponse(): void {
|
|
792
|
+
if (this.submitPendingUntil > Date.now()) return;
|
|
793
|
+
if (this.responseSettleIgnoreUntil > Date.now()) return;
|
|
794
|
+
this.commitCurrentTranscript();
|
|
619
795
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
620
796
|
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
621
797
|
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
798
|
+
if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
|
|
622
799
|
|
|
623
800
|
this.responseBuffer = '';
|
|
624
801
|
this.isWaitingForResponse = false;
|
|
802
|
+
this.responseSettleIgnoreUntil = 0;
|
|
803
|
+
this.submitRetryUsed = false;
|
|
804
|
+
this.submitRetryPromptSnippet = '';
|
|
805
|
+
this.currentTurnScope = null;
|
|
625
806
|
this.activeModal = null;
|
|
626
807
|
this.setStatus('idle', 'response_finished');
|
|
627
808
|
this.onStatusChange?.();
|
|
628
809
|
}
|
|
629
810
|
|
|
811
|
+
private commitCurrentTranscript(): void {
|
|
812
|
+
const baseMessages = [...this.committedMessages];
|
|
813
|
+
const parsed = this.parseCurrentTranscript(baseMessages, '', this.currentTurnScope);
|
|
814
|
+
if (parsed && Array.isArray(parsed.messages) && parsed.messages.length > 0) {
|
|
815
|
+
const parsedMessages = parsed.messages
|
|
816
|
+
.filter((m: any) => m && (m.role === 'user' || m.role === 'assistant'))
|
|
817
|
+
.map((m: any) => ({
|
|
818
|
+
role: m.role,
|
|
819
|
+
content: typeof m.content === 'string' ? m.content : String(m.content || ''),
|
|
820
|
+
timestamp: m.timestamp,
|
|
821
|
+
}));
|
|
822
|
+
const latestAssistant = [...parsedMessages]
|
|
823
|
+
.reverse()
|
|
824
|
+
.find((m: CliChatMessage) => m.role === 'assistant' && m.content.trim());
|
|
825
|
+
if (latestAssistant) {
|
|
826
|
+
LOG.info('CLI', `[${this.cliType}] commitCurrentTranscript parsed assistant len=${latestAssistant.content.length} scopePrompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 120)}`);
|
|
827
|
+
const nextMessages = [...baseMessages];
|
|
828
|
+
const last = nextMessages[nextMessages.length - 1];
|
|
829
|
+
if (last?.role === 'assistant') {
|
|
830
|
+
last.content = latestAssistant.content;
|
|
831
|
+
last.timestamp = latestAssistant.timestamp || last.timestamp;
|
|
832
|
+
} else if (last?.role === 'user') {
|
|
833
|
+
nextMessages.push({
|
|
834
|
+
role: 'assistant',
|
|
835
|
+
content: latestAssistant.content,
|
|
836
|
+
timestamp: latestAssistant.timestamp || Date.now(),
|
|
837
|
+
});
|
|
838
|
+
} else {
|
|
839
|
+
nextMessages.push({
|
|
840
|
+
role: 'assistant',
|
|
841
|
+
content: latestAssistant.content,
|
|
842
|
+
timestamp: latestAssistant.timestamp || Date.now(),
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
this.committedMessages = nextMessages;
|
|
846
|
+
this.syncMessageViews();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const fallback = String(this.responseBuffer || '').trim();
|
|
852
|
+
LOG.info('CLI', `[${this.cliType}] commitCurrentTranscript fallback len=${fallback.length} scopePrompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 120)}`);
|
|
853
|
+
if (!fallback) return;
|
|
854
|
+
const last = baseMessages[baseMessages.length - 1];
|
|
855
|
+
if (last?.role === 'assistant') {
|
|
856
|
+
last.content = fallback;
|
|
857
|
+
} else {
|
|
858
|
+
baseMessages.push({ role: 'assistant', content: fallback, timestamp: Date.now() });
|
|
859
|
+
}
|
|
860
|
+
this.committedMessages = baseMessages;
|
|
861
|
+
this.syncMessageViews();
|
|
862
|
+
}
|
|
863
|
+
|
|
630
864
|
// ─── Script Execution ──────────────────────────
|
|
631
865
|
|
|
632
866
|
private runDetectStatus(text: string): string | null {
|
|
@@ -660,26 +894,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
660
894
|
// ─── Public API (CliAdapter) ───────────────────
|
|
661
895
|
|
|
662
896
|
getStatus(): CliSessionStatus {
|
|
663
|
-
// Use parseOutput script for full result when available
|
|
664
|
-
const scriptResult = this.getScriptParsedStatus();
|
|
665
|
-
if (scriptResult) {
|
|
666
|
-
return {
|
|
667
|
-
status: this.currentStatus,
|
|
668
|
-
messages: (scriptResult.messages || []).map((m: any) => ({
|
|
669
|
-
role: m.role,
|
|
670
|
-
content: m.content,
|
|
671
|
-
timestamp: m.timestamp,
|
|
672
|
-
})),
|
|
673
|
-
workingDir: this.workingDir,
|
|
674
|
-
activeModal: this.activeModal,
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
|
|
678
897
|
return {
|
|
679
898
|
status: this.currentStatus,
|
|
680
|
-
messages: [...this.
|
|
899
|
+
messages: [...this.committedMessages],
|
|
681
900
|
workingDir: this.workingDir,
|
|
682
901
|
activeModal: this.activeModal,
|
|
902
|
+
terminalHistory: this.terminalHistory,
|
|
683
903
|
};
|
|
684
904
|
}
|
|
685
905
|
|
|
@@ -688,31 +908,33 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
688
908
|
* Called by command handler / dashboard for rich content rendering.
|
|
689
909
|
*/
|
|
690
910
|
getScriptParsedStatus(): any {
|
|
911
|
+
const messages = [...this.committedMessages];
|
|
912
|
+
return {
|
|
913
|
+
id: 'cli_session',
|
|
914
|
+
status: this.currentStatus,
|
|
915
|
+
title: this.cliName,
|
|
916
|
+
terminalHistory: this.terminalHistory,
|
|
917
|
+
messages: messages.slice(-50).map((message, index) => ({
|
|
918
|
+
id: `msg_${index}`,
|
|
919
|
+
role: message.role,
|
|
920
|
+
content: message.content,
|
|
921
|
+
timestamp: message.timestamp,
|
|
922
|
+
index,
|
|
923
|
+
kind: 'standard',
|
|
924
|
+
})),
|
|
925
|
+
activeModal: this.activeModal,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
private parseCurrentTranscript(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null): any {
|
|
691
930
|
if (!this.cliScripts?.parseOutput) return null;
|
|
692
931
|
try {
|
|
693
|
-
const input
|
|
694
|
-
|
|
695
|
-
rawBuffer: this.accumulatedRawBuffer,
|
|
696
|
-
recentBuffer: this.recentOutputBuffer,
|
|
697
|
-
screenText: this.terminalScreen.getText(),
|
|
698
|
-
messages: [...(this.structuredMessages.length > 0 ? this.structuredMessages : this.messages)],
|
|
699
|
-
partialResponse: this.responseBuffer,
|
|
700
|
-
};
|
|
701
|
-
const result = this.cliScripts.parseOutput(input);
|
|
702
|
-
if (result && typeof result === 'object') {
|
|
703
|
-
if (Array.isArray((result as any).messages)) {
|
|
704
|
-
this.structuredMessages = (result as any).messages.map((m: any) => ({
|
|
705
|
-
role: m.role,
|
|
706
|
-
content: m.content,
|
|
707
|
-
timestamp: m.timestamp,
|
|
708
|
-
}));
|
|
709
|
-
}
|
|
710
|
-
return result;
|
|
711
|
-
}
|
|
932
|
+
const input = this.buildParseInput(baseMessages, partialResponse, scope);
|
|
933
|
+
return this.cliScripts.parseOutput(input);
|
|
712
934
|
} catch (e: any) {
|
|
713
935
|
LOG.warn('CLI', `[${this.cliType}] parseOutput error: ${e.message}`);
|
|
936
|
+
return null;
|
|
714
937
|
}
|
|
715
|
-
return null;
|
|
716
938
|
}
|
|
717
939
|
|
|
718
940
|
/** Whether this adapter has CLI scripts loaded */
|
|
@@ -744,31 +966,132 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
744
966
|
|
|
745
967
|
async sendMessage(text: string): Promise<void> {
|
|
746
968
|
if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
|
|
969
|
+
if (this.startupParseGate) {
|
|
970
|
+
const deadline = Date.now() + 10000;
|
|
971
|
+
while (this.startupParseGate && Date.now() < deadline) {
|
|
972
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
747
975
|
if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
|
|
748
976
|
if (this.isWaitingForResponse) return;
|
|
749
977
|
|
|
750
|
-
this.
|
|
751
|
-
this.
|
|
978
|
+
this.committedMessages.push({ role: 'user', content: text, timestamp: Date.now() });
|
|
979
|
+
this.syncMessageViews();
|
|
752
980
|
this.isWaitingForResponse = true;
|
|
753
981
|
this.responseBuffer = '';
|
|
982
|
+
this.currentTurnScope = {
|
|
983
|
+
prompt: text,
|
|
984
|
+
startedAt: Date.now(),
|
|
985
|
+
bufferStart: this.accumulatedBuffer.length,
|
|
986
|
+
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
987
|
+
terminalHistoryStart: this.terminalHistory.length,
|
|
988
|
+
};
|
|
989
|
+
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart} terminal=${this.currentTurnScope.terminalHistoryStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
990
|
+
this.submitRetryUsed = false;
|
|
991
|
+
this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
992
|
+
const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet);
|
|
993
|
+
if (this.submitRetryTimer) {
|
|
994
|
+
clearTimeout(this.submitRetryTimer);
|
|
995
|
+
this.submitRetryTimer = null;
|
|
996
|
+
}
|
|
997
|
+
const estimatedLines = estimatePromptDisplayLines(text);
|
|
998
|
+
const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
|
|
999
|
+
const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
|
|
1000
|
+
const retryDelayMs = Math.max(350, Math.min(1500, Math.max(this.sendDelayMs, submitDelayMs)));
|
|
1001
|
+
if (this.settleTimer) {
|
|
1002
|
+
clearTimeout(this.settleTimer);
|
|
1003
|
+
this.settleTimer = null;
|
|
1004
|
+
}
|
|
1005
|
+
this.responseEpoch += 1;
|
|
1006
|
+
this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
|
|
754
1007
|
this.setStatus('generating', 'sendMessage');
|
|
755
1008
|
this.onStatusChange?.();
|
|
756
|
-
|
|
1009
|
+
const startResponseTimeout = () => {
|
|
1010
|
+
if (this.responseTimeout) clearTimeout(this.responseTimeout);
|
|
1011
|
+
this.responseTimeout = setTimeout(() => {
|
|
1012
|
+
if (this.isWaitingForResponse) this.finishResponse();
|
|
1013
|
+
}, this.timeouts.maxResponse);
|
|
1014
|
+
};
|
|
757
1015
|
|
|
758
1016
|
const submit = () => {
|
|
759
1017
|
if (!this.ptyProcess) return;
|
|
760
1018
|
this.submitPendingUntil = 0;
|
|
761
1019
|
this.ptyProcess.write(this.sendKey);
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1020
|
+
const retrySubmitIfStuck = (attempt: number) => {
|
|
1021
|
+
this.submitRetryTimer = null;
|
|
1022
|
+
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
|
|
1023
|
+
if (this.currentStatus !== 'generating') return;
|
|
1024
|
+
if ((this.responseBuffer || '').trim()) return;
|
|
1025
|
+
const screenText = this.terminalScreen.getText();
|
|
1026
|
+
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
|
|
1027
|
+
if (/Esc to interrupt|Do you want to proceed|This command requires approval|Allow Codex to|Approve and run now|Always approve this session|Running…|Running\.\.\./i.test(screenText)) return;
|
|
1028
|
+
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1029
|
+
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
|
|
1030
|
+
this.ptyProcess.write(this.sendKey);
|
|
1031
|
+
if (attempt >= 3) {
|
|
1032
|
+
this.submitRetryUsed = true;
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
this.submitRetryTimer = setTimeout(() => retrySubmitIfStuck(attempt + 1), retryDelayMs);
|
|
1036
|
+
};
|
|
1037
|
+
this.submitRetryTimer = setTimeout(() => retrySubmitIfStuck(1), retryDelayMs);
|
|
1038
|
+
startResponseTimeout();
|
|
765
1039
|
};
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1040
|
+
|
|
1041
|
+
if (this.submitStrategy === 'immediate') {
|
|
1042
|
+
this.submitPendingUntil = 0;
|
|
1043
|
+
this.ptyProcess.write(text + this.sendKey);
|
|
1044
|
+
this.submitRetryTimer = setTimeout(() => {
|
|
1045
|
+
this.submitRetryTimer = null;
|
|
1046
|
+
if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
|
|
1047
|
+
if (this.currentStatus !== 'generating') return;
|
|
1048
|
+
if ((this.responseBuffer || '').trim()) return;
|
|
1049
|
+
const screenText = this.terminalScreen.getText();
|
|
1050
|
+
if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
|
|
1051
|
+
LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
|
|
1052
|
+
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1053
|
+
this.ptyProcess.write(this.sendKey);
|
|
1054
|
+
this.submitRetryUsed = true;
|
|
1055
|
+
}, retryDelayMs);
|
|
1056
|
+
startResponseTimeout();
|
|
1057
|
+
return;
|
|
771
1058
|
}
|
|
1059
|
+
|
|
1060
|
+
if (submitDelayMs > 0) {
|
|
1061
|
+
this.submitPendingUntil = Date.now() + submitDelayMs;
|
|
1062
|
+
}
|
|
1063
|
+
this.ptyProcess.write(text);
|
|
1064
|
+
const submitStartedAt = Date.now();
|
|
1065
|
+
let lastNormalizedScreen = '';
|
|
1066
|
+
let lastScreenChangeAt = submitStartedAt;
|
|
1067
|
+
const waitForEchoAndSubmit = () => {
|
|
1068
|
+
if (!this.ptyProcess) return;
|
|
1069
|
+
const now = Date.now();
|
|
1070
|
+
const elapsed = now - submitStartedAt;
|
|
1071
|
+
const screenText = this.terminalScreen.getText();
|
|
1072
|
+
const normalizedScreen = normalizePromptText(screenText);
|
|
1073
|
+
if (normalizedScreen !== lastNormalizedScreen) {
|
|
1074
|
+
lastNormalizedScreen = normalizedScreen;
|
|
1075
|
+
lastScreenChangeAt = now;
|
|
1076
|
+
}
|
|
1077
|
+
const echoVisible = !normalizedPromptSnippet || promptLikelyVisible(screenText, normalizedPromptSnippet);
|
|
1078
|
+
|
|
1079
|
+
if (echoVisible) {
|
|
1080
|
+
const screenSettled = (now - lastScreenChangeAt) >= 500;
|
|
1081
|
+
if (elapsed >= submitDelayMs && screenSettled) {
|
|
1082
|
+
submit();
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (elapsed >= maxEchoWaitMs) {
|
|
1088
|
+
submit();
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
setTimeout(waitForEchoAndSubmit, 50);
|
|
1093
|
+
};
|
|
1094
|
+
waitForEchoAndSubmit();
|
|
772
1095
|
}
|
|
773
1096
|
|
|
774
1097
|
getPartialResponse(): string {
|
|
@@ -781,6 +1104,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
781
1104
|
shutdown(): void {
|
|
782
1105
|
if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
|
|
783
1106
|
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
1107
|
+
if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
|
|
784
1108
|
if (this.ptyProcess) {
|
|
785
1109
|
this.ptyProcess.write('\x03');
|
|
786
1110
|
setTimeout(() => {
|
|
@@ -796,10 +1120,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
796
1120
|
}
|
|
797
1121
|
|
|
798
1122
|
clearHistory(): void {
|
|
799
|
-
this.
|
|
800
|
-
this.
|
|
1123
|
+
this.committedMessages = [];
|
|
1124
|
+
this.syncMessageViews();
|
|
801
1125
|
this.accumulatedBuffer = '';
|
|
802
1126
|
this.accumulatedRawBuffer = '';
|
|
1127
|
+
this.terminalHistory = '';
|
|
1128
|
+
this.currentTurnScope = null;
|
|
1129
|
+
this.submitRetryUsed = false;
|
|
1130
|
+
this.submitRetryPromptSnippet = '';
|
|
803
1131
|
this.terminalScreen.reset();
|
|
804
1132
|
this.onStatusChange?.();
|
|
805
1133
|
}
|
|
@@ -812,7 +1140,16 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
812
1140
|
}
|
|
813
1141
|
|
|
814
1142
|
resolveModal(buttonIndex: number): void {
|
|
815
|
-
if (!this.ptyProcess || this.currentStatus !== 'waiting_approval') return;
|
|
1143
|
+
if (!this.ptyProcess || (this.currentStatus !== 'waiting_approval' && !this.activeModal)) return;
|
|
1144
|
+
this.activeModal = null;
|
|
1145
|
+
this.lastApprovalResolvedAt = Date.now();
|
|
1146
|
+
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
1147
|
+
if (this.approvalExitTimeout) {
|
|
1148
|
+
clearTimeout(this.approvalExitTimeout);
|
|
1149
|
+
this.approvalExitTimeout = null;
|
|
1150
|
+
}
|
|
1151
|
+
this.setStatus('generating', 'approval_resolved');
|
|
1152
|
+
this.onStatusChange?.();
|
|
816
1153
|
if (buttonIndex in this.approvalKeys) {
|
|
817
1154
|
this.ptyProcess.write(this.approvalKeys[buttonIndex]);
|
|
818
1155
|
} else {
|
|
@@ -842,9 +1179,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
842
1179
|
spawnAt: this.spawnAt,
|
|
843
1180
|
workingDir: this.workingDir,
|
|
844
1181
|
messages: this.messages.slice(-20),
|
|
1182
|
+
committedMessages: this.committedMessages.slice(-20),
|
|
845
1183
|
structuredMessages: this.structuredMessages.slice(-20),
|
|
846
|
-
messageCount: this.
|
|
1184
|
+
messageCount: this.committedMessages.length,
|
|
847
1185
|
screenText: this.terminalScreen.getText().slice(-4000),
|
|
1186
|
+
terminalHistory: this.terminalHistory.slice(-8000),
|
|
1187
|
+
currentTurnScope: this.currentTurnScope,
|
|
848
1188
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
849
1189
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
850
1190
|
settledBuffer: this.settledBuffer.slice(-500),
|
|
@@ -855,6 +1195,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
855
1195
|
isWaitingForResponse: this.isWaitingForResponse,
|
|
856
1196
|
activeModal: this.activeModal,
|
|
857
1197
|
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
1198
|
+
sendDelayMs: this.sendDelayMs,
|
|
1199
|
+
sendKey: this.sendKey,
|
|
1200
|
+
submitStrategy: this.submitStrategy,
|
|
1201
|
+
submitPendingUntil: this.submitPendingUntil,
|
|
1202
|
+
responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
|
|
858
1203
|
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
859
1204
|
hasCliScripts: this.hasCliScripts(),
|
|
860
1205
|
scriptNames: Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function'),
|