@adhdev/daemon-core 0.9.6 → 0.9.8

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.
@@ -32,7 +32,6 @@ import {
32
32
  extractPromptRetrySnippet,
33
33
  getLastUserPromptText,
34
34
  listCliScriptNames,
35
- looksLikeConfirmOnlyLabel,
36
35
  normalizePromptText,
37
36
  normalizeScreenSnapshot,
38
37
  promptLikelyVisible,
@@ -94,6 +93,18 @@ interface IdleFinishCandidate {
94
93
  assistantLength: number;
95
94
  }
96
95
 
96
+ interface SettledEvalContext {
97
+ now: number;
98
+ screenText: string;
99
+ modal: any;
100
+ scriptStatus: string;
101
+ parsedTranscript: any;
102
+ parsedMessages: CliChatMessage[];
103
+ lastParsedAssistant: CliChatMessage | undefined;
104
+ parsedShowsLiveAssistantProgress: boolean;
105
+ prevStatus: string;
106
+ }
107
+
97
108
  function normalizeComparableTranscriptText(value: unknown): string {
98
109
  return sanitizeTerminalText(String(value || ''))
99
110
  .replace(/\s+/g, ' ')
@@ -625,7 +636,7 @@ export class ProviderCliAdapter implements CliAdapter {
625
636
  const stableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
626
637
  if (stableMs < 2000) return;
627
638
 
628
- const startupModal = this.getStartupConfirmationModal(screenText);
639
+ const startupModal = this.runParseApproval(this.recentOutputBuffer);
629
640
  this.startupParseGate = false;
630
641
  if (this.startupSettleTimer) {
631
642
  clearTimeout(this.startupSettleTimer);
@@ -689,11 +700,17 @@ export class ProviderCliAdapter implements CliAdapter {
689
700
  if (this.currentStatus !== 'waiting_approval') return;
690
701
  const tail = this.recentOutputBuffer;
691
702
  const screenText = this.terminalScreen.getText() || '';
692
- const startupModal = this.getStartupConfirmationModal(screenText);
693
- const modal = this.runParseApproval(tail) || startupModal;
703
+ const modal = this.runParseApproval(tail);
694
704
  const stillWaiting = this.runDetectStatus(tail) === 'waiting_approval' || !!modal;
695
705
  if (stillWaiting) {
696
- this.activeModal = modal || this.activeModal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
706
+ if (!modal) {
707
+ LOG.warn('CLI', `[${this.cliType}] approval timeout check found no actionable modal; keeping approval state fail-closed`);
708
+ this.activeModal = null;
709
+ this.onStatusChange?.();
710
+ this.armApprovalExitTimeout();
711
+ return;
712
+ }
713
+ this.activeModal = modal;
697
714
  this.onStatusChange?.();
698
715
  this.armApprovalExitTimeout();
699
716
  return;
@@ -706,103 +723,13 @@ export class ProviderCliAdapter implements CliAdapter {
706
723
  }, 60000);
707
724
  }
708
725
 
709
- private looksLikeVisibleIdlePrompt(screenText: string): boolean {
710
- const text = String(screenText || '');
711
- if (!text.trim()) return false;
712
- if (this.cliType === 'codex-cli' && /(^|\n)\s*[❯›>]\s+(?:Find and fix a bug in @filename|Improve documentation in @filename|Use \/skills|Write tests for @filename|Explain this codebase|Summarize recent commits|Implement \{feature\}|Run \/review on my current changes)(?:\n|$)/im.test(text)) {
713
- return true;
714
- }
715
- return /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(text)
716
- || /⏎\s+send/i.test(text)
717
- || /\?\s*for\s*shortcuts/i.test(text)
718
- || /Type your message(?:\s+or\s+@path\/to\/file)?/i.test(text)
719
- || /workspace\s*\(\/directory\)/i.test(text)
720
- || /for\s*shortcuts/i.test(text);
721
- }
722
-
723
- private findLastMatchingLineIndex(lines: string[], predicate: (line: string) => boolean): number {
724
- for (let index = lines.length - 1; index >= 0; index -= 1) {
725
- if (predicate(lines[index])) return index;
726
- }
727
- return -1;
728
- }
729
-
730
- private looksLikeClaudeGeneratingLine(line: string): boolean {
731
- const trimmed = String(line || '').trim();
732
- if (!trimmed) return false;
733
- if (/^⏵⏵\s+accept edits on/i.test(trimmed)) return false;
734
- if (/esc to (cancel|interrupt|stop)/i.test(trimmed)) return true;
735
- if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+\s+\S+.*\b(?:thinking|thought for \d+s?)\b/i.test(trimmed)) return true;
736
- if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+\s+[A-Z][A-Za-z-]{3,}ing\b.*(?:…|\.{3})/u.test(trimmed)) return true;
737
- if (/^[⏺•]\s+(?:Reading|Writing|Editing|Searching|Inspecting|Planning|Analyzing|Synthesizing|Drafting|Running|Listing|Scanning|Matching)\b.*(?:…|\.{3})/i.test(trimmed)) {
738
- return /ctrl\+o to expand/i.test(trimmed)
739
- || /\b\d+\s+(?:file|files|pattern|patterns|director(?:y|ies)|match|matches|result|results)\b/i.test(trimmed);
740
- }
741
- return false;
742
- }
743
-
744
- private detectClaudeGeneratingOverride(screenText: string, tail: string): boolean {
745
- if (this.cliType !== 'claude-cli') return false;
746
-
747
- const source = sanitizeTerminalText(screenText || tail || '');
748
- if (!source.trim()) return false;
749
-
750
- const allLines = source
751
- .split(/\r\n|\n|\r/g)
752
- .map(line => line.trim())
753
- .filter(Boolean);
754
- if (allLines.length === 0) return false;
755
-
756
- const recentLines = allLines.slice(-12);
757
- const promptIndex = this.findLastMatchingLineIndex(recentLines, (line) => /^[❯›>]\s*$/.test(line));
758
- const activeRegion = promptIndex >= 0 ? recentLines.slice(Math.max(0, promptIndex - 2), promptIndex) : recentLines;
759
- if (activeRegion.length === 0) return false;
760
-
761
- return activeRegion.some((line) => this.looksLikeClaudeGeneratingLine(line));
762
- }
763
-
764
- private refineDetectedStatus(status: string | null, tail: string, screenText?: string): string | null {
765
- if (this.startupParseGate) {
766
- return this.getStartupConfirmationModal(screenText || '')
767
- ? 'waiting_approval'
768
- : 'starting';
769
- }
770
- if (status === 'waiting_approval') return status;
771
- if (this.detectClaudeGeneratingOverride(screenText || '', tail)) return 'generating';
772
- return status;
773
- }
774
-
775
- private looksLikeVisibleAssistantCandidate(screenText: string): boolean {
776
- const lines = sanitizeTerminalText(String(screenText || '')).split(/\r\n|\n|\r/g);
777
- for (const line of lines) {
778
- const trimmed = String(line || '').trim();
779
- if (!trimmed) continue;
780
- if (/^➜\s+\S+/.test(trimmed)) continue;
781
- if (/^Update available!/i.test(trimmed)) continue;
782
- if (/Claude Code v\d/i.test(trimmed)) continue;
783
- if (/^⏵⏵\s+accept edits on/i.test(trimmed)) continue;
784
- if (/^[◐◑◒◓◴◵◶◷◸◹◺◿].*\/effort/i.test(trimmed)) continue;
785
- if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+$/.test(trimmed)) continue;
786
- if (/esc to (cancel|interrupt|stop)/i.test(trimmed)) continue;
787
- const assistantMatch = trimmed.match(/^⏺\s+(.+)$/);
788
- if (!assistantMatch) continue;
789
- const content = assistantMatch[1].trim();
790
- if (!content) continue;
791
- if (/^(?:Bash|Read|Write|Edit|MultiEdit|Task|Glob|Grep|LS|NotebookEdit)\(/.test(content)) continue;
792
- if (/This command requires approval|Do you want to proceed|Allow once|Always allow/i.test(content)) continue;
793
- return true;
794
- }
795
- return false;
796
- }
797
-
798
726
  private shouldRetryFinishResponse(commitResult: { hasAssistant: boolean; assistantContent: string }): boolean {
799
727
  if (!this.currentTurnScope) return false;
800
728
  if (this.currentStatus === 'waiting_approval' || this.activeModal) return false;
801
729
  if (this.finishRetryCount >= ProviderCliAdapter.MAX_FINISH_RETRIES) return false;
802
730
  if (commitResult.hasAssistant && commitResult.assistantContent.trim()) return false;
803
731
 
804
- const screenText = this.terminalScreen.getText() || '';
805
- if (!this.looksLikeVisibleAssistantCandidate(screenText)) return false;
732
+ if (this.runDetectStatus(this.recentOutputBuffer) !== 'idle') return false;
806
733
 
807
734
  const now = Date.now();
808
735
  const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
@@ -831,35 +758,6 @@ export class ProviderCliAdapter implements CliAdapter {
831
758
  return false;
832
759
  }
833
760
 
834
- private getStartupConfirmationModal(screenText: string): { message: string; buttons: string[] } | null {
835
- const text = sanitizeTerminalText(String(screenText || ''));
836
- if (!text.trim()) return null;
837
-
838
- if (this.cliType === 'claude-cli') {
839
- const hasTrustPrompt = /Quick safety check/i.test(text)
840
- || /Is this a project you trust/i.test(text)
841
- || /Do you trust (?:this project|the contents of this directory|the files in this folder)/i.test(text);
842
- const hasConfirmFooter = /Press Enter to (?:continue|confirm)/i.test(text)
843
- || /Enter to confirm/i.test(text)
844
- || /Esc to (?:cancel|exit)/i.test(text);
845
- if (hasTrustPrompt || (hasConfirmFooter && /trust/i.test(text))) {
846
- return {
847
- message: 'Confirm Claude Code project trust',
848
- buttons: ['Continue'],
849
- };
850
- }
851
- }
852
-
853
- return null;
854
- }
855
-
856
- private shouldResolveModalWithEnter(modal: { message: string; buttons: string[] } | null, buttonIndex: number): boolean {
857
- if (!modal || buttonIndex !== 0) return false;
858
- const buttons = Array.isArray(modal.buttons) ? modal.buttons : [];
859
- if (buttons.length !== 1) return false;
860
- const buttonLabel = String(buttons[0] || '').trim();
861
- return looksLikeConfirmOnlyLabel(buttonLabel);
862
- }
863
761
 
864
762
  private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
865
763
  const startedAt = Date.now();
@@ -868,21 +766,18 @@ export class ProviderCliAdapter implements CliAdapter {
868
766
  while (Date.now() - startedAt < maxWaitMs) {
869
767
  this.resolveStartupState('interactive_wait');
870
768
  const screenText = this.terminalScreen.getText() || '';
871
- const hasPrompt = this.looksLikeVisibleIdlePrompt(screenText);
872
769
  const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
873
770
  const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
874
771
  const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
875
- const startupLikelyActive = /Welcome back|Tips for getting|Recent activity|Claude Code v\d/i.test(screenText);
876
- const interactiveReady = hasPrompt
772
+ const interactiveReady = status === 'idle'
877
773
  && stableMs >= 700
878
- && recentlyOutput >= 350
879
- && status !== 'generating';
774
+ && recentlyOutput >= 350;
880
775
 
881
776
  if (interactiveReady) {
882
777
  if (loggedWait) {
883
778
  LOG.info(
884
779
  'CLI',
885
- `[${this.cliType}] Interactive prompt ready after ${Date.now() - startedAt}ms (stableMs=${stableMs}, recentOutputMs=${recentlyOutput}, startup=${startupLikelyActive})`
780
+ `[${this.cliType}] Interactive prompt ready after ${Date.now() - startedAt}ms (stableMs=${stableMs}, recentOutputMs=${recentlyOutput})`
886
781
  );
887
782
  }
888
783
  return;
@@ -892,7 +787,7 @@ export class ProviderCliAdapter implements CliAdapter {
892
787
  loggedWait = true;
893
788
  LOG.info(
894
789
  'CLI',
895
- `[${this.cliType}] Waiting for interactive prompt: hasPrompt=${hasPrompt} stableMs=${stableMs} recentOutputMs=${recentlyOutput} status=${status} startup=${startupLikelyActive} screen=${JSON.stringify(summarizeCliTraceText(screenText, 220)).slice(0, 260)}`
790
+ `[${this.cliType}] Waiting for interactive prompt: status=${status} stableMs=${stableMs} recentOutputMs=${recentlyOutput} screen=${JSON.stringify(summarizeCliTraceText(screenText, 220)).slice(0, 260)}`
896
791
  );
897
792
  }
898
793
  await new Promise(resolve => setTimeout(resolve, 50));
@@ -905,17 +800,31 @@ export class ProviderCliAdapter implements CliAdapter {
905
800
  );
906
801
  }
907
802
 
908
- private clearStaleIdleResponseGuard(reason: string): boolean {
909
- const screenText = this.terminalScreen.getText() || '';
910
- const visibleIdlePrompt = this.looksLikeVisibleIdlePrompt(screenText);
911
- const blockingModal = this.activeModal || this.getStartupConfirmationModal(screenText);
912
- if (!this.isWaitingForResponse || this.currentStatus !== 'idle' || !visibleIdlePrompt || !!blockingModal) {
913
- return false;
914
- }
803
+ private trimLastAssistantEcho(messages: CliChatMessage[], prompt: string | undefined): void {
804
+ if (!prompt) return;
805
+ const last = [...messages].reverse().find((m) => m.role === 'assistant' && typeof m.content === 'string');
806
+ if (last) last.content = trimPromptEchoPrefix(last.content, prompt);
807
+ }
808
+
809
+ private clearAllTimers(): void {
915
810
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
916
811
  if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
917
812
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
813
+ if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
918
814
  if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
815
+ if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
816
+ if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
817
+ if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
818
+ if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
819
+ }
820
+
821
+ private clearStaleIdleResponseGuard(reason: string): boolean {
822
+ const blockingModal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
823
+ const isIdle = this.runDetectStatus(this.recentOutputBuffer) === 'idle';
824
+ if (!this.isWaitingForResponse || this.currentStatus !== 'idle' || !isIdle || !!blockingModal) {
825
+ return false;
826
+ }
827
+ this.clearAllTimers();
919
828
  this.clearIdleFinishCandidate(reason);
920
829
  this.responseBuffer = '';
921
830
  this.isWaitingForResponse = false;
@@ -925,10 +834,7 @@ export class ProviderCliAdapter implements CliAdapter {
925
834
  this.finishRetryCount = 0;
926
835
  this.currentTurnScope = null;
927
836
  this.activeModal = null;
928
- this.recordTrace('stale_idle_response_cleared', {
929
- reason,
930
- screenText: summarizeCliTraceText(screenText, 240),
931
- });
837
+ this.recordTrace('stale_idle_response_cleared', { reason });
932
838
  return true;
933
839
  }
934
840
 
@@ -975,7 +881,6 @@ export class ProviderCliAdapter implements CliAdapter {
975
881
  if (this.startupParseGate) {
976
882
  return;
977
883
  }
978
- const startupModal = this.getStartupConfirmationModal(screenText);
979
884
  const parsedTranscript = this.parseCurrentTranscript(
980
885
  this.committedMessages,
981
886
  this.responseBuffer,
@@ -984,14 +889,11 @@ export class ProviderCliAdapter implements CliAdapter {
984
889
  const parsedModal = parsedTranscript?.activeModal && Array.isArray(parsedTranscript.activeModal.buttons) && parsedTranscript.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
985
890
  ? parsedTranscript.activeModal
986
891
  : null;
987
- const modal = this.runParseApproval(tail) || parsedModal || startupModal;
892
+ const modal = this.runParseApproval(tail) || parsedModal;
988
893
  const rawScriptStatus = this.runDetectStatus(tail);
989
- // detectStatus is the primary authority for status, but if the parsed transcript
990
- // already surfaced actionable approval buttons, promote that state so runtime
991
- // status and resolve_action stay aligned with the visible prompt.
992
- const scriptStatus = startupModal
894
+ const scriptStatus = parsedTranscript?.status === 'waiting_approval' && modal
993
895
  ? 'waiting_approval'
994
- : (parsedModal && parsedTranscript?.status === 'waiting_approval' ? 'waiting_approval' : rawScriptStatus);
896
+ : rawScriptStatus;
995
897
  const parsedMessages = Array.isArray(parsedTranscript?.messages)
996
898
  ? normalizeCliParsedMessages(parsedTranscript.messages, {
997
899
  committedMessages: this.committedMessages,
@@ -1059,246 +961,262 @@ export class ProviderCliAdapter implements CliAdapter {
1059
961
  if (!scriptStatus) return;
1060
962
 
1061
963
  const prevStatus = this.currentStatus;
964
+ const ctx: SettledEvalContext = { now, screenText, modal, scriptStatus, parsedTranscript, parsedMessages, lastParsedAssistant, parsedShowsLiveAssistantProgress, prevStatus };
1062
965
 
1063
- const clearPendingScriptStatus = () => {
1064
- this.pendingScriptStatus = null;
1065
- this.pendingScriptStatusSince = 0;
1066
- if (this.pendingScriptStatusTimer) {
1067
- clearTimeout(this.pendingScriptStatusTimer);
1068
- this.pendingScriptStatusTimer = null;
1069
- }
1070
- };
1071
- const armPendingScriptStatus = (delayMs: number) => {
1072
- if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
1073
- this.pendingScriptStatusTimer = setTimeout(() => {
1074
- this.pendingScriptStatusTimer = null;
1075
- this.settledBuffer = this.recentOutputBuffer;
1076
- this.evaluateSettled();
1077
- }, delayMs);
1078
- };
1079
- const shouldDebouncePromotion = (status: string) =>
1080
- prevStatus === 'idle'
1081
- && !this.isWaitingForResponse
1082
- && !this.currentTurnScope
1083
- && (status === 'generating' || status === 'waiting_approval');
1084
-
1085
- if (shouldDebouncePromotion(scriptStatus)) {
1086
- if (this.pendingScriptStatus !== scriptStatus) {
1087
- this.pendingScriptStatus = scriptStatus as 'generating' | 'waiting_approval';
1088
- this.pendingScriptStatusSince = now;
1089
- armPendingScriptStatus(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS);
1090
- return;
1091
- }
1092
- const elapsed = now - this.pendingScriptStatusSince;
1093
- if (elapsed < ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS) {
1094
- armPendingScriptStatus(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS - elapsed);
1095
- return;
1096
- }
1097
- } else {
1098
- clearPendingScriptStatus();
1099
- }
966
+ if (!this.applyPendingScriptStatusDebounce(ctx)) return;
1100
967
 
1101
968
  const recentInteractiveActivity = this.hasRecentInteractiveActivity(now);
1102
- const statusActivityHoldMs = this.getStatusActivityHoldMs();
1103
- const visibleIdlePrompt = this.looksLikeVisibleIdlePrompt(screenText);
1104
- const visibleAssistantCandidate = this.looksLikeVisibleAssistantCandidate(screenText);
1105
- if (this.currentTurnScope && this.cliType === 'claude-cli') {
1106
- LOG.info(
1107
- 'CLI',
1108
- `[${this.cliType}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} scriptStatus=${String(scriptStatus || '')} parsedStatus=${String(parsedTranscript?.status || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify(summarizeCliTraceText(lastParsedAssistant?.content || '', 120)).slice(0, 160)} visibleIdlePrompt=${String(visibleIdlePrompt)} visibleAssistantCandidate=${String(visibleAssistantCandidate)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 160)).slice(0, 220)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 160)).slice(0, 220)}`
1109
- );
1110
- }
969
+ LOG.info(
970
+ 'CLI',
971
+ `[${this.cliType}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 140)} scriptStatus=${String(scriptStatus || '')} parsedStatus=${String(parsedTranscript?.status || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify(summarizeCliTraceText(lastParsedAssistant?.content || '', 120)).slice(0, 160)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 160)).slice(0, 220)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 160)).slice(0, 220)}`
972
+ );
973
+
1111
974
  const shouldHoldGenerating =
1112
975
  scriptStatus === 'idle'
1113
976
  && this.isWaitingForResponse
1114
977
  && !modal
1115
978
  && recentInteractiveActivity
1116
- && !(visibleIdlePrompt && visibleAssistantCandidate)
1117
979
  && !(parsedTranscript?.status === 'idle' && !!lastParsedAssistant);
1118
980
 
1119
981
  if (shouldHoldGenerating) {
1120
- this.clearIdleFinishCandidate('hold_generating_recent_activity');
1121
- this.setStatus('generating', 'recent_activity_hold');
1122
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1123
- this.idleTimeout = setTimeout(() => {
1124
- if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1125
- if (this.shouldDeferIdleTimeoutFinish()) return;
1126
- this.finishResponse();
1127
- }
1128
- }, this.timeouts.generatingIdle);
1129
- this.recordTrace('hold_generating_recent_activity', {
1130
- scriptStatus,
1131
- recentInteractiveActivity,
1132
- lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1133
- lastScreenChangeAt: this.lastScreenChangeAt,
1134
- holdMs: statusActivityHoldMs,
1135
- ...buildCliTraceParseSnapshot({
1136
- accumulatedBuffer: this.accumulatedBuffer,
1137
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1138
- responseBuffer: this.responseBuffer,
1139
- partialResponse: this.responseBuffer,
1140
- scope: this.currentTurnScope,
1141
- }),
1142
- });
1143
- this.onStatusChange?.();
982
+ this.applyHoldGenerating(ctx, recentInteractiveActivity);
1144
983
  return;
1145
984
  }
1146
985
 
1147
986
  if (scriptStatus === 'waiting_approval') {
1148
- this.clearIdleFinishCandidate('waiting_approval');
1149
- const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
1150
- const visibleIdlePrompt = this.looksLikeVisibleIdlePrompt(screenText);
1151
- if ((inCooldown || visibleIdlePrompt) && !modal) {
1152
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1153
- this.activeModal = null;
1154
- if (this.isWaitingForResponse) {
1155
- this.setStatus('generating', inCooldown ? 'approval_cooldown_ignore' : 'approval_prompt_gone');
1156
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1157
- this.idleTimeout = setTimeout(() => {
1158
- if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1159
- if (this.shouldDeferIdleTimeoutFinish()) return;
1160
- this.finishResponse();
1161
- }
1162
- }, this.timeouts.generatingIdle);
1163
- } else {
1164
- this.setStatus('idle', inCooldown ? 'approval_cooldown_ignore' : 'approval_prompt_gone');
1165
- }
1166
- this.onStatusChange?.();
1167
- return;
1168
- }
1169
- if (!inCooldown) {
1170
- this.isWaitingForResponse = true;
1171
- this.setStatus('waiting_approval', 'script_detect');
1172
-
1173
- // Use parseApproval script for modal info
1174
- this.activeModal = modal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
1175
-
1176
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1177
- this.armApprovalExitTimeout();
1178
- this.onStatusChange?.();
1179
- return;
1180
- }
987
+ this.applyWaitingApproval(ctx);
988
+ return;
1181
989
  }
1182
990
 
1183
991
  if (scriptStatus === 'generating') {
1184
- this.clearIdleFinishCandidate('generating');
1185
- const effectiveScreenText = screenText || this.accumulatedBuffer;
1186
- const noActiveTurn = !this.currentTurnScope;
1187
- const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText)
1188
- || (/accept edits on/i.test(effectiveScreenText)
1189
- && (/Update available!/i.test(screenText)
1190
- || /\/effort/i.test(screenText)
1191
- || /^.*➜\s+\S+/m.test(effectiveScreenText)));
1192
- if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
1193
- return;
1194
- }
1195
- if (prevStatus === 'waiting_approval') {
1196
- // Transitioned out of approval → generating
1197
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1198
- this.activeModal = null;
1199
- this.lastApprovalResolvedAt = Date.now();
1200
- }
1201
- if (!this.isWaitingForResponse) {
1202
- this.isWaitingForResponse = true;
1203
- this.responseBuffer = '';
1204
- }
1205
- this.setStatus('generating', 'script_detect');
1206
- // Reset idle timeout
1207
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1208
- this.idleTimeout = setTimeout(() => {
1209
- if (this.isWaitingForResponse) {
1210
- if (this.shouldDeferIdleTimeoutFinish()) return;
1211
- this.finishResponse();
1212
- }
1213
- }, this.timeouts.generatingIdle);
1214
- this.onStatusChange?.();
992
+ this.applyGenerating(ctx);
1215
993
  return;
1216
994
  }
1217
995
 
1218
996
  if (scriptStatus === 'idle') {
1219
- if (prevStatus === 'waiting_approval') {
1220
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1221
- this.activeModal = null;
1222
- this.lastApprovalResolvedAt = Date.now();
1223
- }
1224
- if (this.isWaitingForResponse) {
1225
- const visibleIdlePrompt = this.looksLikeVisibleIdlePrompt(screenText);
1226
- const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
1227
- const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1228
- const hasAssistantTurn = !!lastParsedAssistant;
1229
- const assistantLength = lastParsedAssistant?.content?.length || 0;
1230
- const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
1231
- const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
1232
- const idleStableThresholdMs = idleFinishConfirmMs;
1233
- const idleReady = visibleIdlePrompt
1234
- && !modal
1235
- && hasAssistantTurn
1236
- && quietForMs >= idleQuietThresholdMs
1237
- && screenStableMs >= idleStableThresholdMs;
1238
- const candidate = this.idleFinishCandidate;
1239
- const candidateQuiet = !!candidate
1240
- && candidate.responseEpoch === this.responseEpoch
1241
- && candidate.lastOutputAt === this.lastOutputAt
1242
- && candidate.lastScreenChangeAt === this.lastScreenChangeAt
1243
- && assistantLength >= candidate.assistantLength
1244
- && (now - candidate.armedAt) >= idleFinishConfirmMs;
1245
- const canFinishImmediately = idleReady && candidateQuiet;
1246
-
1247
- this.recordTrace('idle_decision', {
1248
- visibleIdlePrompt,
1249
- quietForMs,
1250
- screenStableMs,
1251
- hasAssistantTurn,
1252
- assistantLength,
1253
- hasModal: !!modal,
1254
- idleQuietThresholdMs,
1255
- idleStableThresholdMs,
1256
- idleReady,
1257
- idleFinishConfirmMs,
1258
- idleFinishCandidate: candidate,
1259
- candidateQuiet,
1260
- canFinishImmediately,
1261
- submitPendingUntil: this.submitPendingUntil,
1262
- responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
1263
- ...buildCliTraceParseSnapshot({
1264
- accumulatedBuffer: this.accumulatedBuffer,
1265
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1266
- responseBuffer: this.responseBuffer,
1267
- partialResponse: this.responseBuffer,
1268
- scope: this.currentTurnScope,
1269
- }),
1270
- });
997
+ this.applyIdle(ctx, now);
998
+ }
999
+ }
1271
1000
 
1272
- if (canFinishImmediately) {
1273
- this.clearIdleFinishCandidate('finish_response');
1274
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1275
- this.finishResponse();
1276
- return;
1277
- }
1001
+ // Returns false if the caller should bail out (debounce pending).
1002
+ private applyPendingScriptStatusDebounce(ctx: SettledEvalContext): boolean {
1003
+ const { now, scriptStatus, prevStatus } = ctx;
1004
+ const shouldDebounce =
1005
+ prevStatus === 'idle'
1006
+ && !this.isWaitingForResponse
1007
+ && !this.currentTurnScope
1008
+ && (scriptStatus === 'generating' || scriptStatus === 'waiting_approval');
1278
1009
 
1279
- if (idleReady) {
1280
- if (!candidate) {
1281
- this.armIdleFinishCandidate(assistantLength);
1282
- return;
1283
- }
1284
- } else {
1285
- this.clearIdleFinishCandidate('idle_not_ready');
1286
- }
1010
+ if (!shouldDebounce) {
1011
+ this.pendingScriptStatus = null;
1012
+ this.pendingScriptStatusSince = 0;
1013
+ if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
1014
+ return true;
1015
+ }
1016
+
1017
+ const armPending = (delayMs: number) => {
1018
+ if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
1019
+ this.pendingScriptStatusTimer = setTimeout(() => {
1020
+ this.pendingScriptStatusTimer = null;
1021
+ this.settledBuffer = this.recentOutputBuffer;
1022
+ this.evaluateSettled();
1023
+ }, delayMs);
1024
+ };
1025
+
1026
+ if (this.pendingScriptStatus !== scriptStatus) {
1027
+ this.pendingScriptStatus = scriptStatus as 'generating' | 'waiting_approval';
1028
+ this.pendingScriptStatusSince = now;
1029
+ armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS);
1030
+ return false;
1031
+ }
1032
+ const elapsed = now - this.pendingScriptStatusSince;
1033
+ if (elapsed < ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS) {
1034
+ armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS - elapsed);
1035
+ return false;
1036
+ }
1037
+ return true;
1038
+ }
1039
+
1040
+ private applyHoldGenerating(ctx: SettledEvalContext, recentInteractiveActivity: boolean): void {
1041
+ const { scriptStatus } = ctx;
1042
+ this.clearIdleFinishCandidate('hold_generating_recent_activity');
1043
+ this.setStatus('generating', 'recent_activity_hold');
1044
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1045
+ this.idleTimeout = setTimeout(() => {
1046
+ if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1047
+ if (this.shouldDeferIdleTimeoutFinish()) return;
1048
+ this.finishResponse();
1049
+ }
1050
+ }, this.timeouts.generatingIdle);
1051
+ this.recordTrace('hold_generating_recent_activity', {
1052
+ scriptStatus,
1053
+ recentInteractiveActivity,
1054
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1055
+ lastScreenChangeAt: this.lastScreenChangeAt,
1056
+ holdMs: this.getStatusActivityHoldMs(),
1057
+ ...buildCliTraceParseSnapshot({
1058
+ accumulatedBuffer: this.accumulatedBuffer,
1059
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1060
+ responseBuffer: this.responseBuffer,
1061
+ partialResponse: this.responseBuffer,
1062
+ scope: this.currentTurnScope,
1063
+ }),
1064
+ });
1065
+ this.onStatusChange?.();
1066
+ }
1287
1067
 
1068
+ private applyWaitingApproval(ctx: SettledEvalContext): void {
1069
+ const { modal } = ctx;
1070
+ this.clearIdleFinishCandidate('waiting_approval');
1071
+ const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
1072
+ if (inCooldown && !modal) {
1073
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1074
+ this.activeModal = null;
1075
+ if (this.isWaitingForResponse) {
1076
+ this.setStatus('generating', inCooldown ? 'approval_cooldown_ignore' : 'approval_prompt_gone');
1288
1077
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
1289
1078
  this.idleTimeout = setTimeout(() => {
1290
1079
  if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1291
1080
  if (this.shouldDeferIdleTimeoutFinish()) return;
1292
- this.clearIdleFinishCandidate('idle_timeout_finish');
1293
1081
  this.finishResponse();
1294
1082
  }
1295
- }, this.timeouts.idleFinish);
1296
- } else if (prevStatus !== 'idle') {
1083
+ }, this.timeouts.generatingIdle);
1084
+ } else {
1085
+ this.setStatus('idle', inCooldown ? 'approval_cooldown_ignore' : 'approval_prompt_gone');
1086
+ }
1087
+ this.onStatusChange?.();
1088
+ return;
1089
+ }
1090
+ if (!inCooldown) {
1091
+ if (!modal) {
1092
+ LOG.warn('CLI', `[${this.cliType}] detectStatus reported waiting_approval without parseApproval modal; ignoring non-actionable approval state`);
1093
+ return;
1094
+ }
1095
+ this.isWaitingForResponse = true;
1096
+ this.setStatus('waiting_approval', 'script_detect');
1097
+ this.activeModal = modal;
1098
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1099
+ this.armApprovalExitTimeout();
1100
+ this.onStatusChange?.();
1101
+ }
1102
+ }
1103
+
1104
+ private applyGenerating(ctx: SettledEvalContext): void {
1105
+ const { screenText, modal, parsedShowsLiveAssistantProgress, prevStatus } = ctx;
1106
+ this.clearIdleFinishCandidate('generating');
1107
+ const effectiveScreenText = screenText || this.accumulatedBuffer;
1108
+ const noActiveTurn = !this.currentTurnScope;
1109
+ const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText)
1110
+ || (/accept edits on/i.test(effectiveScreenText)
1111
+ && (/Update available!/i.test(screenText)
1112
+ || /\/effort/i.test(screenText)
1113
+ || /^.*➜\s+\S+/m.test(effectiveScreenText)));
1114
+ if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
1115
+ return;
1116
+ }
1117
+ if (prevStatus === 'waiting_approval') {
1118
+ // Transitioned out of approval → generating
1119
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1120
+ this.activeModal = null;
1121
+ this.lastApprovalResolvedAt = Date.now();
1122
+ }
1123
+ if (!this.isWaitingForResponse) {
1124
+ this.isWaitingForResponse = true;
1125
+ this.responseBuffer = '';
1126
+ }
1127
+ this.setStatus('generating', 'script_detect');
1128
+ // Reset idle timeout
1129
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1130
+ this.idleTimeout = setTimeout(() => {
1131
+ if (this.isWaitingForResponse) {
1132
+ if (this.shouldDeferIdleTimeoutFinish()) return;
1133
+ this.finishResponse();
1134
+ }
1135
+ }, this.timeouts.generatingIdle);
1136
+ this.onStatusChange?.();
1137
+ }
1138
+
1139
+ private applyIdle(ctx: SettledEvalContext, now: number): void {
1140
+ const { screenText, modal, lastParsedAssistant, prevStatus } = ctx;
1141
+ if (prevStatus === 'waiting_approval') {
1142
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1143
+ this.activeModal = null;
1144
+ this.lastApprovalResolvedAt = Date.now();
1145
+ }
1146
+ if (!this.isWaitingForResponse) {
1147
+ if (prevStatus !== 'idle') {
1297
1148
  this.clearIdleFinishCandidate('idle_without_response');
1298
1149
  this.setStatus('idle', 'script_detect');
1299
1150
  this.onStatusChange?.();
1300
1151
  }
1152
+ return;
1301
1153
  }
1154
+ const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
1155
+ const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1156
+ const hasAssistantTurn = !!lastParsedAssistant;
1157
+ const assistantLength = lastParsedAssistant?.content?.length || 0;
1158
+ const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
1159
+ const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
1160
+ const idleReady = !modal
1161
+ && hasAssistantTurn
1162
+ && quietForMs >= idleQuietThresholdMs
1163
+ && screenStableMs >= idleFinishConfirmMs;
1164
+ const candidate = this.idleFinishCandidate;
1165
+ const candidateQuiet = !!candidate
1166
+ && candidate.responseEpoch === this.responseEpoch
1167
+ && candidate.lastOutputAt === this.lastOutputAt
1168
+ && candidate.lastScreenChangeAt === this.lastScreenChangeAt
1169
+ && assistantLength >= candidate.assistantLength
1170
+ && (now - candidate.armedAt) >= idleFinishConfirmMs;
1171
+
1172
+ this.recordTrace('idle_decision', {
1173
+ quietForMs,
1174
+ screenStableMs,
1175
+ hasAssistantTurn,
1176
+ assistantLength,
1177
+ hasModal: !!modal,
1178
+ idleQuietThresholdMs,
1179
+ idleStableThresholdMs: idleFinishConfirmMs,
1180
+ idleReady,
1181
+ idleFinishConfirmMs,
1182
+ idleFinishCandidate: candidate,
1183
+ candidateQuiet,
1184
+ canFinishImmediately: idleReady && candidateQuiet,
1185
+ submitPendingUntil: this.submitPendingUntil,
1186
+ responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
1187
+ ...buildCliTraceParseSnapshot({
1188
+ accumulatedBuffer: this.accumulatedBuffer,
1189
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1190
+ responseBuffer: this.responseBuffer,
1191
+ partialResponse: this.responseBuffer,
1192
+ scope: this.currentTurnScope,
1193
+ }),
1194
+ });
1195
+
1196
+ if (idleReady && candidateQuiet) {
1197
+ this.clearIdleFinishCandidate('finish_response');
1198
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1199
+ this.finishResponse();
1200
+ return;
1201
+ }
1202
+
1203
+ if (idleReady) {
1204
+ if (!candidate) {
1205
+ this.armIdleFinishCandidate(assistantLength);
1206
+ return;
1207
+ }
1208
+ } else {
1209
+ this.clearIdleFinishCandidate('idle_not_ready');
1210
+ }
1211
+
1212
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1213
+ this.idleTimeout = setTimeout(() => {
1214
+ if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1215
+ if (this.shouldDeferIdleTimeoutFinish()) return;
1216
+ this.clearIdleFinishCandidate('idle_timeout_finish');
1217
+ this.finishResponse();
1218
+ }
1219
+ }, this.timeouts.idleFinish);
1302
1220
  }
