@adhdev/daemon-core 0.8.22 → 0.8.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/agent-stream/types.d.ts +3 -0
  2. package/dist/cli-adapter-types.d.ts +3 -0
  3. package/dist/cli-adapters/provider-cli-adapter.d.ts +71 -11
  4. package/dist/commands/stream-commands.d.ts +1 -0
  5. package/dist/config/config.d.ts +6 -0
  6. package/dist/index.js +1162 -307
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1162 -307
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/providers/cli-provider-instance.d.ts +6 -0
  11. package/dist/providers/contracts.d.ts +59 -1
  12. package/dist/providers/control-effects.d.ts +4 -0
  13. package/dist/providers/extension-provider-instance.d.ts +9 -0
  14. package/dist/providers/ide-provider-instance.d.ts +8 -0
  15. package/dist/shared-types.d.ts +2 -1
  16. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  17. package/package.json +3 -2
  18. package/src/agent-stream/forward.ts +2 -0
  19. package/src/agent-stream/provider-adapter.ts +5 -15
  20. package/src/agent-stream/types.ts +4 -0
  21. package/src/cli-adapter-types.ts +3 -0
  22. package/src/cli-adapters/provider-cli-adapter.ts +399 -49
  23. package/src/commands/handler.ts +1 -0
  24. package/src/commands/stream-commands.ts +99 -8
  25. package/src/config/config.d.ts +1 -0
  26. package/src/config/config.ts +9 -0
  27. package/src/launch.ts +57 -11
  28. package/src/providers/cli-provider-instance.ts +148 -2
  29. package/src/providers/contracts.ts +65 -2
  30. package/src/providers/control-effects.ts +114 -0
  31. package/src/providers/extension-provider-instance.ts +163 -3
  32. package/src/providers/ide-provider-instance.ts +181 -2
  33. package/src/shared-types.d.ts +1 -0
  34. package/src/shared-types.ts +2 -1
  35. package/src/status/snapshot.ts +1 -0
@@ -35,8 +35,19 @@ export interface CliChatMessage {
35
35
  role: 'user' | 'assistant';
36
36
  content: string;
37
37
  timestamp?: number;
38
+ receivedAt?: number;
39
+ kind?: string;
40
+ id?: string;
41
+ index?: number;
42
+ meta?: Record<string, any>;
43
+ senderName?: string;
38
44
  }
39
45
 
