@adhdev/daemon-core 0.9.6 → 0.9.7

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,8 @@ 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;
988
- 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
993
- ? 'waiting_approval'
994
- : (parsedModal && parsedTranscript?.status === 'waiting_approval' ? 'waiting_approval' : rawScriptStatus);
892
+ const modal = this.runParseApproval(tail) || parsedModal;
893
+ const scriptStatus = this.runDetectStatus(tail);
995
894
  const parsedMessages = Array.isArray(parsedTranscript?.messages)
996
895
  ? normalizeCliParsedMessages(parsedTranscript.messages, {
997
896
  committedMessages: this.committedMessages,
@@ -1059,246 +958,262 @@ export class ProviderCliAdapter implements CliAdapter {
1059
958
  if (!scriptStatus) return;
1060
959
 
1061
960
  const prevStatus = this.currentStatus;
961
+ const ctx: SettledEvalContext = { now, screenText, modal, scriptStatus, parsedTranscript, parsedMessages, lastParsedAssistant, parsedShowsLiveAssistantProgress, prevStatus };
1062
962
 
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
- }
963
+ if (!this.applyPendingScriptStatusDebounce(ctx)) return;
1100
964
 
1101
965
  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
- }
966
+ LOG.info(
967
+ 'CLI',
968
+ `[${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)}`
969
+ );
970
+
1111
971
  const shouldHoldGenerating =
1112
972
  scriptStatus === 'idle'
1113
973
  && this.isWaitingForResponse
1114
974
  && !modal
1115
975
  && recentInteractiveActivity
1116
- && !(visibleIdlePrompt && visibleAssistantCandidate)
1117
976
  && !(parsedTranscript?.status === 'idle' && !!lastParsedAssistant);
1118
977
 
1119
978
  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?.();
979
+ this.applyHoldGenerating(ctx, recentInteractiveActivity);
1144
980
  return;
1145
981
  }
1146
982
 
1147
983
  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
- }
984
+ this.applyWaitingApproval(ctx);
985
+ return;
1181
986
  }
1182
987
 
1183
988
  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?.();
989
+ this.applyGenerating(ctx);
1215
990
  return;
1216
991
  }
1217
992
 
1218
993
  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
- });
994
+ this.applyIdle(ctx, now);
995
+ }
996
+ }
1271
997
 
1272
- if (canFinishImmediately) {
1273
- this.clearIdleFinishCandidate('finish_response');
1274
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1275
- this.finishResponse();
1276
- return;
1277
- }
998
+ // Returns false if the caller should bail out (debounce pending).
999
+ private applyPendingScriptStatusDebounce(ctx: SettledEvalContext): boolean {
1000
+ const { now, scriptStatus, prevStatus } = ctx;
1001
+ const shouldDebounce =
1002
+ prevStatus === 'idle'
1003
+ && !this.isWaitingForResponse
1004
+ && !this.currentTurnScope
1005
+ && (scriptStatus === 'generating' || scriptStatus === 'waiting_approval');
1278
1006
 
1279
- if (idleReady) {
1280
- if (!candidate) {
1281
- this.armIdleFinishCandidate(assistantLength);
1282
- return;
1283
- }
1284
- } else {
1285
- this.clearIdleFinishCandidate('idle_not_ready');
1286
- }
1007
+ if (!shouldDebounce) {
1008
+ this.pendingScriptStatus = null;
1009
+ this.pendingScriptStatusSince = 0;
1010
+ if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
1011
+ return true;
1012
+ }
1013
+
1014
+ const armPending = (delayMs: number) => {
1015
+ if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
1016
+ this.pendingScriptStatusTimer = setTimeout(() => {
1017
+ this.pendingScriptStatusTimer = null;
1018
+ this.settledBuffer = this.recentOutputBuffer;
1019
+ this.evaluateSettled();
1020
+ }, delayMs);
1021
+ };
1022
+
1023
+ if (this.pendingScriptStatus !== scriptStatus) {
1024
+ this.pendingScriptStatus = scriptStatus as 'generating' | 'waiting_approval';
1025
+ this.pendingScriptStatusSince = now;
1026
+ armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS);
1027
+ return false;
1028
+ }
1029
+ const elapsed = now - this.pendingScriptStatusSince;
1030
+ if (elapsed < ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS) {
1031
+ armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS - elapsed);
1032
+ return false;
1033
+ }
1034
+ return true;
1035
+ }
1036
+
1037
+ private applyHoldGenerating(ctx: SettledEvalContext, recentInteractiveActivity: boolean): void {
1038
+ const { scriptStatus } = ctx;
1039
+ this.clearIdleFinishCandidate('hold_generating_recent_activity');
1040
+ this.setStatus('generating', 'recent_activity_hold');
1041
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1042
+ this.idleTimeout = setTimeout(() => {
1043
+ if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1044
+ if (this.shouldDeferIdleTimeoutFinish()) return;
1045
+ this.finishResponse();
1046
+ }
1047
+ }, this.timeouts.generatingIdle);
1048
+ this.recordTrace('hold_generating_recent_activity', {
1049
+ scriptStatus,
1050
+ recentInteractiveActivity,
1051
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1052
+ lastScreenChangeAt: this.lastScreenChangeAt,
1053
+ holdMs: this.getStatusActivityHoldMs(),
1054
+ ...buildCliTraceParseSnapshot({
1055
+ accumulatedBuffer: this.accumulatedBuffer,
1056
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1057
+ responseBuffer: this.responseBuffer,
1058
+ partialResponse: this.responseBuffer,
1059
+ scope: this.currentTurnScope,
1060
+ }),
1061
+ });
1062
+ this.onStatusChange?.();
1063
+ }
1287
1064
 