1303
1221
 
1304
1222
  private finishResponse(): void {
@@ -1338,12 +1256,7 @@ export class ProviderCliAdapter implements CliAdapter {
1338
1256
  }, ProviderCliAdapter.FINISH_RETRY_DELAY_MS);
1339
1257
  return;
1340
1258
  }
1341
- if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
1342
- if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
1343
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1344
- if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
1345
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1346
-
1259
+ this.clearAllTimers();
1347
1260
  this.responseBuffer = '';
1348
1261
  this.isWaitingForResponse = false;
1349
1262
  this.responseSettleIgnoreUntil = 0;
@@ -1356,10 +1269,7 @@ export class ProviderCliAdapter implements CliAdapter {
1356
1269
  this.onStatusChange?.();
1357
1270
  }
1358
1271
 
1359
- private maybeCommitVisibleIdleTranscript(
1360
- parsed: any,
1361
- options?: { requireVisibleAssistantCandidate?: boolean; screenText?: string },
1362
- ): boolean {
1272
+ private maybeCommitVisibleIdleTranscript(parsed: any): boolean {
1363
1273
  const allowImmediateScriptIdleCommit = this.provider.allowInputDuringGeneration === true;
1364
1274
  if (!allowImmediateScriptIdleCommit) return false;
1365
1275
  if (
@@ -1374,13 +1284,6 @@ export class ProviderCliAdapter implements CliAdapter {
1374
1284
  return false;
1375
1285
  }
1376
1286
 
1377
- if (options?.requireVisibleAssistantCandidate) {
1378
- const candidateText = options.screenText || this.terminalScreen.getText() || '';
1379
- if (!this.looksLikeVisibleAssistantCandidate(candidateText)) {
1380
- return false;
1381
- }
1382
- }
1383
-
1384
1287
  const hydratedForIdleCommit = normalizeCliParsedMessages(parsed.messages, {
1385
1288
  committedMessages: this.committedMessages,
1386
1289
  scope: this.currentTurnScope,
@@ -1390,18 +1293,8 @@ export class ProviderCliAdapter implements CliAdapter {
1390
1293
  if (!visibleAssistant) return false;
1391
1294
 
1392
1295
  this.committedMessages = hydratedForIdleCommit;
1393
- const promptForTrim = this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages);
1394
- if (promptForTrim) {
1395
- const lastAssistantForTrim = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
1396
- if (lastAssistantForTrim) {
1397
- lastAssistantForTrim.content = trimPromptEchoPrefix(lastAssistantForTrim.content, promptForTrim);
1398
- }
1399
- }
1400
- if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
1401
- if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
1402
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1403
- if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
1404
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1296
+ this.trimLastAssistantEcho(this.committedMessages, this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages));
1297
+ this.clearAllTimers();
1405
1298
  this.syncMessageViews();
1406
1299
  this.responseBuffer = '';
1407
1300
  this.isWaitingForResponse = false;
@@ -1432,13 +1325,7 @@ export class ProviderCliAdapter implements CliAdapter {
1432
1325
  scope: this.currentTurnScope,
1433
1326
  lastOutputAt: this.lastOutputAt,
1434
1327
  });
1435
- const promptForTrim = this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages);
1436
- if (promptForTrim) {
1437
- const lastAssistantForTrim = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
1438
- if (lastAssistantForTrim) {
1439
- lastAssistantForTrim.content = trimPromptEchoPrefix(lastAssistantForTrim.content, promptForTrim);
1440
- }
1441
- }
1328
+ this.trimLastAssistantEcho(this.committedMessages, this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages));
1442
1329
  this.syncMessageViews();
1443
1330
  const lastAssistant = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
1444
1331
  if (this.currentTurnScope) {
@@ -1499,7 +1386,7 @@ export class ProviderCliAdapter implements CliAdapter {
1499
1386
  screen: buildCliScreenSnapshot(screenText),
1500
1387
  tailScreen: buildCliScreenSnapshot(text.slice(-500)),
1501
1388
  });
1502
- return this.refineDetectedStatus(status, text, screenText || '');
1389
+ return status;
1503
1390
  } catch (e: any) {
1504
1391
  LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
1505
1392
  return null;
@@ -1552,19 +1439,18 @@ export class ProviderCliAdapter implements CliAdapter {
1552
1439
  return parsed;
1553
1440
  }
1554
1441
 
1555
- const startupModal = this.getStartupConfirmationModal(screenText || '');
1556
- const visibleModal = this.runParseApproval(recentBuffer) || startupModal;
1442
+ const visibleModal = this.runParseApproval(recentBuffer);
1557
1443
  if (visibleModal) {
1558
1444
  return parsed;
1559
1445
  }
1560
1446
 
1561
1447
  const detectedStatus = this.runDetectStatus(recentBuffer);
1562
- const fallbackStatus = detectedStatus && detectedStatus !== 'waiting_approval'
1448
+ const resolvedStatus = detectedStatus && detectedStatus !== 'waiting_approval'
1563
1449
  ? detectedStatus
1564
1450
  : ((this.isWaitingForResponse || this.currentTurnScope) ? 'generating' : (this.currentStatus === 'waiting_approval' ? 'idle' : this.currentStatus));
1565
1451
  return {
1566
1452
  ...parsed,
1567
- status: fallbackStatus,
1453
+ status: resolvedStatus,
1568
1454
  activeModal: null,
1569
1455
  };
1570
1456
  }
