@adhdev/daemon-core 0.9.82-rc.169 → 0.9.82-rc.170

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.
@@ -36,6 +36,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
36
36
  terminalLedgerEvidence?: undefined;
37
37
  terminalLedgerKind?: undefined;
38
38
  duplicateRefineTerminalEvent?: undefined;
39
+ duplicateApproval?: undefined;
39
40
  duplicateCompletion?: undefined;
40
41
  error?: undefined;
41
42
  bufferedForGeneratingCoordinator?: undefined;
@@ -47,6 +48,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
47
48
  terminalLedgerKind: unknown;
48
49
  intentionalCleanupStop?: undefined;
49
50
  duplicateRefineTerminalEvent?: undefined;
51
+ duplicateApproval?: undefined;
50
52
  duplicateCompletion?: undefined;
51
53
  error?: undefined;
52
54
  bufferedForGeneratingCoordinator?: undefined;
@@ -58,6 +60,19 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
58
60
  intentionalCleanupStop?: undefined;
59
61
  terminalLedgerEvidence?: undefined;
60
62
  terminalLedgerKind?: undefined;
63
+ duplicateApproval?: undefined;
64
+ duplicateCompletion?: undefined;
65
+ error?: undefined;
66
+ bufferedForGeneratingCoordinator?: undefined;
67
+ } | {
68
+ success: boolean;
69
+ forwarded: number;
70
+ suppressed: boolean;
71
+ duplicateApproval: boolean;
72
+ intentionalCleanupStop?: undefined;
73
+ terminalLedgerEvidence?: undefined;
74
+ terminalLedgerKind?: undefined;
75
+ duplicateRefineTerminalEvent?: undefined;
61
76
  duplicateCompletion?: undefined;
62
77
  error?: undefined;
63
78
  bufferedForGeneratingCoordinator?: undefined;
@@ -70,6 +85,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
70
85
  intentionalCleanupStop?: undefined;
71
86
  terminalLedgerKind?: undefined;
72
87
  duplicateRefineTerminalEvent?: undefined;
88
+ duplicateApproval?: undefined;
73
89
  error?: undefined;
74
90
  bufferedForGeneratingCoordinator?: undefined;
75
91
  } | {
@@ -81,6 +97,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
81
97
  terminalLedgerEvidence?: undefined;
82
98
  terminalLedgerKind?: undefined;
83
99
  duplicateRefineTerminalEvent?: undefined;
100
+ duplicateApproval?: undefined;
84
101
  error?: undefined;
85
102
  bufferedForGeneratingCoordinator?: undefined;
86
103
  } | {
@@ -91,6 +108,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
91
108
  terminalLedgerEvidence?: undefined;
92
109
  terminalLedgerKind?: undefined;
93
110
  duplicateRefineTerminalEvent?: undefined;
111
+ duplicateApproval?: undefined;
94
112
  duplicateCompletion?: undefined;
95
113
  error?: undefined;
96
114
  bufferedForGeneratingCoordinator?: undefined;
@@ -103,6 +121,7 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
103
121
  terminalLedgerEvidence?: undefined;
104
122
  terminalLedgerKind?: undefined;
105
123
  duplicateRefineTerminalEvent?: undefined;
124
+ duplicateApproval?: undefined;
106
125
  duplicateCompletion?: undefined;
107
126
  error?: undefined;
108
127
  } | {
@@ -45,7 +45,7 @@ export declare class CliProviderInstance implements ProviderInstance {
45
45
  private monitor;
46
46
  private generatingDebounceTimer;
47
47
  private generatingDebouncePending;
48
- private lastApprovalEventAt;
48
+ private lastApprovalEventFingerprint;
49
49
  private autoApproveBusy;
50
50
  private autoApproveBusyTimer;
51
51
  private lastAutoApprovalSignature;
@@ -64,6 +64,7 @@ export declare class CliProviderInstance implements ProviderInstance {
64
64
  private suppressIdleHistoryReplay;
65
65
  private errorMessage;
66
66
  private errorReason;
67
+ private activeInteractivePrompt;
67
68
  private presentationMode;
68
69
  private providerSessionId?;
69
70
  private launchMode;
@@ -11,6 +11,7 @@ import type { ProviderModule, ProviderResumeCapability } from './contracts.js';
11
11
  import type { AcpConfigOption, AcpMode, ProviderControlSchema, ProviderSummaryMetadata, SessionCapability } from '../shared-types.js';
12
12
  import type { MessageInputSupport } from './provider-input-support.js';
13
13
  import type { ChatMessage } from '../types.js';
14
+ import type { InteractivePrompt } from './types/interactive-prompt.js';
14
15
  export type ProviderStatus = 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting';
15
16
  export interface ProviderRuntimeWriteOwner {
16
17
  clientId: string;
@@ -42,6 +43,7 @@ export interface ActiveChatData {
42
43
  message: string;
43
44
  buttons: string[];
44
45
  } | null;
46
+ activeInteractivePrompt?: InteractivePrompt | null;
45
47
  inputContent?: string;
46
48
  }
47
49
  /** Standardized error reasons across all provider categories */
@@ -56,6 +58,7 @@ interface ProviderStateBase {
56
58
  status: ProviderStatus;
57
59
  /** chat data */
58
60
  activeChat: ActiveChatData | null;
61
+ activeInteractivePrompt?: InteractivePrompt | null;
59
62
  /** Workspace — project path or name (all categories) */
60
63
  workspace?: string | null;
61
64
  /** Runtime info (real-time detection) */
@@ -1,5 +1,6 @@
1
1
  import type { CliAdapter, CliAdapterStatus } from '../../cli-adapter-types.js';
2
2
  import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
3
+ import { type InteractivePromptResponse } from '../types/interactive-prompt.js';
3
4
  export declare class SpecCliAdapter implements CliAdapter {
4
5
  readonly cliType: string;
5
6
  readonly cliName: string;
@@ -21,6 +22,10 @@ export declare class SpecCliAdapter implements CliAdapter {
21
22
  private statusCallback;
22
23
  private ptyDataCallback;
23
24
  private partialResponse;
25
+ private activeInteractivePrompt;
26
+ private interactivePromptTransport;
27
+ private claudeTuiPromptCaptureInFlight;
28
+ private jsonLineTail;
24
29
  private exited;
25
30
  private spawned;
26
31
  private providerSessionId;
@@ -56,6 +61,7 @@ export declare class SpecCliAdapter implements CliAdapter {
56
61
  resize(cols: number, rows: number): void;
57
62
  resolveModal(buttonIndex: number): void;
58
63
  resolveAction(data: unknown): Promise<void>;
64
+ setInteractivePromptResponse(response: InteractivePromptResponse): Promise<void>;
59
65
  isApprovalRecentlyResolved(): boolean;
60
66
  clearHistory(): void;
61
67
  updateRuntimeSettings(): void;
@@ -82,4 +88,8 @@ export declare class SpecCliAdapter implements CliAdapter {
82
88
  updateRuntimeMeta(meta?: Record<string, unknown>): void;
83
89
  refreshProviderDefinition(): void;
84
90
  private handleEvent;
91
+ private detectInteractivePromptFromPtyChunk;
92
+ private maybeCaptureClaudeTuiPrompt;
93
+ private readClaudeTuiHeaders;
94
+ private captureClaudeTuiPrompt;
85
95
  }
@@ -123,6 +123,7 @@ export declare class SpecDriver {
123
123
  subscribe(listener: (ev: DashboardEvent) => void): () => void;
124
124
  start(): void;
125
125
  dispatch(cmd: DashboardCommand): void;
126
+ snapshot(): string;
126
127
  shutdown(): void;
127
128
  private loadSpecOrThrow;
128
129
  private buildAdapterOpts;
@@ -0,0 +1,48 @@
1
+ export interface InteractivePrompt {
2
+ promptId: string;
3
+ origin: 'cli' | 'mcp' | 'agent';
4
+ providerType: string;
5
+ createdAt: number;
6
+ questions: InteractiveQuestion[];
7
+ }
8
+ export interface InteractiveQuestion {
9
+ questionId: string;
10
+ question: string;
11
+ header?: string;
12
+ multiSelect: boolean;
13
+ options: InteractiveOption[];
14
+ allowFreeform?: boolean;
15
+ }
16
+ export interface InteractiveOption {
17
+ label: string;
18
+ description?: string;
19
+ preview?: string;
20
+ }
21
+ export interface InteractivePromptResponse {
22
+ promptId: string;
23
+ answers: Record<string, InteractiveAnswer>;
24
+ }
25
+ export interface InteractiveAnswer {
26
+ selectedLabels: string[];
27
+ freeformText?: string;
28
+ }
29
+ export declare function normalizeInteractivePrompt(raw: unknown): InteractivePrompt | null;
30
+ export declare function normalizeInteractivePromptResponse(raw: unknown): InteractivePromptResponse;
31
+ export declare function buildClaudeInteractiveToolResult(response: InteractivePromptResponse): string;
32
+ export interface ClaudeInteractiveTuiPage {
33
+ screenText: string;
34
+ header?: string;
35
+ }
36
+ export declare function detectClaudeAskUserQuestionPromptFromTuiPages(pages: ClaudeInteractiveTuiPage[], options: {
37
+ promptId: string;
38
+ providerType?: string;
39
+ createdAt?: number;
40
+ }): InteractivePrompt | null;
41
+ export declare function buildClaudeInteractiveTuiAnswerSteps(prompt: InteractivePrompt, response: InteractivePromptResponse): string[];
42
+ export declare function interactivePromptFromClaudeAskUserQuestion(input: unknown, options: {
43
+ promptId: string;
44
+ providerType: string;
45
+ createdAt?: number;
46
+ origin?: InteractivePrompt['origin'];
47
+ }): InteractivePrompt | null;
48
+ export declare function detectClaudeAskUserQuestionPromptFromJson(value: unknown, providerType?: string): InteractivePrompt | null;
@@ -14,6 +14,7 @@ import type { ActiveChatData as _ActiveChatData, ProviderErrorReason as _Provide
14
14
  import type { WorkspaceEntry } from './config/workspaces.js';
15
15
  import type { ProviderMeshCoordinatorConfig, ProviderResumeCapability } from './providers/contracts.js';
16
16
  import type { GitCompactSummary, GitWorkspaceUpdate, WorkspaceGitSubscriptionParams } from './git/git-types.js';
17
+ import type { InteractivePrompt } from './providers/types/interactive-prompt.js';
17
18
  export type { GitCommandName, GitCompactSummary, GitDiffSummary, GitFailureReason, GitFileChange, GitFileChangeStatus, GitRepoIdentity, GitRepoStatus, GitSnapshot, GitSnapshotCompareSummary, GitSnapshotReason, GitWorkspaceUpdate, WorkspaceGitSubscriptionParams, } from './git/git-types.js';
18
19
  export interface SessionActiveChatData extends Omit<_ActiveChatData, 'messages'> {
19
20
  messages?: _ActiveChatData['messages'];
@@ -49,6 +50,7 @@ export interface ReadChatSyncResult {
49
50
  message: string;
50
51
  buttons: string[];
51
52
  } | null;
53
+ activeInteractivePrompt?: InteractivePrompt | null;
52
54
  /**
53
55
  * Chat source provenance from ChatSourceMachine (A2). Carries the
54
56
  * selected source, transition cause, lock state, and legacy
@@ -275,6 +277,7 @@ export interface SessionEntry {
275
277
  runtimeRecoveryState?: string | null;
276
278
  resume?: ProviderResumeCapability;
277
279
  activeChat: SessionActiveChatData | null;
280
+ activeInteractivePrompt?: InteractivePrompt | null;
278
281
  capabilities?: SessionCapability[];
279
282
  /** Effective message input/media support for this session. Defaults fail-closed to text-only. */
280
283
  messageInput?: MessageInputSupport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.169",
3
+ "version": "0.9.82-rc.170",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { ChatMessage } from './types.js';
8
+ import type { InteractivePrompt, InteractivePromptResponse } from './providers/types/interactive-prompt.js';
8
9
 
9
10
  export interface CliAdapterStatus {
10
11
  status?: string;
@@ -13,6 +14,7 @@ export interface CliAdapterStatus {
13
14
  message: string;
14
15
  buttons: string[];
15
16
  } | null;
17
+ activeInteractivePrompt?: InteractivePrompt | null;
16
18
  providerSessionId?: string;
17
19
  errorMessage?: string;
18
20
  errorReason?: string;
@@ -60,6 +62,7 @@ export interface CliAdapter {
60
62
  setServerConn?(serverConn: unknown): void;
61
63
  clearHistory?(): void;
62
64
  resolveAction?(data: unknown): Promise<void>;
65
+ setInteractivePromptResponse?(response: InteractivePromptResponse): Promise<void>;
63
66
  resolveModal?(buttonIndex: number): void;
64
67
  isApprovalRecentlyResolved?(): boolean;
65
68
  // Raw PTY I/O (for terminal view)
@@ -16,6 +16,7 @@
16
16
 
17
17
  import * as os from 'os';
18
18
  import type { CliAdapter } from '../cli-adapter-types.js';
19
+ import type { InteractivePromptResponse } from '../providers/types/interactive-prompt.js';
19
20
  import { LOG } from '../logging/logger.js';
20
21
  import { getDebugRuntimeConfig } from '../logging/debug-config.js';
21
22
  import { TerminalScreen } from './terminal-screen.js';
@@ -1076,6 +1077,11 @@ export class ProviderCliAdapter implements CliAdapter {
1076
1077
  await this.sendMessage(promptText);
1077
1078
  }
1078
1079
 
1080
+ async setInteractivePromptResponse(_response: InteractivePromptResponse): Promise<void> {
1081
+ // Legacy TUI providers do not currently expose an interactive prompt
1082
+ // protocol. Spec-backed claude-cli implements the first real wiring.
1083
+ }
1084
+
1079
1085
  private isSubmitStuck(normalizedPromptSnippet: string): boolean {
1080
1086
  if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
1081
1087
  if (this.engine.hasActionableApproval()) return false;
@@ -4,6 +4,7 @@ import { execSync } from 'child_process';
4
4
  import type { ProviderResumeCapability } from '../providers/contracts.js';
5
5
  import type { ChatMessageKind } from '../providers/chat-message-normalization.js';
6
6
  import type { ChatBubbleState } from '../types.js';
7
+ import type { InteractivePrompt } from '../providers/types/interactive-prompt.js';
7
8
  import { sanitizeSpawnEnv } from './spawn-env.js';
8
9
 
9
10
  export interface CliChatMessage {
@@ -28,6 +29,7 @@ export interface CliSessionStatus {
28
29
  messages: CliChatMessage[];
29
30
  workingDir: string;
30
31
  activeModal: { message: string; buttons: string[] } | null;
32
+ activeInteractivePrompt?: InteractivePrompt | null;
31
33
  pendingOutboundCount?: number;
32
34
  pendingOutboundMessages?: Array<{
33
35
  id: string;
package/src/index.ts CHANGED
@@ -85,6 +85,21 @@ export type {
85
85
  InputMediaType,
86
86
  } from './shared-types.js';
87
87
 
88
+ export type {
89
+ InteractivePrompt,
90
+ InteractiveQuestion,
91
+ InteractiveOption,
92
+ InteractivePromptResponse,
93
+ InteractiveAnswer,
94
+ } from './providers/types/interactive-prompt.js';
95
+ export {
96
+ normalizeInteractivePrompt,
97
+ normalizeInteractivePromptResponse,
98
+ buildClaudeInteractiveToolResult,
99
+ interactivePromptFromClaudeAskUserQuestion,
100
+ detectClaudeAskUserQuestionPromptFromJson,
101
+ } from './providers/types/interactive-prompt.js';
102
+
88
103
  // ── Repo Mesh Types (cross-package) ──
89
104
  export type {
90
105
  RepoMesh,
@@ -508,6 +508,33 @@ function isDuplicateMeshCompletionEvent(args: {
508
508
  return false;
509
509
  }
510
510
 
511
+ function isDuplicateMeshApprovalEvent(args: {
512
+ meshId: string;
513
+ sessionId: string;
514
+ providerType?: string;
515
+ timestamp?: number | null;
516
+ modalMessage?: string;
517
+ modalButtons?: unknown;
518
+ }): boolean {
519
+ const modalButtons = Array.isArray(args.modalButtons)
520
+ ? args.modalButtons.map(button => String(button).trim()).filter(Boolean)
521
+ : [];
522
+ const approvalIdentity = Number.isFinite(args.timestamp)
523
+ ? String(args.timestamp)
524
+ : JSON.stringify({ message: args.modalMessage || '', buttons: modalButtons });
525
+ if (!approvalIdentity || approvalIdentity === '{"message":"","buttons":[]}') return false;
526
+ const fingerprint = [
527
+ args.meshId,
528
+ 'agent:waiting_approval',
529
+ args.sessionId,
530
+ args.providerType || '',
531
+ approvalIdentity,
532
+ ].join('::');
533
+ if (hasFingerprintSeen(fingerprint)) return true;
534
+ recordFingerprintSeen(fingerprint);
535
+ return false;
536
+ }
537
+
511
538
  function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
512
539
  const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
513
540
  if (!fingerprint) return false;
@@ -1214,6 +1241,20 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1214
1241
  }
1215
1242
 
1216
1243
  const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
1244
+ if (args.event === 'agent:waiting_approval' && eventSessionId) {
1245
+ const duplicateApproval = isDuplicateMeshApprovalEvent({
1246
+ meshId: args.meshId,
1247
+ sessionId: eventSessionId,
1248
+ providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
1249
+ timestamp: eventTimestamp,
1250
+ modalMessage: readNonEmptyString(args.metadataEvent.modalMessage) || undefined,
1251
+ modalButtons: args.metadataEvent.modalButtons,
1252
+ });
1253
+ if (duplicateApproval) {
1254
+ LOG.info('MeshEvents', `Suppressed duplicate approval event for mesh ${args.meshId} session ${eventSessionId}`);
1255
+ return { success: true, forwarded: 0, suppressed: true, duplicateApproval: true };
1256
+ }
1257
+ }
1217
1258
  if (args.event === 'agent:generating_completed' && eventSessionId) {
1218
1259
  const terminal = findRecentTerminalLedgerEvidence({
1219
1260
  meshId: args.meshId,
@@ -13,6 +13,7 @@ import { createRequire } from 'node:module';
13
13
  import { normalizeInputEnvelope, type ProviderModule, flattenContent, type InputEnvelope, type InputPart } from './contracts.js';
14
14
  import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport } from './provider-input-support.js';
15
15
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState, SessionModalState } from './provider-instance.js';
16
+ import { normalizeInteractivePrompt, normalizeInteractivePromptResponse, type InteractivePrompt } from './types/interactive-prompt.js';
16
17
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
17
18
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
18
19
  import { createCliAdapter } from './spec/route.js';
@@ -41,6 +42,7 @@ type CompletedDebouncePending = {
41
42
  duration: number;
42
43
  timestamp: number;
43
44
  firstObservedAt: number;
45
+ previousStatus: string;
44
46
  loggedBlockReason?: string;
45
47
  };
46
48
 
@@ -328,7 +330,7 @@ export class CliProviderInstance implements ProviderInstance {
328
330
  private monitor: StatusMonitor;
329
331
  private generatingDebounceTimer: NodeJS.Timeout | null = null;
330
332
  private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
331
- private lastApprovalEventAt = 0;
333
+ private lastApprovalEventFingerprint = '';
332
334
  private autoApproveBusy = false;
333
335
  private autoApproveBusyTimer: NodeJS.Timeout | null = null;
334
336
  private lastAutoApprovalSignature = '';
@@ -350,6 +352,7 @@ export class CliProviderInstance implements ProviderInstance {
350
352
  private suppressIdleHistoryReplay = false;
351
353
  private errorMessage: string | undefined = undefined;
352
354
  private errorReason: ProviderErrorReason | undefined = undefined;
355
+ private activeInteractivePrompt: InteractivePrompt | null = null;
353
356
 
354
357
  private presentationMode: 'terminal' | 'chat';
355
358
  private providerSessionId?: string;
@@ -525,6 +528,9 @@ export class CliProviderInstance implements ProviderInstance {
525
528
  // parseSession), with provider-loader.ts updated to store script source strings
526
529
  // alongside the loaded function references for extended-legacy providers.
527
530
  const adapterStatus = this.adapter.getStatus();
531
+ if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
532
+ this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
533
+ }
528
534
  let parsedStatus: any = null;
529
535
  let parseErrorMessage: string | undefined;
530
536
  if (typeof this.adapter.getScriptParsedStatus === 'function') {
@@ -674,8 +680,10 @@ export class CliProviderInstance implements ProviderInstance {
674
680
  status: activeChatStatus,
675
681
  messages: statusMessages,
676
682
  activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
683
+ activeInteractivePrompt: this.activeInteractivePrompt,
677
684
  inputContent: '',
678
685
  },
686
+ activeInteractivePrompt: this.activeInteractivePrompt,
679
687
  workspace: this.workingDir,
680
688
  instanceId: this.instanceId,
681
689
  providerSessionId: this.providerSessionId,
@@ -771,6 +779,32 @@ export class CliProviderInstance implements ProviderInstance {
771
779
  void this.adapter.resolveAction(data).catch((e: any) => {
772
780
  LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
773
781
  });
782
+ } else if (event === 'interactive_prompt' && data) {
783
+ const prompt = normalizeInteractivePrompt(data);
784
+ if (prompt) {
785
+ this.activeInteractivePrompt = prompt;
786
+ this.events.push({
787
+ event: 'interactive_prompt',
788
+ timestamp: Date.now(),
789
+ promptId: prompt.promptId,
790
+ });
791
+ }
792
+ } else if (event === 'interactive_prompt_response' && data) {
793
+ try {
794
+ const response = normalizeInteractivePromptResponse(data);
795
+ if (this.activeInteractivePrompt?.promptId === response.promptId) {
796
+ this.activeInteractivePrompt = null;
797
+ }
798
+ if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
799
+ LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
800
+ return;
801
+ }
802
+ void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
803
+ LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
804
+ });
805
+ } catch (e: any) {
806
+ LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
807
+ }
774
808
  } else if (event === 'provider_state_patch' && data && typeof data === 'object') {
775
809
  this.applyProviderResponse(data, { phase: 'immediate' });
776
810
  }
@@ -939,13 +973,16 @@ export class CliProviderInstance implements ProviderInstance {
939
973
  return true;
940
974
  }
941
975
 
942
- private getCompletedFinalizationBlock(latestVisibleStatus: string): CompletedFinalizationBlock | null {
976
+ private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
943
977
  if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
944
978
 
945
979
  const adapterAny = this.adapter as any;
946
- if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
947
- if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
948
- if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
980
+ const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
981
+ if (!approvalResolvedIdle) {
982
+ if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
983
+ if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
984
+ if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
985
+ }
949
986
 
950
987
  const partial = typeof this.adapter.getPartialResponse === 'function'
951
988
  ? this.adapter.getPartialResponse()
@@ -987,7 +1024,7 @@ export class CliProviderInstance implements ProviderInstance {
987
1024
  if (screenText) {
988
1025
  const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
989
1026
  if (looksLikeActiveApprovalPromptText(tailLines)) {
990
- return { reason: 'screen_shows_approval_prompt', terminal: false };
1027
+ return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
991
1028
  }
992
1029
  }
993
1030
  } catch { /* defensive: screen text read is best-effort */ }
@@ -1017,7 +1054,7 @@ export class CliProviderInstance implements ProviderInstance {
1017
1054
  return;
1018
1055
  }
1019
1056
 
1020
- const block = this.getCompletedFinalizationBlock(latestVisibleStatus);
1057
+ const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
1021
1058
  if (block) {
1022
1059
  const blockReason = block.reason;
1023
1060
  const waitedMs = Date.now() - pending.firstObservedAt;
@@ -1051,6 +1088,7 @@ export class CliProviderInstance implements ProviderInstance {
1051
1088
  this.completedDebouncePending = null;
1052
1089
  this.completedDebounceTimer = null;
1053
1090
  this.generatingStartedAt = 0;
1091
+ this.lastApprovalEventFingerprint = '';
1054
1092
  return;
1055
1093
  }
1056
1094
 
@@ -1065,6 +1103,7 @@ export class CliProviderInstance implements ProviderInstance {
1065
1103
  this.completedDebouncePending = null;
1066
1104
  this.completedDebounceTimer = null;
1067
1105
  this.generatingStartedAt = 0;
1106
+ this.lastApprovalEventFingerprint = '';
1068
1107
  }
1069
1108
 
1070
1109
  private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
@@ -1183,10 +1222,14 @@ export class CliProviderInstance implements ProviderInstance {
1183
1222
  if (!this.generatingStartedAt) this.generatingStartedAt = now;
1184
1223
  const modal = adapterStatus.activeModal;
1185
1224
  LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
1186
- // Only push event if not already in waiting_approval (prevent flood from rapid cycles)
1187
- const approvalCooldown = 5000;
1188
- if (this.lastStatus !== 'waiting_approval' && (!this.lastApprovalEventAt || now - this.lastApprovalEventAt > approvalCooldown)) {
1189
- this.lastApprovalEventAt = now;
1225
+ const approvalFingerprint = JSON.stringify({
1226
+ message: typeof modal?.message === 'string' ? modal.message.trim() : '',
1227
+ buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
1228
+ });
1229
+ // PTY redraws can briefly leave waiting_approval and then re-enter it.
1230
+ // Keep one coordinator event per logical modal until the turn completes.
1231
+ if (this.lastStatus !== 'waiting_approval' && approvalFingerprint !== this.lastApprovalEventFingerprint) {
1232
+ this.lastApprovalEventFingerprint = approvalFingerprint;
1190
1233
  this.appendRuntimeSystemMessage(
1191
1234
  this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
1192
1235
  `approval_request:${now}`,
@@ -1227,7 +1270,13 @@ export class CliProviderInstance implements ProviderInstance {
1227
1270
  } else {
1228
1271
  // Debounce completed, then require the rich transcript path that read_chat
1229
1272
  // uses to show an idle turn whose last user-facing message is assistant.
1230
- this.completedDebouncePending = { chatTitle, duration, timestamp: now, firstObservedAt: now };
1273
+ this.completedDebouncePending = {
1274
+ chatTitle,
1275
+ duration,
1276
+ timestamp: now,
1277
+ firstObservedAt: now,
1278
+ previousStatus: this.lastStatus,
1279
+ };
1231
1280
  this.scheduleCompletedDebounceFlush(3000);
1232
1281
  }
1233
1282
  } else if (newStatus === 'idle' && this.lastStatus === 'starting') {
@@ -12,6 +12,7 @@ import type { ProviderModule, ProviderSettingDef, ProviderResumeCapability } fro
12
12
  import type { AcpConfigOption, AcpMode, ProviderControlSchema, ProviderSummaryMetadata, SessionCapability } from '../shared-types.js';
13
13
  import type { MessageInputSupport } from './provider-input-support.js';
14
14
  import type { ChatMessage } from '../types.js';
15
+ import type { InteractivePrompt } from './types/interactive-prompt.js';
15
16
 
16
17
  // ─── ProviderState — Discriminated union by category ─────────────
17
18
 
@@ -47,6 +48,7 @@ export interface ActiveChatData {
47
48
  status: string;
48
49
  messages: ChatMessage[];
49
50
  activeModal: { message: string; buttons: string[] } | null;
51
+ activeInteractivePrompt?: InteractivePrompt | null;
50
52
  inputContent?: string;
51
53
  }
52
54
 
@@ -72,6 +74,7 @@ interface ProviderStateBase {
72
74
  status: ProviderStatus;
73
75
  /** chat data */
74
76
  activeChat: ActiveChatData | null;
77
+ activeInteractivePrompt?: InteractivePrompt | null;
75
78
  /** Workspace — project path or name (all categories) */
76
79
  workspace?: string | null;
77
80
  /** Runtime info (real-time detection) */