1065
+ private applyWaitingApproval(ctx: SettledEvalContext): void {
1066
+ const { modal } = ctx;
1067
+ this.clearIdleFinishCandidate('waiting_approval');
1068
+ const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
1069
+ if (inCooldown && !modal) {
1070
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1071
+ this.activeModal = null;
1072
+ if (this.isWaitingForResponse) {
1073
+ this.setStatus('generating', inCooldown ? 'approval_cooldown_ignore' : 'approval_prompt_gone');
1288
1074
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
1289
1075
  this.idleTimeout = setTimeout(() => {
1290
1076
  if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1291
1077
  if (this.shouldDeferIdleTimeoutFinish()) return;
1292
- this.clearIdleFinishCandidate('idle_timeout_finish');
1293
1078
  this.finishResponse();
1294
1079
  }
1295
- }, this.timeouts.idleFinish);
1296
- } else if (prevStatus !== 'idle') {
1080
+ }, this.timeouts.generatingIdle);
1081
+ } else {
1082
+ this.setStatus('idle', inCooldown ? 'approval_cooldown_ignore' : 'approval_prompt_gone');
1083
+ }
1084
+ this.onStatusChange?.();
1085
+ return;
1086
+ }
1087
+ if (!inCooldown) {
1088
+ if (!modal) {
1089
+ LOG.warn('CLI', `[${this.cliType}] detectStatus reported waiting_approval without parseApproval modal; ignoring non-actionable approval state`);
1090
+ return;
1091
+ }
1092
+ this.isWaitingForResponse = true;
1093
+ this.setStatus('waiting_approval', 'script_detect');
1094
+ this.activeModal = modal;
1095
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1096
+ this.armApprovalExitTimeout();
1097
+ this.onStatusChange?.();
1098
+ }
1099
+ }
1100
+
1101
+ private applyGenerating(ctx: SettledEvalContext): void {
1102
+ const { screenText, modal, parsedShowsLiveAssistantProgress, prevStatus } = ctx;
1103
+ this.clearIdleFinishCandidate('generating');
1104
+ const effectiveScreenText = screenText || this.accumulatedBuffer;
1105
+ const noActiveTurn = !this.currentTurnScope;
1106
+ const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText)
1107
+ || (/accept edits on/i.test(effectiveScreenText)
1108
+ && (/Update available!/i.test(screenText)
1109
+ || /\/effort/i.test(screenText)
1110
+ || /^.*➜\s+\S+/m.test(effectiveScreenText)));
1111
+ if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
1112
+ return;
1113
+ }
1114
+ if (prevStatus === 'waiting_approval') {
1115
+ // Transitioned out of approval → generating
1116
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1117
+ this.activeModal = null;
1118
+ this.lastApprovalResolvedAt = Date.now();
1119
+ }
1120
+ if (!this.isWaitingForResponse) {
1121
+ this.isWaitingForResponse = true;
1122
+ this.responseBuffer = '';
1123
+ }
1124
+ this.setStatus('generating', 'script_detect');
1125
+ // Reset idle timeout
1126
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1127
+ this.idleTimeout = setTimeout(() => {
1128
+ if (this.isWaitingForResponse) {
1129
+ if (this.shouldDeferIdleTimeoutFinish()) return;
1130
+ this.finishResponse();
1131
+ }
1132
+ }, this.timeouts.generatingIdle);
1133
+ this.onStatusChange?.();
1134
+ }
1135
+
1136
+ private applyIdle(ctx: SettledEvalContext, now: number): void {
1137
+ const { screenText, modal, lastParsedAssistant, prevStatus } = ctx;
1138
+ if (prevStatus === 'waiting_approval') {
1139
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1140
+ this.activeModal = null;
1141
+ this.lastApprovalResolvedAt = Date.now();
1142
+ }
1143
+ if (!this.isWaitingForResponse) {
1144
+ if (prevStatus !== 'idle') {
1297
1145
  this.clearIdleFinishCandidate('idle_without_response');
1298
1146
  this.setStatus('idle', 'script_detect');
1299
1147
  this.onStatusChange?.();
1300
1148
  }
1149
+ return;
1301
1150
  }
1151
+ const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
1152
+ const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1153
+ const hasAssistantTurn = !!lastParsedAssistant;
1154
+ const assistantLength = lastParsedAssistant?.content?.length || 0;
1155
+ const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
1156
+ const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
1157
+ const idleReady = !modal
1158
+ && hasAssistantTurn
1159
+ && quietForMs >= idleQuietThresholdMs
1160
+ && screenStableMs >= idleFinishConfirmMs;
1161
+ const candidate = this.idleFinishCandidate;
1162
+ const candidateQuiet = !!candidate
1163
+ && candidate.responseEpoch === this.responseEpoch
1164
+ && candidate.lastOutputAt === this.lastOutputAt
1165
+ && candidate.lastScreenChangeAt === this.lastScreenChangeAt
1166
+ && assistantLength >= candidate.assistantLength
1167
+ && (now - candidate.armedAt) >= idleFinishConfirmMs;
1168
+
1169
+ this.recordTrace('idle_decision', {
1170
+ quietForMs,
1171
+ screenStableMs,
1172
+ hasAssistantTurn,
1173
+ assistantLength,
1174
+ hasModal: !!modal,
1175
+ idleQuietThresholdMs,
1176
+ idleStableThresholdMs: idleFinishConfirmMs,
1177
+ idleReady,
1178
+ idleFinishConfirmMs,
1179
+ idleFinishCandidate: candidate,
1180
+ candidateQuiet,
1181
+ canFinishImmediately: idleReady && candidateQuiet,
1182
+ submitPendingUntil: this.submitPendingUntil,
1183
+ responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
1184
+ ...buildCliTraceParseSnapshot({
1185
+ accumulatedBuffer: this.accumulatedBuffer,
1186
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1187
+ responseBuffer: this.responseBuffer,
1188
+ partialResponse: this.responseBuffer,
1189
+ scope: this.currentTurnScope,
1190
+ }),
1191
+ });
1192
+
1193
+ if (idleReady && candidateQuiet) {
1194
+ this.clearIdleFinishCandidate('finish_response');
1195
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1196
+ this.finishResponse();
1197
+ return;
1198
+ }
1199
+
1200
+ if (idleReady) {
1201
+ if (!candidate) {
1202
+ this.armIdleFinishCandidate(assistantLength);
1203
+ return;
1204
+ }
1205
+ } else {
1206
+ this.clearIdleFinishCandidate('idle_not_ready');
1207
+ }
1208
+
1209
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1210
+ this.idleTimeout = setTimeout(() => {
1211
+ if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1212
+ if (this.shouldDeferIdleTimeoutFinish()) return;
1213
+ this.clearIdleFinishCandidate('idle_timeout_finish');
1214
+ this.finishResponse();
1215
+ }
1216
+ }, this.timeouts.idleFinish);
1302
1217
  }