46
+ type SeedCliChatMessage = Omit<Partial<CliChatMessage>, 'role'> & {
47
+ role?: string;
48
+ content?: string;
49
+ };
50
+
40
51
  export interface CliSessionStatus {
41
52
  status: 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting';
42
53
  messages: CliChatMessage[];
@@ -53,23 +64,70 @@ export interface CliScripts {
53
64
  /** Full PTY buffer → ReadChatResult (messages, status, activeModal) */
54
65
  parseOutput?: (input: CliScriptInput) => any;
55
66
  /** Lightweight status detection (high-frequency polling) → AgentStatus string */
56
- detectStatus?: (input: { tail: string; screenText?: string; rawBuffer?: string }) => string | null;
67
+ detectStatus?: (input: CliStatusInput) => string | null;
57
68
  /** Parse approval modal from PTY output → ModalInfo | null */
58
- parseApproval?: (input: { buffer: string; screenText?: string; rawBuffer?: string; tail: string }) => { message: string; buttons: string[] } | null;
69
+ parseApproval?: (input: CliApprovalInput) => { message: string; buttons: string[] } | null;
59
70
  /** Produce a cli-specific prompt from a dashboard action payload */
60
71
  resolveAction?: (data: any) => string;
61
72
  /** Custom scripts */
62
73
  [name: string]: ((input: any) => any) | undefined;
63
74
  }
64
75
 
76
+ export interface CliScreenLine {
77
+ index: number;
78
+ fromTop: number;
79
+ fromBottom: number;
80
+ text: string;
81
+ trimmed: string;
82
+ isEmpty: boolean;
83
+ }
84
+
85
+ export interface CliScreenSnapshot {
86
+ text: string;
87
+ lineCount: number;
88
+ lines: CliScreenLine[];
89
+ nonEmptyLines: CliScreenLine[];
90
+ firstNonEmptyLineIndex: number;
91
+ lastNonEmptyLineIndex: number;
92
+ firstNonEmptyLine: CliScreenLine | null;
93
+ lastNonEmptyLine: CliScreenLine | null;
94
+ promptLineIndex: number;
95
+ promptLine: CliScreenLine | null;
96
+ linesAbovePrompt: CliScreenLine[];
97
+ linesBelowPrompt: CliScreenLine[];
98
+ }
99
+
65
100
  export interface CliScriptInput {
66
101
  buffer: string; // Full ANSI-stripped accumulated PTY output
67
102
  rawBuffer: string; // Raw PTY output (with ANSI)
68
103
  recentBuffer: string; // Recent 1000 chars (ANSI-stripped)
69
104
  screenText: string; // Current visible screen snapshot
105
+ screen: CliScreenSnapshot;
106
+ bufferScreen: CliScreenSnapshot;
107
+ recentScreen: CliScreenSnapshot;
70
108
  messages: CliChatMessage[]; // Previously parsed messages
71
109
  partialResponse: string; // Current partial response being generated
72
110
  promptText?: string; // Current turn prompt when available
111
+ settings?: Record<string, any>;
112
+ args?: Record<string, any>;
113
+ }
114
+
115
+ export interface CliStatusInput {
116
+ tail: string;
117
+ screenText?: string;
118
+ rawBuffer?: string;
119
+ screen: CliScreenSnapshot;
120
+ tailScreen: CliScreenSnapshot;
121
+ }
122
+
123
+ export interface CliApprovalInput {
124
+ buffer: string;
125
+ screenText?: string;
126
+ rawBuffer?: string;
127
+ tail: string;
128
+ screen: CliScreenSnapshot;
129
+ bufferScreen: CliScreenSnapshot;
130
+ tailScreen: CliScreenSnapshot;
73
131
  }
74
132
 
75
133
  interface TurnParseScope {
@@ -174,6 +232,61 @@ function sanitizeTerminalText(str: string): string {
174
232
  return stripTerminalNoise(stripAnsi(str));
175
233
  }
176
234
 
235
+ function splitCliScreenLines(text: string): string[] {
236
+ return String(text || '')
237
+ .replace(/\u0007/g, '')
238
+ .replace(/\r\n/g, '\n')
239
+ .replace(/\r/g, '\n')
240
+ .split('\n')
241
+ .map((line) => line.replace(/\s+$/, ''));
242
+ }
243
+
244
+ function isPromptLikeCliLine(line: string): boolean {
245
+ const trimmed = String(line || '').trim();
246
+ if (!trimmed) return false;
247
+ return /^[❯›>]\s*(?:$|\S.*)$/.test(trimmed);
248
+ }
249
+
250
+ function buildCliScreenSnapshot(text: string): CliScreenSnapshot {
251
+ const normalizedText = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
252
+ const rawLines = splitCliScreenLines(normalizedText);
253
+ const lines = rawLines.map((line, index, arr) => {
254
+ const trimmed = String(line || '').trim();
255
+ return {
256
+ index,
257
+ fromTop: index,
258
+ fromBottom: arr.length - index - 1,
259
+ text: line,
260
+ trimmed,
261
+ isEmpty: trimmed.length === 0,
262
+ };
263
+ });
264
+ const nonEmptyLines = lines.filter((line) => !line.isEmpty);
265
+ const firstNonEmptyLine = nonEmptyLines[0] ?? null;
266
+ const lastNonEmptyLine = nonEmptyLines[nonEmptyLines.length - 1] ?? null;
267
+ let promptLineIndex = -1;
268
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
269
+ if (isPromptLikeCliLine(lines[i].text)) {
270
+ promptLineIndex = i;
271
+ break;
272
+ }
273
+ }
274
+ return {
275
+ text: normalizedText,
276
+ lineCount: lines.length,
277
+ lines,
278
+ nonEmptyLines,
279
+ firstNonEmptyLineIndex: firstNonEmptyLine?.index ?? -1,
280
+ lastNonEmptyLineIndex: lastNonEmptyLine?.index ?? -1,
281
+ firstNonEmptyLine,
282
+ lastNonEmptyLine,
283
+ promptLineIndex,
284
+ promptLine: promptLineIndex >= 0 ? lines[promptLineIndex] : null,
285
+ linesAbovePrompt: promptLineIndex >= 0 ? lines.slice(0, promptLineIndex) : [...lines],
286
+ linesBelowPrompt: promptLineIndex >= 0 ? lines.slice(promptLineIndex + 1) : [],
287
+ };
288
+ }
289
+
177
290
  // Re-export sanitizeSpawnEnv under the local alias for backward compat within this file
178
291
  const buildCliSpawnEnv = sanitizeSpawnEnv;
179
292
 
@@ -412,7 +525,9 @@ export class ProviderCliAdapter implements CliAdapter {
412
525
  private ready = false;
413
526
  private startupBuffer = '';
414
527
  private startupParseGate = false;
528
+ private startupSettleTimer: NodeJS.Timeout | null = null;
415
529
  private spawnAt = 0;
530
+ private startupFirstOutputAt = 0;
416
531
 
417
532
  // PTY I/O
418
533
  private onPtyDataCallback: ((data: string) => void) | null = null;
@@ -459,8 +574,9 @@ export class ProviderCliAdapter implements CliAdapter {
459
574
  // Debug: status transition history
460
575
  private statusHistory: { status: string; at: number; trigger?: string }[] = [];
461
576
 
462
- // ─── CLI Scripts (script-based parsing) ───
577
+ // ─── CLI Scripts (script-based parsing) ───
463
578
  private cliScripts: CliScripts;
579
+ private runtimeSettings: Record<string, any> = {};
464
580
  /** Full accumulated ANSI-stripped PTY output */
465
581
  private accumulatedBuffer: string = '';
466
582
  /** Full accumulated raw PTY output (with ANSI) */
@@ -475,7 +591,8 @@ export class ProviderCliAdapter implements CliAdapter {
475
591
  private traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
476
592
  private static readonly MAX_TRACE_ENTRIES = 250;
477
593
  private readonly providerResolutionMeta: Record<string, any>;
478
- private static readonly IDLE_FINISH_CONFIRM_MS = 900;
594
+ private static readonly IDLE_FINISH_CONFIRM_MS = 2000;
595
+ private static readonly STATUS_ACTIVITY_HOLD_MS = 2000;
479
596
  private static readonly FINISH_RETRY_DELAY_MS = 300;
480
597
  private static readonly MAX_FINISH_RETRIES = 2;
481
598
 
@@ -484,7 +601,7 @@ export class ProviderCliAdapter implements CliAdapter {
484
601
  this.structuredMessages = [...this.committedMessages];
485
602
  }
486
603
 
487
- private normalizeParsedMessages(parsedMessages: any[]): CliChatMessage[] {
604
+ private hydrateParsedMessages(parsedMessages: any[], scope?: TurnParseScope | null): any[] {
488
605
  const referenceMessages = [...this.committedMessages];
489
606
  const usedReferenceIndexes = new Set<number>();
490
607
  const now = Date.now();
@@ -533,14 +650,36 @@ export class ProviderCliAdapter implements CliAdapter {
533
650
  ? message.timestamp
534
651
  : undefined;
535
652
  const referenceTimestamp = parsedTimestamp ?? findReferenceTimestamp(role, content, index);
653
+ const fallbackTimestamp = role === 'user'
654
+ ? (scope?.startedAt || now)
655
+ : (this.lastOutputAt || scope?.startedAt || now);
656
+ const timestamp = referenceTimestamp ?? fallbackTimestamp;
536
657
  return {
658
+ ...message,
537
659
  role,
538
660
  content,
539
- timestamp: referenceTimestamp ?? now,
661
+ timestamp,
662
+ receivedAt: typeof message.receivedAt === 'number' && Number.isFinite(message.receivedAt)
663
+ ? message.receivedAt
664
+ : timestamp,
540
665
  };
541
666
  });
542
667
  }
543
668
 
669
+ private normalizeParsedMessages(parsedMessages: any[], scope?: TurnParseScope | null): CliChatMessage[] {
670
+ return this.hydrateParsedMessages(parsedMessages, scope).map((message) => ({
671
+ role: message.role,
672
+ content: message.content,
673
+ timestamp: message.timestamp,
674
+ receivedAt: message.receivedAt,
675
+ kind: message.kind,
676
+ id: message.id,
677
+ index: message.index,
678
+ meta: message.meta,
679
+ senderName: message.senderName,
680
+ }));
681
+ }
682
+
544
683
  private sliceFromOffset(text: string, start: number): string {
545
684
  if (!text) return '';
546
685
  if (!Number.isFinite(start) || start <= 0) return text;
@@ -555,15 +694,21 @@ export class ProviderCliAdapter implements CliAdapter {
555
694
  const rawBuffer = scope
556
695
  ? (this.sliceFromOffset(this.accumulatedRawBuffer, scope.rawBufferStart) || this.accumulatedRawBuffer)
557
696
  : this.accumulatedRawBuffer;
697
+ const screenText = this.terminalScreen.getText();
698
+ const recentBuffer = buffer.slice(-1000) || this.recentOutputBuffer;
558
699
 
559
700
  return {
560
701
  buffer,
561
702
  rawBuffer,
562
- recentBuffer: buffer.slice(-1000) || this.recentOutputBuffer,
563
- screenText: this.terminalScreen.getText(),
703
+ recentBuffer,
704
+ screenText,
705
+ screen: buildCliScreenSnapshot(screenText),
706
+ bufferScreen: buildCliScreenSnapshot(buffer),
707
+ recentScreen: buildCliScreenSnapshot(recentBuffer),
564
708
  messages: [...baseMessages],
565
709
  partialResponse,
566
710
  promptText: scope?.prompt || '',
711
+ settings: { ...this.runtimeSettings },
567
712
  };
568
713
  }
569
714
 
@@ -746,6 +891,10 @@ export class ProviderCliAdapter implements CliAdapter {
746
891
  LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
747
892
  }
748
893
 
894
+ updateRuntimeSettings(settings: Record<string, any>): void {
895
+ this.runtimeSettings = { ...settings };
896
+ }
897
+
749
898
  // ─── Lifecycle ─────────────────────────────────
750
899
 
751
900
  setServerConn(serverConn: any): void {
@@ -796,7 +945,8 @@ export class ProviderCliAdapter implements CliAdapter {
796
945
  );
797
946
  // On Windows, .cmd/.bat shims cannot be spawned directly — must go through cmd.exe
798
947
  const isCmdShim = isWin && /\.(cmd|bat)$/i.test(binaryPath);
799
- const useShellWin = isCmdShim
948
+ const useShellWin = !!spawnConfig.shell
949
+ || isCmdShim
800
950
  || !path.isAbsolute(binaryPath)
801
951
  || isScriptBinary(binaryPath);
802
952
  const useShell = isWin ? useShellWin : useShellUnix;
@@ -916,6 +1066,8 @@ export class ProviderCliAdapter implements CliAdapter {
916
1066
  this.spawnAt = Date.now();
917
1067
  this.startupParseGate = true;
918
1068
  this.startupBuffer = '';
1069
+ this.startupFirstOutputAt = 0;
1070
+ if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
919
1071
  this.terminalScreen.reset(24, 80);
920
1072
  this.pendingTerminalQueryTail = '';
921
1073
  this.currentTurnScope = null;
@@ -926,7 +1078,8 @@ export class ProviderCliAdapter implements CliAdapter {
926
1078
  this.recordTrace('ready', {
927
1079
  runtimeMeta: this.getRuntimeMetadata(),
928
1080
  });
929
- this.setStatus('idle', 'pty_ready');
1081
+ this.setStatus('starting', 'pty_ready');
1082
+ this.scheduleStartupSettleCheck();
930
1083
  this.onStatusChange?.();
931
1084
  }
932
1085
 
@@ -943,6 +1096,9 @@ export class ProviderCliAdapter implements CliAdapter {
943
1096
  this.lastScreenSnapshot = normalizedScreenSnapshot;
944
1097
  this.lastScreenChangeAt = now;
945
1098
  }
1099
+ if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
1100
+ this.startupFirstOutputAt = now;
1101
+ }
946
1102
  if (this.idleFinishCandidate && (rawData.length > 0 || cleanData.length > 0)) {
947
1103
  this.clearIdleFinishCandidate('new_output');
948
1104
  }
@@ -954,6 +1110,10 @@ export class ProviderCliAdapter implements CliAdapter {
954
1110
  screenText: this.summarizeTraceText(this.terminalScreen.getText(), 1200),
955
1111
  });
956
1112
 
1113
+ if (this.startupParseGate) {
1114
+ this.scheduleStartupSettleCheck();
1115
+ }
1116
+
957
1117
  if (this.isWaitingForResponse && cleanData) {
958
1118
  this.responseBuffer = (this.responseBuffer + cleanData).slice(-8000);
959
1119
  }
@@ -972,36 +1132,61 @@ export class ProviderCliAdapter implements CliAdapter {
972
1132
  this.accumulatedBuffer = (this.accumulatedBuffer + cleanData).slice(-ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
973
1133
  this.accumulatedRawBuffer = (this.accumulatedRawBuffer + rawData).slice(-ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
974
1134
 
975
- // ─── Startup: detect CLI readiness (no auto-proceed)
976
- if (this.startupParseGate) {
977
- this.startupBuffer += cleanData;
978
- const elapsed = Date.now() - this.spawnAt;
979
- const screenText = this.terminalScreen.getText() || '';
980
- const startupModal = this.getStartupConfirmationModal(screenText);
981
- const scriptStatus = startupModal ? 'waiting_approval' : this.runDetectStatus(this.startupBuffer);
982
- const hasInteractivePrompt = this.looksLikeVisibleIdlePrompt(screenText);
983
- const startupStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
984
- const isReady = ((scriptStatus === 'idle' || scriptStatus === 'waiting_approval') && hasInteractivePrompt && startupStableMs >= 700)
985
- || (!!startupModal && startupStableMs >= 700)
986
- || elapsed > 8000
987
- || this.startupBuffer.length > 12000;
988
-
989
- if (isReady) {
990
- this.startupParseGate = false;
991
- this.ready = true;
992
- LOG.info(
993
- 'CLI',
994
- `[${this.cliType}] Startup ready (${elapsed}ms, scriptStatus=${scriptStatus}, prompt=${hasInteractivePrompt}, stableMs=${startupStableMs}) providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
995
- );
996
- this.onStatusChange?.();
997
- }
998
- // No early return — status detection runs from the start
999
- }
1135
+ this.resolveStartupState('output');
1000
1136
 
1001
1137
  // ─── Script-based status detection
1002
1138
  this.scheduleSettle();
1003
1139
  }
1004
1140
 
1141
+ private resolveStartupState(trigger: string): void {
1142
+ if (!this.startupParseGate) return;
1143
+
1144
+ const now = Date.now();
1145
+ const screenText = this.terminalScreen.getText() || '';
1146
+ const normalizedScreen = normalizeScreenSnapshot(screenText);
1147
+ const hasStartupOutput = !!this.startupFirstOutputAt || !!normalizedScreen.trim();
1148
+ if (!hasStartupOutput) return;
1149
+
1150
+ const stableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1151
+ if (stableMs < 2000) return;
1152
+
1153
+ const startupModal = this.getStartupConfirmationModal(screenText);
1154
+ this.startupParseGate = false;
1155
+ if (this.startupSettleTimer) {
1156
+ clearTimeout(this.startupSettleTimer);
1157
+ this.startupSettleTimer = null;
1158
+ }
1159
+ this.ready = true;
1160
+ if (startupModal) {
1161
+ this.activeModal = startupModal;
1162
+ this.setStatus('waiting_approval', `startup_ready:${trigger}`);
1163
+ } else {
1164
+ this.setStatus('idle', `startup_ready:${trigger}`);
1165
+ }
1166
+ LOG.info(
1167
+ 'CLI',
1168
+ `[${this.cliType}] Startup settled (${trigger}, stableMs=${stableMs}, modal=${!!startupModal}) providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
1169
+ );
1170
+ this.onStatusChange?.();
1171
+ }
1172
+
1173
+ private scheduleStartupSettleCheck(): void {
1174
+ if (!this.startupParseGate) return;
1175
+ if (this.startupSettleTimer) clearTimeout(this.startupSettleTimer);
1176
+
1177
+ const now = Date.now();
1178
+ const stableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1179
+ const delayMs = Math.max(250, 2050 - stableMs);
1180
+
1181
+ this.startupSettleTimer = setTimeout(() => {
1182
+ this.startupSettleTimer = null;
1183
+ this.resolveStartupState('startup_timer');
1184
+ if (this.startupParseGate && (Date.now() - this.spawnAt) < 10000) {
1185
+ this.scheduleStartupSettleCheck();
1186
+ }
1187
+ }, delayMs);
1188
+ }
1189
+
1005
1190
  private scheduleSettle(): void {
1006
1191
  if (this.settleTimer) clearTimeout(this.settleTimer);
1007
1192
  const settleEpoch = this.responseEpoch;
@@ -1053,6 +1238,57 @@ export class ProviderCliAdapter implements CliAdapter {
1053
1238
  || /for\s*shortcuts/i.test(text);
1054
1239
  }
1055
1240
 
1241
+ private findLastMatchingLineIndex(lines: string[], predicate: (line: string) => boolean): number {
1242
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1243
+ if (predicate(lines[index])) return index;
1244
+ }
1245
+ return -1;
1246
+ }
1247
+
1248
+ private looksLikeClaudeGeneratingLine(line: string): boolean {
1249
+ const trimmed = String(line || '').trim();
1250
+ if (!trimmed) return false;
1251
+ if (/esc to (cancel|interrupt|stop)/i.test(trimmed)) return true;
1252
+ if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+\s+\S+.*\b(?:thinking|thought for \d+s?)\b/i.test(trimmed)) return true;
1253
+ if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+\s+[A-Z][A-Za-z-]{3,}ing\b.*(?:…|\.{3})/u.test(trimmed)) return true;
1254
+ if (/^[⏺•]\s+(?:Reading|Writing|Editing|Searching|Inspecting|Planning|Analyzing|Synthesizing|Drafting|Running|Listing|Scanning|Matching)\b.*(?:…|\.{3})/i.test(trimmed)) {
1255
+ return /ctrl\+o to expand/i.test(trimmed)
1256
+ || /\b\d+\s+(?:file|files|pattern|patterns|director(?:y|ies)|match|matches|result|results)\b/i.test(trimmed);
1257
+ }
1258
+ return false;
1259
+ }
1260
+
1261
+ private detectClaudeGeneratingOverride(screenText: string, tail: string): boolean {
1262
+ if (this.cliType !== 'claude-cli') return false;
1263
+
1264
+ const source = sanitizeTerminalText(screenText || tail || '');
1265
+ if (!source.trim()) return false;
1266
+
1267
+ const allLines = source
1268
+ .split(/\r\n|\n|\r/g)
1269
+ .map(line => line.trim())
1270
+ .filter(Boolean);
1271
+ if (allLines.length === 0) return false;
1272
+
1273
+ const recentLines = allLines.slice(-12);
1274
+ const promptIndex = this.findLastMatchingLineIndex(recentLines, (line) => /^[❯›>]\s*$/.test(line));
1275
+ const activeRegion = promptIndex >= 0 ? recentLines.slice(Math.max(0, promptIndex - 2), promptIndex) : recentLines;
1276
+ if (activeRegion.length === 0) return false;
1277
+
1278
+ return activeRegion.some((line) => this.looksLikeClaudeGeneratingLine(line));
1279
+ }
1280
+
1281
+ private refineDetectedStatus(status: string | null, tail: string, screenText?: string): string | null {
1282
+ if (this.startupParseGate) {
1283
+ return this.getStartupConfirmationModal(screenText || '')
1284
+ ? 'waiting_approval'
1285
+ : 'starting';
1286
+ }
1287
+ if (status === 'waiting_approval') return status;
1288
+ if (this.detectClaudeGeneratingOverride(screenText || '', tail)) return 'generating';
1289
+ return status;
1290
+ }
1291
+
1056
1292
  private looksLikeVisibleAssistantCandidate(screenText: string): boolean {
1057
1293
  const lines = sanitizeTerminalText(String(screenText || '')).split(/\r\n|\n|\r/g);
1058
1294
  for (const line of lines) {
@@ -1091,6 +1327,13 @@ export class ProviderCliAdapter implements CliAdapter {
1091
1327
  return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
1092
1328
  }
1093
1329
 
1330
+ private hasRecentInteractiveActivity(now: number): boolean {
1331
+ const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
1332
+ const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : Number.MAX_SAFE_INTEGER;
1333
+ return quietForMs < ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS
1334
+ || screenStableMs < ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS;
1335
+ }
1336
+
1094
1337
  private getStartupConfirmationModal(screenText: string): { message: string; buttons: string[] } | null {
1095
1338
  const text = sanitizeTerminalText(String(screenText || ''));
1096
1339
  if (!text.trim()) return null;
@@ -1128,6 +1371,7 @@ export class ProviderCliAdapter implements CliAdapter {
1128
1371
  let loggedWait = false;
1129
1372
 
1130
1373
  while (Date.now() - startedAt < maxWaitMs) {
1374
+ this.resolveStartupState('interactive_wait');
1131
1375
  const screenText = this.terminalScreen.getText() || '';
1132
1376
  const hasPrompt = this.looksLikeVisibleIdlePrompt(screenText);
1133
1377
  const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
@@ -1137,7 +1381,6 @@ export class ProviderCliAdapter implements CliAdapter {
1137
1381
  const interactiveReady = hasPrompt
1138
1382
  && stableMs >= 700
1139
1383
  && recentlyOutput >= 350
1140
- && status !== 'starting'
1141
1384
  && status !== 'generating';
1142
1385
 
1143
1386
  if (interactiveReady) {
@@ -1181,6 +1424,10 @@ export class ProviderCliAdapter implements CliAdapter {
1181
1424
  }
1182
1425
  const tail = this.settledBuffer;
1183
1426
  const screenText = this.terminalScreen.getText() || '';
1427
+ this.resolveStartupState('settled');
1428
+ if (this.startupParseGate) {
1429
+ return;
1430
+ }
1184
1431
  const startupModal = this.getStartupConfirmationModal(screenText);
1185
1432
  const modal = this.runParseApproval(tail) || startupModal;
1186
1433
  const rawScriptStatus = this.runDetectStatus(tail);
@@ -1254,6 +1501,34 @@ export class ProviderCliAdapter implements CliAdapter {
1254
1501
  clearPendingScriptStatus();
1255
1502
  }
1256
1503
 
1504
+ const recentInteractiveActivity = this.hasRecentInteractiveActivity(now);
1505
+ const shouldHoldGenerating =
1506
+ scriptStatus === 'idle'
1507
+ && this.isWaitingForResponse
1508
+ && !modal
1509
+ && recentInteractiveActivity;
1510
+
1511
+ if (shouldHoldGenerating) {
1512
+ this.clearIdleFinishCandidate('hold_generating_recent_activity');
1513
+ this.setStatus('generating', 'recent_activity_hold');
1514
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1515
+ this.idleTimeout = setTimeout(() => {
1516
+ if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
1517
+ this.finishResponse();
1518
+ }
1519
+ }, this.timeouts.generatingIdle);
1520
+ this.recordTrace('hold_generating_recent_activity', {
1521
+ scriptStatus,
1522
+ recentInteractiveActivity,
1523
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1524
+ lastScreenChangeAt: this.lastScreenChangeAt,
1525
+ holdMs: ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS,
1526
+ ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1527
+ });
1528
+ this.onStatusChange?.();
1529
+ return;
1530
+ }
1531
+
1257
1532
  if (scriptStatus === 'waiting_approval') {
1258
1533
  this.clearIdleFinishCandidate('waiting_approval');
1259
1534
  const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
@@ -1333,8 +1608,8 @@ export class ProviderCliAdapter implements CliAdapter {
1333
1608
  const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1334
1609
  const hasAssistantTurn = !!lastParsedAssistant;
1335
1610
  const assistantLength = lastParsedAssistant?.content?.length || 0;
1336
- const idleQuietThresholdMs = Math.max(220, this.timeouts.outputSettle);
1337
- const idleStableThresholdMs = Math.max(120, Math.min(220, this.timeouts.outputSettle));
1611
+ const idleQuietThresholdMs = Math.max(2000, this.timeouts.outputSettle);
1612
+ const idleStableThresholdMs = 2000;
1338
1613
  const idleReady = visibleIdlePrompt
1339
1614
  && !modal
1340
1615
  && hasAssistantTurn
@@ -1449,7 +1724,7 @@ export class ProviderCliAdapter implements CliAdapter {
1449
1724
  this.currentTurnScope,
1450
1725
  );
1451
1726
  if (parsed && Array.isArray(parsed.messages)) {
1452
- this.committedMessages = this.normalizeParsedMessages(parsed.messages);
1727
+ this.committedMessages = this.normalizeParsedMessages(parsed.messages, this.currentTurnScope);
1453
1728
  const promptForTrim = this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages);
1454
1729
  if (promptForTrim) {
1455
1730
  const lastAssistantForTrim = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
@@ -1488,11 +1763,15 @@ export class ProviderCliAdapter implements CliAdapter {
1488
1763
  private runDetectStatus(text: string): string | null {
1489
1764
  if (!this.cliScripts?.detectStatus) return null;
1490
1765
  try {
1491
- return this.cliScripts.detectStatus({
1766
+ const screenText = this.terminalScreen.getText();
1767
+ const status = this.cliScripts.detectStatus({
1492
1768
  tail: text.slice(-500),
1493
- screenText: this.terminalScreen.getText(),
1769
+ screenText,
1494
1770
  rawBuffer: this.accumulatedRawBuffer,
1771
+ screen: buildCliScreenSnapshot(screenText),
1772
+ tailScreen: buildCliScreenSnapshot(text.slice(-500)),
1495
1773
  });
1774
+ return this.refineDetectedStatus(status, text, screenText || '');
1496
1775
  } catch (e: any) {
1497
1776
  LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
1498
1777
  return null;
@@ -1502,11 +1781,16 @@ export class ProviderCliAdapter implements CliAdapter {
1502
1781
  private runParseApproval(tail: string): { message: string; buttons: string[] } | null {
1503
1782
  if (!this.cliScripts?.parseApproval) return null;
1504
1783
  try {
1784
+ const screenText = this.terminalScreen.getText();
1785
+ const buffer = screenText || this.accumulatedBuffer;
1505
1786
  return this.cliScripts.parseApproval({
1506
- buffer: this.terminalScreen.getText() || this.accumulatedBuffer,
1507
- screenText: this.terminalScreen.getText(),
1787
+ buffer,
1788
+ screenText,
1508
1789
  rawBuffer: this.accumulatedRawBuffer,
1509
1790
  tail,
1791
+ screen: buildCliScreenSnapshot(screenText),
1792
+ bufferScreen: buildCliScreenSnapshot(buffer),
1793
+ tailScreen: buildCliScreenSnapshot(tail),
1510
1794
  });
1511
1795
  } catch (e: any) {
1512
1796
  LOG.warn('CLI', `[${this.cliType}] parseApproval error: ${e.message}`);
@@ -1525,6 +1809,28 @@ export class ProviderCliAdapter implements CliAdapter {
1525
1809
  };
1526
1810
  }
1527
1811
 
1812
+ seedCommittedMessages(messages: SeedCliChatMessage[]): void {
1813
+ const normalized = (Array.isArray(messages) ? messages : [])
1814
+ .filter((message) => message && (message.role === 'user' || message.role === 'assistant'))
1815
+ .map((message) => ({
1816
+ role: message.role as 'user' | 'assistant',
1817
+ content: typeof message.content === 'string' ? message.content : String(message.content || ''),
1818
+ timestamp: typeof message.timestamp === 'number' && Number.isFinite(message.timestamp)
1819
+ ? message.timestamp
1820
+ : undefined,
1821
+ receivedAt: typeof message.receivedAt === 'number' && Number.isFinite(message.receivedAt)
1822
+ ? message.receivedAt
1823
+ : undefined,
1824
+ kind: typeof message.kind === 'string' ? message.kind : undefined,
1825
+ id: typeof message.id === 'string' ? message.id : undefined,
1826
+ index: typeof message.index === 'number' ? message.index : undefined,
1827
+ meta: message.meta && typeof message.meta === 'object' ? { ...(message.meta as Record<string, any>) } : undefined,
1828
+ senderName: typeof message.senderName === 'string' ? message.senderName : undefined,
1829
+ }));
1830
+ this.committedMessages = normalized;
1831
+ this.syncMessageViews();
1832
+ }
1833
+
1528
1834
  /**
1529
1835
  * Script-based full parse — returns ReadChatResult.
1530
1836
  * Called by command handler / dashboard for rich content rendering.
@@ -1535,12 +1841,27 @@ export class ProviderCliAdapter implements CliAdapter {
1535
1841
  this.responseBuffer,
1536
1842
  this.currentTurnScope,
1537
1843
  );
1844
+ const shouldPreferCommittedMessages =
1845
+ !this.currentTurnScope
1846
+ && this.currentStatus === 'idle'
1847
+ && !this.activeModal;
1538
1848
  if (parsed && Array.isArray(parsed.messages)) {
1849
+ const hydratedMessages = shouldPreferCommittedMessages
1850
+ ? this.committedMessages.map((message, index) => ({
1851
+ ...message,
1852
+ id: (message as any).id || `msg_${index}`,
1853
+ index: typeof (message as any).index === 'number' ? (message as any).index : index,
1854
+ kind: (message as any).kind || 'standard',
1855
+ receivedAt: typeof (message as any).receivedAt === 'number'
1856
+ ? (message as any).receivedAt
1857
+ : message.timestamp,
1858
+ }))
1859
+ : this.hydrateParsedMessages(parsed.messages, this.currentTurnScope);
1539
1860
  return {
1540
1861
  id: parsed.id || 'cli_session',
1541
1862
  status: parsed.status || this.currentStatus,
1542
1863
  title: parsed.title || this.cliName,
1543
- messages: parsed.messages,
1864
+ messages: hydratedMessages,
1544
1865
  activeModal: parsed.activeModal ?? this.activeModal,
1545
1866
  providerSessionId: typeof parsed.providerSessionId === 'string' ? parsed.providerSessionId : undefined,
1546
1867
  };
@@ -1563,11 +1884,31 @@ export class ProviderCliAdapter implements CliAdapter {
1563
1884
  };
1564
1885
  }
1565
1886
 
1887
+ async invokeScript(scriptName: string, args?: Record<string, any>): Promise<any> {
1888
+ const fn = this.cliScripts?.[scriptName];
1889
+ if (typeof fn !== 'function') {
1890
+ throw new Error(`CLI script '${scriptName}' not available`);
1891
+ }
1892
+ const input = this.buildParseInput(
1893
+ this.committedMessages,
1894
+ this.responseBuffer,
1895
+ this.currentTurnScope,
1896
+ );
1897
+ return await Promise.resolve(fn({
1898
+ ...input,
1899
+ args: args && typeof args === 'object' ? { ...args } : {},
1900
+ }));
1901
+ }
1902
+
1566
1903
  private parseCurrentTranscript(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null): any {
1567
1904
  if (!this.cliScripts?.parseOutput) return null;
1568
1905
  try {
1569
1906
  const input = this.buildParseInput(baseMessages, partialResponse, scope);
1570
1907
  const parsed = this.cliScripts.parseOutput(input);
1908
+ const refinedStatus = this.refineDetectedStatus(typeof parsed?.status === 'string' ? parsed.status : null, input.recentBuffer, input.screenText);
1909
+ if (parsed && refinedStatus && parsed.status !== refinedStatus) {
1910
+ parsed.status = refinedStatus;
1911
+ }
1571
1912
  const promptForTrim = scope?.prompt || getLastUserPromptText(baseMessages);
1572
1913
  if (parsed && Array.isArray(parsed.messages) && promptForTrim) {
1573
1914
  const lastAssistant = [...parsed.messages].reverse().find((message: any) => message?.role === 'assistant' && typeof message.content === 'string');
@@ -1614,12 +1955,23 @@ export class ProviderCliAdapter implements CliAdapter {
1614
1955
  if (this.startupParseGate) {
1615
1956
  const deadline = Date.now() + 10000;
1616
1957
  while (this.startupParseGate && Date.now() < deadline) {
1958
+ this.resolveStartupState('send_wait');
1617
1959
  await new Promise(resolve => setTimeout(resolve, 50));
1618
1960
  }
1619
1961
  }
1962
+ await this.waitForInteractivePrompt();
1963
+ if (!this.ready) {
1964
+ this.resolveStartupState('send_precheck');
1965
+ const screenText = this.terminalScreen.getText() || '';
1966
+ const hasPrompt = this.looksLikeVisibleIdlePrompt(screenText);
1967
+ if (hasPrompt && this.currentStatus === 'idle') {
1968
+ this.ready = true;
1969
+ this.startupParseGate = false;
1970
+ LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
1971
+ }
1972
+ }
1620
1973
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
1621
1974
  if (this.isWaitingForResponse) return;
1622
- await this.waitForInteractivePrompt();
1623
1975
  const blockingModal = this.activeModal || this.getStartupConfirmationModal(this.terminalScreen.getText() || '');
1624
1976
  if (blockingModal || this.currentStatus === 'waiting_approval') {
1625
1977
  throw new Error(`${this.cliName} is awaiting confirmation before it can accept a prompt`);
@@ -1661,8 +2013,6 @@ export class ProviderCliAdapter implements CliAdapter {
1661
2013
  }
1662
2014
  this.responseEpoch += 1;
1663
2015
  this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
1664
- this.setStatus('generating', 'sendMessage');
1665
- this.onStatusChange?.();
1666
2016
  const startResponseTimeout = () => {
1667
2017
  if (this.responseTimeout) clearTimeout(this.responseTimeout);
1668
2018
  this.responseTimeout = setTimeout(() => {
@@ -1682,7 +2032,7 @@ export class ProviderCliAdapter implements CliAdapter {
1682
2032
  const retrySubmitIfStuck = (attempt: number) => {
1683
2033
  this.submitRetryTimer = null;
1684
2034
  if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
1685
- if (this.currentStatus !== 'generating') return;
2035
+ if (this.currentStatus === 'waiting_approval') return;
1686
2036
  if ((this.responseBuffer || '').trim()) return;
1687
2037
  const screenText = this.terminalScreen.getText();
1688
2038
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
@@ -1718,7 +2068,7 @@ export class ProviderCliAdapter implements CliAdapter {
1718
2068
  this.submitRetryTimer = setTimeout(() => {
1719
2069
  this.submitRetryTimer = null;
1720
2070
  if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
1721
- if (this.currentStatus !== 'generating') return;
2071
+ if (this.currentStatus === 'waiting_approval') return;
1722
2072
  if ((this.responseBuffer || '').trim()) return;
1723
2073
  const screenText = this.terminalScreen.getText();
1724
2074
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;