@@ -1572,8 +1458,7 @@ export class ProviderCliAdapter implements CliAdapter {
1572
1458
  // ─── Public API (CliAdapter) ───────────────────
1573
1459
 
1574
1460
  getStatus(): CliSessionStatus {
1575
- const screenText = this.terminalScreen.getText() || '';
1576
- const startupModal = this.startupParseGate ? this.getStartupConfirmationModal(screenText) : null;
1461
+ const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
1577
1462
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1578
1463
  let effectiveModal = startupModal || this.activeModal;
1579
1464
  if (!startupModal && !effectiveModal && typeof this.cliScripts?.parseOutput === 'function') {
@@ -1688,7 +1573,6 @@ export class ProviderCliAdapter implements CliAdapter {
1688
1573
  : message.timestamp,
1689
1574
  }));
1690
1575
  const parsedLastAssistant = [...parsedHydratedMessages].reverse().find((message) => message.role === 'assistant' && typeof message.content === 'string' && message.content.trim());
1691
- const visibleIdlePrompt = this.looksLikeVisibleIdlePrompt(screenText);
1692
1576
  const shouldAdoptParsedIdleReplay =
1693
1577
  !this.currentTurnScope
1694
1578
  && !this.activeModal
@@ -1700,7 +1584,7 @@ export class ProviderCliAdapter implements CliAdapter {
1700
1584
  this.currentStatus === 'generating'
1701
1585
  && this.isWaitingForResponse
1702
1586
  && parsed.status === 'idle'
1703
- && visibleIdlePrompt
1587
+ && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
1704
1588
  )
1705
1589
  );