1303
1218
 
1304
1219
  private finishResponse(): void {
@@ -1338,12 +1253,7 @@ export class ProviderCliAdapter implements CliAdapter {
1338
1253
  }, ProviderCliAdapter.FINISH_RETRY_DELAY_MS);
1339
1254
  return;
1340
1255
  }
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
-
1256
+ this.clearAllTimers();
1347
1257
  this.responseBuffer = '';
1348
1258
  this.isWaitingForResponse = false;
1349
1259
  this.responseSettleIgnoreUntil = 0;
@@ -1356,10 +1266,7 @@ export class ProviderCliAdapter implements CliAdapter {
1356
1266
  this.onStatusChange?.();
1357
1267
  }
1358
1268
 
1359
- private maybeCommitVisibleIdleTranscript(
1360
- parsed: any,
1361
- options?: { requireVisibleAssistantCandidate?: boolean; screenText?: string },
1362
- ): boolean {
1269
+ private maybeCommitVisibleIdleTranscript(parsed: any): boolean {
1363
1270
  const allowImmediateScriptIdleCommit = this.provider.allowInputDuringGeneration === true;
1364
1271
  if (!allowImmediateScriptIdleCommit) return false;
1365
1272
  if (
@@ -1374,13 +1281,6 @@ export class ProviderCliAdapter implements CliAdapter {
1374
1281
  return false;
1375
1282
  }
1376
1283
 
1377
- if (options?.requireVisibleAssistantCandidate) {
1378
- const candidateText = options.screenText || this.terminalScreen.getText() || '';
1379
- if (!this.looksLikeVisibleAssistantCandidate(candidateText)) {
1380
- return false;
1381
- }
1382
- }
1383
-
1384
1284
  const hydratedForIdleCommit = normalizeCliParsedMessages(parsed.messages, {
1385
1285
  committedMessages: this.committedMessages,
1386
1286
  scope: this.currentTurnScope,
@@ -1390,18 +1290,8 @@ export class ProviderCliAdapter implements CliAdapter {
1390
1290
  if (!visibleAssistant) return false;
1391
1291
 
1392
1292
  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; }
1293
+ this.trimLastAssistantEcho(this.committedMessages, this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages));
1294
+ this.clearAllTimers();
1405
1295
  this.syncMessageViews();
1406
1296
  this.responseBuffer = '';
1407
1297
  this.isWaitingForResponse = false;
@@ -1432,13 +1322,7 @@ export class ProviderCliAdapter implements CliAdapter {
1432
1322
  scope: this.currentTurnScope,
1433
1323
  lastOutputAt: this.lastOutputAt,
1434
1324
  });
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
- }
1325
+ this.trimLastAssistantEcho(this.committedMessages, this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages));
1442
1326
  this.syncMessageViews();
1443
1327
  const lastAssistant = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
1444
1328
  if (this.currentTurnScope) {
@@ -1499,7 +1383,7 @@ export class ProviderCliAdapter implements CliAdapter {
1499
1383
  screen: buildCliScreenSnapshot(screenText),
1500
1384
  tailScreen: buildCliScreenSnapshot(text.slice(-500)),
1501
1385
  });
1502
- return this.refineDetectedStatus(status, text, screenText || '');
1386
+ return status;
1503
1387
  } catch (e: any) {
1504
1388
  LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
1505
1389
  return null;
@@ -1552,19 +1436,18 @@ export class ProviderCliAdapter implements CliAdapter {
1552
1436
  return parsed;
1553
1437
  }
1554
1438
 
1555
- const startupModal = this.getStartupConfirmationModal(screenText || '');
1556
- const visibleModal = this.runParseApproval(recentBuffer) || startupModal;
1439
+ const visibleModal = this.runParseApproval(recentBuffer);
1557
1440
  if (visibleModal) {
1558
1441
  return parsed;
1559
1442
  }
1560
1443
 
1561
1444
  const detectedStatus = this.runDetectStatus(recentBuffer);
1562
- const fallbackStatus = detectedStatus && detectedStatus !== 'waiting_approval'
1445
+ const resolvedStatus = detectedStatus && detectedStatus !== 'waiting_approval'
1563
1446
  ? detectedStatus
1564
1447
  : ((this.isWaitingForResponse || this.currentTurnScope) ? 'generating' : (this.currentStatus === 'waiting_approval' ? 'idle' : this.currentStatus));
1565
1448
  return {
1566
1449
  ...parsed,
1567
- status: fallbackStatus,
1450
+ status: resolvedStatus,
1568
1451
  activeModal: null,
1569
1452
  };
1570
1453
  }
