@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.
Files changed (37) hide show
  1. package/dist/index.d.ts +23 -0
  2. package/dist/index.js +423 -95
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/providers/_builtin/cli/aider-cli/scripts/1.0/parse_output.js +51 -3
  6. package/providers/_builtin/cli/claude-cli/provider.json +18 -6
  7. package/providers/_builtin/cli/claude-cli/scripts/1.0/detect_status.js +68 -16
  8. package/providers/_builtin/cli/claude-cli/scripts/1.0/parse_approval.js +81 -22
  9. package/providers/_builtin/cli/claude-cli/scripts/1.0/parse_output.js +347 -94
  10. package/providers/_builtin/cli/codex-cli/provider.json +2 -0
  11. package/providers/_builtin/cli/codex-cli/scripts/1.0/detect_status.js +44 -10
  12. package/providers/_builtin/cli/codex-cli/scripts/1.0/parse_approval.js +83 -7
  13. package/providers/_builtin/cli/codex-cli/scripts/1.0/parse_output.js +501 -47
  14. package/providers/_builtin/cli/cursor-cli/scripts/1.0/parse_output.js +1 -1
  15. package/providers/_builtin/cli/github-copilot-cli/scripts/1.0/parse_output.js +1 -1
  16. package/providers/_builtin/cli/goose-cli/scripts/1.0/parse_output.js +1 -1
  17. package/providers/_builtin/cli/opencode-cli/scripts/1.0/parse_output.js +1 -1
  18. package/providers/_builtin/ide/vscode/provider.json +5 -1
  19. package/providers/_builtin/ide/vscode/scripts/1.0/focus_editor.js +1 -0
  20. package/providers/_builtin/ide/vscode/scripts/1.0/list_models.js +1 -0
  21. package/providers/_builtin/ide/vscode/scripts/1.0/list_sessions.js +1 -0
  22. package/providers/_builtin/ide/vscode/scripts/1.0/new_session.js +1 -0
  23. package/providers/_builtin/ide/vscode/scripts/1.0/open_panel.js +1 -0
  24. package/providers/_builtin/ide/vscode/scripts/1.0/read_chat.js +1 -0
  25. package/providers/_builtin/ide/vscode/scripts/1.0/resolve_action.js +1 -0
  26. package/providers/_builtin/ide/vscode/scripts/1.0/scripts.js +25 -0
  27. package/providers/_builtin/ide/vscode/scripts/1.0/send_message.js +1 -0
  28. package/providers/_builtin/ide/vscode/scripts/1.0/set_model.js +1 -0
  29. package/providers/_builtin/ide/vscode/scripts/1.0/switch_session.js +1 -0
  30. package/providers/_builtin/registry.json +1 -1
  31. package/src/cli-adapters/provider-cli-adapter.ts +410 -65
  32. package/src/commands/chat-commands.ts +7 -1
  33. package/src/config/chat-history.ts +53 -1
  34. package/src/daemon/dev-server.ts +7 -9
  35. package/src/providers/cli-provider-instance.ts +10 -23
  36. package/src/providers/provider-instance.ts +1 -0
  37. 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.ready = true;
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 scriptStatus = this.runDetectStatus(tail);
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
- if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
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.finishResponse();
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.messages],
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: CliScriptInput = {
694
- buffer: this.accumulatedBuffer,
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.messages.push({ role: 'user', content: text, timestamp: Date.now() });
751
- this.structuredMessages.push({ role: 'user', content: text, timestamp: Date.now() });
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
- this.ptyProcess.write(text);
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
- this.responseTimeout = setTimeout(() => {
763
- if (this.isWaitingForResponse) this.finishResponse();
764
- }, this.timeouts.maxResponse);
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
- if (this.sendDelayMs > 0) {
767
- this.submitPendingUntil = Date.now() + this.sendDelayMs;
768
- setTimeout(submit, this.sendDelayMs);
769
- } else {
770
- submit();
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.messages = [];
800
- this.structuredMessages = [];
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.messages.length,
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'),