1706
1590
  if (shouldAdoptParsedIdleReplay) {
@@ -1856,17 +1740,9 @@ export class ProviderCliAdapter implements CliAdapter {
1856
1740
  if (parsed && typeof parsed === 'object') {
1857
1741
  Object.assign(parsed, validateReadChatResultPayload(parsed, `${this.cliType} parseOutput`));
1858
1742
  }
1859
- const refinedStatus = this.refineDetectedStatus(typeof parsed?.status === 'string' ? parsed.status : null, input.recentBuffer, input.screenText);
1860
- if (parsed && refinedStatus && parsed.status !== refinedStatus) {
1861
- parsed.status = refinedStatus;
1862
- }
1863
1743
  const normalizedParsed = this.suppressStaleParsedApproval(parsed, input.recentBuffer, input.screenText);
1864
- const promptForTrim = scope?.prompt || getLastUserPromptText(baseMessages);
1865
- if (normalizedParsed && Array.isArray(normalizedParsed.messages) && promptForTrim) {
1866
- const lastAssistant = [...normalizedParsed.messages].reverse().find((message: any) => message?.role === 'assistant' && typeof message.content === 'string');
1867
- if (lastAssistant) {
1868
- lastAssistant.content = trimPromptEchoPrefix(lastAssistant.content, promptForTrim);
1869
- }
1744
+ if (normalizedParsed && Array.isArray(normalizedParsed.messages)) {
1745
+ this.trimLastAssistantEcho(normalizedParsed.messages, scope?.prompt || getLastUserPromptText(baseMessages));
1870
1746
  }
1871
1747
  this.parseErrorMessage = null;
1872
1748
  return normalizedParsed;
@@ -1896,13 +1772,11 @@ export class ProviderCliAdapter implements CliAdapter {
1896
1772
  LOG.warn('CLI', `[${this.cliType}] resolveAction error: ${e.message}`);
1897
1773
  }
1898
1774
  }
1899
- if (!promptText && data) {
1900
- // Default fallback
1901
- promptText = `Please fix the following issue:\n${data.title || ''}\n${data.explanation || ''}\n\n${data.message || ''}`.trim();
1902
- }
1903
- if (promptText) {
1904
- await this.sendMessage(promptText);
1775
+ if (!promptText) {
1776
+ LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not supply a prompt`);
1777
+ return;
1905
1778
  }
1779
+ await this.sendMessage(promptText);
1906
1780
  }
1907
1781
 
1908
1782
  async sendMessage(text: string): Promise<void> {
@@ -1923,9 +1797,7 @@ export class ProviderCliAdapter implements CliAdapter {
1923
1797
  }
1924
1798
  if (!this.ready) {
1925
1799
  this.resolveStartupState('send_precheck');
1926
- const screenText = this.terminalScreen.getText() || '';
1927
- const hasPrompt = this.looksLikeVisibleIdlePrompt(screenText);
1928
- if (hasPrompt && this.currentStatus === 'idle') {
1800
+ if (this.runDetectStatus(this.recentOutputBuffer) === 'idle' && this.currentStatus === 'idle') {
1929
1801
  this.ready = true;
1930
1802
  this.startupParseGate = false;
1931
1803
  LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
@@ -2038,7 +1910,10 @@ export class ProviderCliAdapter implements CliAdapter {
2038
1910
  if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return;
2039
1911
  const screenText = this.terminalScreen.getText();
2040
1912
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
2041
- 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;
1913
+ const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
1914
+ if (liveApproval) return;
1915
+ const liveStatus = this.runDetectStatus(screenText) || this.runDetectStatus(this.recentOutputBuffer);
1916
+ if (liveStatus === 'generating' || liveStatus === 'waiting_approval') return;
2042
1917
  this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
2043
1918
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
2044
1919
  this.recordTrace('submit_write', {
@@ -2076,6 +1951,10 @@ export class ProviderCliAdapter implements CliAdapter {
2076
1951
  if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return;
2077
1952
  const screenText = this.terminalScreen.getText();
2078
1953
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
1954
+ const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
1955
+ if (liveApproval) return;
1956
+ const liveStatus = this.runDetectStatus(screenText) || this.runDetectStatus(this.recentOutputBuffer);
1957
+ if (liveStatus === 'generating' || liveStatus === 'waiting_approval') return;
2079
1958
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
2080
1959
  this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
2081
1960
  this.recordTrace('submit_write', {
@@ -2223,17 +2102,9 @@ export class ProviderCliAdapter implements CliAdapter {
2223
2102
 
2224
2103
  shutdown(): void {
2225
2104
  this.clearIdleFinishCandidate('shutdown');
2226
- if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
2227
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
2228
- if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
2229
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
2230
- if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
2231
- if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
2232
- if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
2233
- if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
2105
+ this.clearAllTimers();
2234
2106
  this.pendingOutputParseBuffer = '';
2235
2107
  this.pendingTerminalQueryTail = '';
2236
- if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
2237
2108
  this.ptyOutputBuffer = '';
2238
2109
  this.finishRetryCount = 0;
2239
2110
  if (this.ptyProcess) {
@@ -2252,17 +2123,9 @@ export class ProviderCliAdapter implements CliAdapter {
2252
2123
 
2253
2124
  detach(): void {
2254
2125
  this.clearIdleFinishCandidate('detach');
2255
- if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
2256
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
2257
- if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
2258
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
2259
- if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
2260
- if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
2261
- if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
2262
- if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
2126
+ this.clearAllTimers();
2263
2127
  this.pendingOutputParseBuffer = '';
2264
2128
  this.pendingTerminalQueryTail = '';
2265
- if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
2266
2129
  this.ptyOutputBuffer = '';
2267
2130
  this.finishRetryCount = 0;
2268
2131
  if (this.ptyProcess) {
@@ -2314,8 +2177,7 @@ export class ProviderCliAdapter implements CliAdapter {
2314
2177
  }
2315
2178
 
2316
2179
  resolveModal(buttonIndex: number): void {
2317
- const screenText = this.terminalScreen.getText() || '';
2318
- let modal = this.activeModal || this.getStartupConfirmationModal(screenText);
2180
+ let modal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
2319
2181
  if (!modal && typeof this.cliScripts?.parseOutput === 'function') {
2320
2182
  try {
2321
2183
  const parsed = this.getScriptParsedStatus();
@@ -2350,12 +2212,7 @@ export class ProviderCliAdapter implements CliAdapter {
2350
2212
  }
2351
2213
  this.setStatus('generating', 'approval_resolved');
2352
2214
  this.onStatusChange?.();
2353
- const startupTrustModal = /Quick safety check|project trust|Confirm Claude Code project trust|trust (?:this project|the contents of this directory|the files in this folder)/i.test(String(modal?.message || ''));
2354
- if (startupTrustModal && buttonIndex in this.approvalKeys) {
2355
- this.ptyProcess.write(`${this.approvalKeys[buttonIndex]}\r`);
2356
- } else if (this.shouldResolveModalWithEnter(modal, buttonIndex)) {
2357
- this.ptyProcess.write('\r');
2358
- } else if (buttonIndex in this.approvalKeys) {
2215
+ if (buttonIndex in this.approvalKeys) {
2359
2216
  this.ptyProcess.write(this.approvalKeys[buttonIndex]);
2360
2217
  } else {
2361
2218
  const DOWN = '\x1B[B';
@@ -2376,7 +2233,7 @@ export class ProviderCliAdapter implements CliAdapter {
2376
2233
 
2377
2234
  getDebugState(): Record<string, any> {
2378
2235
  const screenText = sanitizeTerminalText(this.terminalScreen.getText());
2379
- const startupModal = this.startupParseGate ? this.getStartupConfirmationModal(screenText) : null;
2236
+ const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
2380
2237
  const effectiveStatus = this.projectEffectiveStatus(startupModal);
2381
2238
  const effectiveReady = this.ready || !!startupModal;
2382
2239
  return {