@@ -1572,8 +1455,7 @@ export class ProviderCliAdapter implements CliAdapter {
1572
1455
  // ─── Public API (CliAdapter) ───────────────────
1573
1456
 
1574
1457
  getStatus(): CliSessionStatus {
1575
- const screenText = this.terminalScreen.getText() || '';
1576
- const startupModal = this.startupParseGate ? this.getStartupConfirmationModal(screenText) : null;
1458
+ const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
1577
1459
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1578
1460
  let effectiveModal = startupModal || this.activeModal;
1579
1461
  if (!startupModal && !effectiveModal && typeof this.cliScripts?.parseOutput === 'function') {
@@ -1688,7 +1570,6 @@ export class ProviderCliAdapter implements CliAdapter {
1688
1570
  : message.timestamp,
1689
1571
  }));
1690
1572
  const parsedLastAssistant = [...parsedHydratedMessages].reverse().find((message) => message.role === 'assistant' && typeof message.content === 'string' && message.content.trim());
1691
- const visibleIdlePrompt = this.looksLikeVisibleIdlePrompt(screenText);
1692
1573
  const shouldAdoptParsedIdleReplay =
1693
1574
  !this.currentTurnScope
1694
1575
  && !this.activeModal
@@ -1700,7 +1581,7 @@ export class ProviderCliAdapter implements CliAdapter {
1700
1581
  this.currentStatus === 'generating'
1701
1582
  && this.isWaitingForResponse
1702
1583
  && parsed.status === 'idle'
1703
- && visibleIdlePrompt
1584
+ && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
1704
1585
  )
1705
1586
  );
1706
1587
  if (shouldAdoptParsedIdleReplay) {
@@ -1856,17 +1737,9 @@ export class ProviderCliAdapter implements CliAdapter {
1856
1737
  if (parsed && typeof parsed === 'object') {
1857
1738
  Object.assign(parsed, validateReadChatResultPayload(parsed, `${this.cliType} parseOutput`));
1858
1739
  }
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
1740
  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
- }
1741
+ if (normalizedParsed && Array.isArray(normalizedParsed.messages)) {
1742
+ this.trimLastAssistantEcho(normalizedParsed.messages, scope?.prompt || getLastUserPromptText(baseMessages));
1870
1743
  }
1871
1744
  this.parseErrorMessage = null;
1872
1745
  return normalizedParsed;
@@ -1896,13 +1769,11 @@ export class ProviderCliAdapter implements CliAdapter {
1896
1769
  LOG.warn('CLI', `[${this.cliType}] resolveAction error: ${e.message}`);
1897
1770
  }
1898
1771
  }
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);
1772
+ if (!promptText) {
1773
+ LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not supply a prompt`);
1774
+ return;
1905
1775
  }
1776
+ await this.sendMessage(promptText);
1906
1777
  }
1907
1778
 
1908
1779
  async sendMessage(text: string): Promise<void> {
@@ -1923,9 +1794,7 @@ export class ProviderCliAdapter implements CliAdapter {
1923
1794
  }
1924
1795
  if (!this.ready) {
1925
1796
  this.resolveStartupState('send_precheck');
1926
- const screenText = this.terminalScreen.getText() || '';
1927
- const hasPrompt = this.looksLikeVisibleIdlePrompt(screenText);
1928
- if (hasPrompt && this.currentStatus === 'idle') {
1797
+ if (this.runDetectStatus(this.recentOutputBuffer) === 'idle' && this.currentStatus === 'idle') {
1929
1798
  this.ready = true;
1930
1799
  this.startupParseGate = false;
1931
1800
  LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
@@ -2038,7 +1907,10 @@ export class ProviderCliAdapter implements CliAdapter {
2038
1907
  if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return;
2039
1908
  const screenText = this.terminalScreen.getText();
2040
1909
  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;
1910
+ const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
1911
+ if (liveApproval) return;
1912
+ const liveStatus = this.runDetectStatus(screenText) || this.runDetectStatus(this.recentOutputBuffer);
1913
+ if (liveStatus === 'generating' || liveStatus === 'waiting_approval') return;
2042
1914
  this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
2043
1915
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
2044
1916
  this.recordTrace('submit_write', {
@@ -2076,6 +1948,10 @@ export class ProviderCliAdapter implements CliAdapter {
2076
1948
  if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return;
2077
1949
  const screenText = this.terminalScreen.getText();
2078
1950
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
1951
+ const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
1952
+ if (liveApproval) return;
1953
+ const liveStatus = this.runDetectStatus(screenText) || this.runDetectStatus(this.recentOutputBuffer);
1954
+ if (liveStatus === 'generating' || liveStatus === 'waiting_approval') return;
2079
1955
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
2080
1956
  this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
2081
1957
  this.recordTrace('submit_write', {
@@ -2223,17 +2099,9 @@ export class ProviderCliAdapter implements CliAdapter {
2223
2099
 
2224
2100
  shutdown(): void {
2225
2101
  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; }
2102
+ this.clearAllTimers();
2234
2103
  this.pendingOutputParseBuffer = '';
2235
2104
  this.pendingTerminalQueryTail = '';
2236
- if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
2237
2105
  this.ptyOutputBuffer = '';
2238
2106
  this.finishRetryCount = 0;
2239
2107
  if (this.ptyProcess) {
@@ -2252,17 +2120,9 @@ export class ProviderCliAdapter implements CliAdapter {
2252
2120
 
2253
2121
  detach(): void {
2254
2122
  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; }
2123
+ this.clearAllTimers();
2263
2124
  this.pendingOutputParseBuffer = '';
2264
2125
  this.pendingTerminalQueryTail = '';
2265
- if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
2266
2126
  this.ptyOutputBuffer = '';
2267
2127
  this.finishRetryCount = 0;
2268
2128
  if (this.ptyProcess) {
@@ -2314,8 +2174,7 @@ export class ProviderCliAdapter implements CliAdapter {
2314
2174
  }
2315
2175
 
2316
2176
  resolveModal(buttonIndex: number): void {
2317
- const screenText = this.terminalScreen.getText() || '';
2318
- let modal = this.activeModal || this.getStartupConfirmationModal(screenText);
2177
+ let modal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
2319
2178
  if (!modal && typeof this.cliScripts?.parseOutput === 'function') {
2320
2179
  try {
2321
2180
  const parsed = this.getScriptParsedStatus();
@@ -2350,12 +2209,7 @@ export class ProviderCliAdapter implements CliAdapter {
2350
2209
  }
2351
2210
  this.setStatus('generating', 'approval_resolved');
2352
2211
  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) {
2212
+ if (buttonIndex in this.approvalKeys) {
2359
2213
  this.ptyProcess.write(this.approvalKeys[buttonIndex]);
2360
2214
  } else {
2361
2215
  const DOWN = '\x1B[B';
@@ -2376,7 +2230,7 @@ export class ProviderCliAdapter implements CliAdapter {
2376
2230
 
2377
2231
  getDebugState(): Record<string, any> {
2378
2232
  const screenText = sanitizeTerminalText(this.terminalScreen.getText());
2379
- const startupModal = this.startupParseGate ? this.getStartupConfirmationModal(screenText) : null;
2233
+ const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
2380
2234
  const effectiveStatus = this.projectEffectiveStatus(startupModal);
2381
2235
  const effectiveReady = this.ready || !!startupModal;
2382
2236
  return {