@adhdev/daemon-core 0.6.53 → 0.6.56

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 (164) hide show
  1. package/dist/index.d.ts +84 -34
  2. package/dist/index.js +631 -305
  3. package/dist/index.js.map +1 -1
  4. package/package.json +2 -1
  5. package/providers/_builtin/_helpers/index.js +26 -26
  6. package/providers/_builtin/cli/aider-cli/provider.json +37 -0
  7. package/providers/_builtin/cli/aider-cli/scripts/1.0/detect_status.js +20 -0
  8. package/providers/_builtin/cli/aider-cli/scripts/1.0/parse_approval.js +10 -0
  9. package/providers/_builtin/cli/aider-cli/scripts/1.0/parse_output.js +49 -0
  10. package/providers/_builtin/cli/aider-cli/scripts/1.0/scripts.js +7 -0
  11. package/providers/_builtin/cli/claude-cli/provider.json +9 -2
  12. package/providers/_builtin/cli/claude-cli/scripts/1.0/detect_status.js +31 -0
  13. package/providers/_builtin/cli/claude-cli/scripts/1.0/parse_approval.js +38 -0
  14. package/providers/_builtin/cli/claude-cli/scripts/1.0/parse_output.js +164 -0
  15. package/providers/_builtin/cli/claude-cli/scripts/1.0/scripts.js +42 -0
  16. package/providers/_builtin/cli/codex-cli/provider.json +9 -2
  17. package/providers/_builtin/cli/codex-cli/scripts/1.0/detect_status.js +20 -0
  18. package/providers/_builtin/cli/codex-cli/scripts/1.0/parse_approval.js +15 -0
  19. package/providers/_builtin/cli/codex-cli/scripts/1.0/parse_output.js +68 -0
  20. package/providers/_builtin/cli/codex-cli/scripts/1.0/scripts.js +11 -0
  21. package/providers/_builtin/cli/cursor-cli/provider.json +37 -0
  22. package/providers/_builtin/cli/cursor-cli/scripts/1.0/detect_status.js +19 -0
  23. package/providers/_builtin/cli/cursor-cli/scripts/1.0/parse_approval.js +10 -0
  24. package/providers/_builtin/cli/cursor-cli/scripts/1.0/parse_output.js +48 -0
  25. package/providers/_builtin/cli/cursor-cli/scripts/1.0/scripts.js +7 -0
  26. package/providers/_builtin/cli/gemini-cli/provider.json +9 -2
  27. package/providers/_builtin/cli/gemini-cli/scripts/1.0/detect_status.js +26 -0
  28. package/providers/_builtin/cli/gemini-cli/scripts/1.0/parse_approval.js +22 -0
  29. package/providers/_builtin/cli/gemini-cli/scripts/1.0/parse_output.js +71 -0
  30. package/providers/_builtin/cli/gemini-cli/scripts/1.0/scripts.js +12 -0
  31. package/providers/_builtin/cli/github-copilot-cli/provider.json +37 -0
  32. package/providers/_builtin/cli/github-copilot-cli/scripts/1.0/detect_status.js +19 -0
  33. package/providers/_builtin/cli/github-copilot-cli/scripts/1.0/parse_approval.js +9 -0
  34. package/providers/_builtin/cli/github-copilot-cli/scripts/1.0/parse_output.js +48 -0
  35. package/providers/_builtin/cli/github-copilot-cli/scripts/1.0/scripts.js +7 -0
  36. package/providers/_builtin/cli/goose-cli/provider.json +37 -0
  37. package/providers/_builtin/cli/goose-cli/scripts/1.0/detect_status.js +20 -0
  38. package/providers/_builtin/cli/goose-cli/scripts/1.0/parse_approval.js +10 -0
  39. package/providers/_builtin/cli/goose-cli/scripts/1.0/parse_output.js +48 -0
  40. package/providers/_builtin/cli/goose-cli/scripts/1.0/scripts.js +7 -0
  41. package/providers/_builtin/cli/opencode-cli/provider.json +37 -0
  42. package/providers/_builtin/cli/opencode-cli/scripts/1.0/detect_status.js +15 -0
  43. package/providers/_builtin/cli/opencode-cli/scripts/1.0/parse_approval.js +9 -0
  44. package/providers/_builtin/cli/opencode-cli/scripts/1.0/parse_output.js +48 -0
  45. package/providers/_builtin/cli/opencode-cli/scripts/1.0/scripts.js +7 -0
  46. package/providers/_builtin/extension/cline/scripts/1.0/focus_editor.js +5 -5
  47. package/providers/_builtin/extension/cline/scripts/1.0/list_chats.js +11 -11
  48. package/providers/_builtin/extension/cline/scripts/1.0/list_models.js +6 -6
  49. package/providers/_builtin/extension/cline/scripts/1.0/list_modes.js +5 -5
  50. package/providers/_builtin/extension/cline/scripts/1.0/new_session.js +12 -12
  51. package/providers/_builtin/extension/cline/scripts/1.0/open_panel.js +7 -7
  52. package/providers/_builtin/extension/cline/scripts/1.0/read_chat.js +29 -29
  53. package/providers/_builtin/extension/cline/scripts/1.0/resolve_action.js +19 -19
  54. package/providers/_builtin/extension/cline/scripts/1.0/send_message.js +15 -15
  55. package/providers/_builtin/extension/cline/scripts/1.0/set_mode.js +4 -4
  56. package/providers/_builtin/extension/cline/scripts/1.0/set_model.js +4 -4
  57. package/providers/_builtin/extension/cline/scripts/1.0/switch_session.js +21 -21
  58. package/providers/_builtin/extension/codex/scripts/1.0/explore_controls_webview.js +2 -2
  59. package/providers/_builtin/extension/codex/scripts/1.0/list_models.js +1 -1
  60. package/providers/_builtin/extension/codex/scripts/1.0/list_modes.js +1 -1
  61. package/providers/_builtin/extension/codex/scripts/1.0/new_session.js +2 -2
  62. package/providers/_builtin/extension/codex/scripts/1.0/read_chat.js +6 -6
  63. package/providers/_builtin/extension/codex/scripts/1.0/resolve_action.js +3 -3
  64. package/providers/_builtin/extension/codex/scripts/1.0/set_model.js +1 -1
  65. package/providers/_builtin/ide/antigravity/scripts/1.0/focus_editor.js +2 -2
  66. package/providers/_builtin/ide/antigravity/scripts/1.0/list_chats.js +19 -19
  67. package/providers/_builtin/ide/antigravity/scripts/1.0/list_models.js +1 -1
  68. package/providers/_builtin/ide/antigravity/scripts/1.0/list_modes.js +1 -1
  69. package/providers/_builtin/ide/antigravity/scripts/1.0/new_session.js +17 -17
  70. package/providers/_builtin/ide/antigravity/scripts/1.0/read_chat.js +41 -41
  71. package/providers/_builtin/ide/antigravity/scripts/1.0/resolve_action.js +6 -6
  72. package/providers/_builtin/ide/antigravity/scripts/1.0/send_message.js +13 -13
  73. package/providers/_builtin/ide/antigravity/scripts/1.0/set_model.js +1 -1
  74. package/providers/_builtin/ide/antigravity/scripts/1.0/switch_session.js +13 -13
  75. package/providers/_builtin/ide/cursor/scripts/1.0/dismiss_notification.js +3 -3
  76. package/providers/_builtin/ide/cursor/scripts/1.0/focus_editor.js +1 -1
  77. package/providers/_builtin/ide/cursor/scripts/1.0/list_models.js +12 -12
  78. package/providers/_builtin/ide/cursor/scripts/1.0/list_modes.js +5 -5
  79. package/providers/_builtin/ide/cursor/scripts/1.0/list_notifications.js +2 -2
  80. package/providers/_builtin/ide/cursor/scripts/1.0/list_sessions.js +5 -5
  81. package/providers/_builtin/ide/cursor/scripts/1.0/new_session.js +1 -1
  82. package/providers/_builtin/ide/cursor/scripts/1.0/open_panel.js +3 -3
  83. package/providers/_builtin/ide/cursor/scripts/1.0/read_chat.js +9 -9
  84. package/providers/_builtin/ide/cursor/scripts/1.0/resolve_action.js +4 -4
  85. package/providers/_builtin/ide/cursor/scripts/1.0/send_message.js +5 -5
  86. package/providers/_builtin/ide/cursor/scripts/1.0/set_mode.js +4 -4
  87. package/providers/_builtin/ide/cursor/scripts/1.0/set_model.js +12 -12
  88. package/providers/_builtin/ide/cursor/scripts/1.0/switch_session.js +2 -2
  89. package/providers/_builtin/ide/kiro/scripts/1.0/focus_editor.js +2 -2
  90. package/providers/_builtin/ide/kiro/scripts/1.0/open_panel.js +7 -7
  91. package/providers/_builtin/ide/kiro/scripts/1.0/resolve_action.js +3 -3
  92. package/providers/_builtin/ide/kiro/scripts/1.0/send_message.js +4 -4
  93. package/providers/_builtin/ide/kiro/scripts/1.0/webview_list_sessions.js +2 -2
  94. package/providers/_builtin/ide/kiro/scripts/1.0/webview_new_session.js +5 -5
  95. package/providers/_builtin/ide/kiro/scripts/1.0/webview_read_chat.js +13 -13
  96. package/providers/_builtin/ide/kiro/scripts/1.0/webview_resolve_action.js +1 -1
  97. package/providers/_builtin/ide/kiro/scripts/1.0/webview_send_message.js +8 -8
  98. package/providers/_builtin/ide/kiro/scripts/1.0/webview_set_mode.js +1 -1
  99. package/providers/_builtin/ide/kiro/scripts/1.0/webview_set_model.js +1 -1
  100. package/providers/_builtin/ide/kiro/scripts/1.0/webview_switch_session.js +3 -3
  101. package/providers/_builtin/ide/pearai/.tmp/after_rich.json +157 -0
  102. package/providers/_builtin/ide/pearai/.tmp/after_rich_raw.json +161 -0
  103. package/providers/_builtin/ide/pearai/.tmp/after_switch.json +592 -0
  104. package/providers/_builtin/ide/pearai/.tmp/after_switch_raw.json +596 -0
  105. package/providers/_builtin/ide/pearai/.tmp/home_dump_raw.json +176 -0
  106. package/providers/_builtin/ide/pearai/.tmp/items_raw.json +778 -0
  107. package/providers/_builtin/ide/pearai/.tmp/webview_dump.json +157 -0
  108. package/providers/_builtin/ide/pearai/.tmp/webview_dump_raw.json +161 -0
  109. package/providers/_builtin/ide/pearai/provider.json +1 -3
  110. package/providers/_builtin/ide/pearai/scripts/1.0/focus_editor.js +12 -16
  111. package/providers/_builtin/ide/pearai/scripts/1.0/list_models.js +45 -0
  112. package/providers/_builtin/ide/pearai/scripts/1.0/list_modes.js +44 -0
  113. package/providers/_builtin/ide/pearai/scripts/1.0/list_sessions.js +82 -27
  114. package/providers/_builtin/ide/pearai/scripts/1.0/new_session.js +26 -43
  115. package/providers/_builtin/ide/pearai/scripts/1.0/open_panel.js +10 -33
  116. package/providers/_builtin/ide/pearai/scripts/1.0/read_chat.js +169 -0
  117. package/providers/_builtin/ide/pearai/scripts/1.0/resolve_action.js +47 -41
  118. package/providers/_builtin/ide/pearai/scripts/1.0/scripts.js +36 -7
  119. package/providers/_builtin/ide/pearai/scripts/1.0/send_message.js +97 -22
  120. package/providers/_builtin/ide/pearai/scripts/1.0/set_mode.js +51 -0
  121. package/providers/_builtin/ide/pearai/scripts/1.0/set_model.js +54 -0
  122. package/providers/_builtin/ide/pearai/scripts/1.0/switch_session.js +81 -0
  123. package/providers/_builtin/ide/trae/scripts/1.0/focus_editor.js +2 -2
  124. package/providers/_builtin/ide/trae/scripts/1.0/list_chats.js +2 -2
  125. package/providers/_builtin/ide/trae/scripts/1.0/new_session.js +4 -4
  126. package/providers/_builtin/ide/trae/scripts/1.0/open_panel.js +6 -6
  127. package/providers/_builtin/ide/trae/scripts/1.0/read_chat.js +13 -13
  128. package/providers/_builtin/ide/trae/scripts/1.0/resolve_action.js +3 -3
  129. package/providers/_builtin/ide/trae/scripts/1.0/send_message.js +11 -11
  130. package/providers/_builtin/ide/trae/scripts/1.0/switch_session.js +2 -2
  131. package/providers/_builtin/ide/windsurf/scripts/1.0/focus_editor.js +6 -6
  132. package/providers/_builtin/ide/windsurf/scripts/1.0/list_chats.js +4 -4
  133. package/providers/_builtin/ide/windsurf/scripts/1.0/list_sessions.js +6 -6
  134. package/providers/_builtin/ide/windsurf/scripts/1.0/new_session.js +9 -9
  135. package/providers/_builtin/ide/windsurf/scripts/1.0/open_panel.js +6 -6
  136. package/providers/_builtin/ide/windsurf/scripts/1.0/resolve_action.js +8 -8
  137. package/providers/_builtin/ide/windsurf/scripts/1.0/switch_session.js +8 -8
  138. package/providers/_builtin/ide/windsurf/scripts/1.0.bak/focus_editor.js +6 -6
  139. package/providers/_builtin/ide/windsurf/scripts/1.0.bak/list_chats.js +12 -12
  140. package/providers/_builtin/ide/windsurf/scripts/1.0.bak/new_session.js +15 -15
  141. package/providers/_builtin/ide/windsurf/scripts/1.0.bak/open_panel.js +10 -10
  142. package/providers/_builtin/ide/windsurf/scripts/1.0.bak/read_chat.js +33 -33
  143. package/providers/_builtin/ide/windsurf/scripts/1.0.bak/resolve_action.js +6 -6
  144. package/providers/_builtin/ide/windsurf/scripts/1.0.bak/send_message.js +16 -16
  145. package/providers/_builtin/ide/windsurf/scripts/1.0.bak/switch_session.js +7 -7
  146. package/providers/_builtin/registry.json +76 -3
  147. package/src/cli-adapters/provider-cli-adapter.ts +312 -258
  148. package/src/cli-adapters/terminal-screen.ts +95 -0
  149. package/src/commands/chat-commands.ts +41 -15
  150. package/src/commands/cli-manager.ts +4 -2
  151. package/src/daemon/dev-server.ts +306 -62
  152. package/src/providers/acp-provider-instance.ts +1 -1
  153. package/src/providers/cli-provider-instance.ts +23 -9
  154. package/src/providers/provider-loader.ts +3 -2
  155. package/providers/_builtin/ide/pearai/scripts/1.0/webview_list_models.js +0 -43
  156. package/providers/_builtin/ide/pearai/scripts/1.0/webview_list_modes.js +0 -35
  157. package/providers/_builtin/ide/pearai/scripts/1.0/webview_list_sessions.js +0 -62
  158. package/providers/_builtin/ide/pearai/scripts/1.0/webview_new_session.js +0 -49
  159. package/providers/_builtin/ide/pearai/scripts/1.0/webview_read_chat.js +0 -92
  160. package/providers/_builtin/ide/pearai/scripts/1.0/webview_resolve_action.js +0 -59
  161. package/providers/_builtin/ide/pearai/scripts/1.0/webview_send_message.js +0 -72
  162. package/providers/_builtin/ide/pearai/scripts/1.0/webview_set_mode.js +0 -36
  163. package/providers/_builtin/ide/pearai/scripts/1.0/webview_set_model.js +0 -36
  164. package/providers/_builtin/ide/pearai/scripts/1.0/webview_switch_session.js +0 -34
@@ -1,18 +1,17 @@
1
1
  /**
2
- * ProviderCliAdapter — generic CLI Adapter based on provider.js
2
+ * ProviderCliAdapter — Script-based CLI Adapter
3
3
  *
4
- * Replaces individual adapters (gemini-cli.ts, claude-cli.ts, codex-cli.ts).
5
- * Single engine driven by provider.js patterns, spawn, and cleanOutput.
4
+ * All CLI providers use versioned scripts (like IDE providers).
5
+ * Scripts are Node.js functions that receive PTY buffer data and return structured results.
6
6
  *
7
- * provider.js contract:
8
- * type: string 'gemini-cli', 'claude-cli', 'codex-cli', ...
9
- * name: string 'Gemini CLI', 'Claude Code', ...
10
- * category: 'cli'
11
- * binary: string — binary name
12
- * spawn: { command, args, shell, env }
13
- * patterns: { prompt, generating, approval, ready } — prompt helps end startup splash-gate early; sendMessage does not wait for it
14
- * timeouts?: { idleFinish, generatingIdle, maxResponse, approvalCooldown, outputSettle, ... }
15
- * cleanOutput(raw, lastUserInput): string
7
+ * Required scripts in scripts/{version}/scripts.js:
8
+ * - detectStatus(input) → AgentStatus string ('idle' | 'generating' | 'waiting_approval')
9
+ * - parseOutput(input) → ReadChatResult { messages, status, activeModal, ... }
10
+ * - parseApproval(input) → ModalInfo | null
11
+ *
12
+ * provider.json contract:
13
+ * type, name, category: 'cli', binary, spawn, approvalKeys
14
+ * compatibility: [{ ideVersion, scriptDir }] ← versioned scripts
16
15
  */
17
16
 
18
17
  import * as os from 'os';
@@ -20,6 +19,7 @@ import * as path from 'path';
20
19
  import { execSync } from 'child_process';
21
20
  import type { CliAdapter } from '../cli-adapter-types.js';
22
21
  import { LOG } from '../logging/logger.js';
22
+ import { TerminalScreen } from './terminal-screen.js';
23
23
 
24
24
  let pty: any;
25
25
  try {
@@ -44,6 +44,8 @@ try {
44
44
  LOG.error('CLI', '[ProviderCliAdapter] node-pty not found. Terminal features disabled.');
45
45
  }
46
46
 
47
+ // ─── Types ──────────────────────────────────────────
48
+
47
49
  export interface CliChatMessage {
48
50
  role: 'user' | 'assistant';
49
51
  content: string;
@@ -57,23 +59,46 @@ export interface CliSessionStatus {
57
59
  activeModal: { message: string; buttons: string[] } | null;
58
60
  }
59
61
 
62
+ /**
63
+ * CLI Script Functions.
64
+ * Unlike IDE scripts (which return JS code strings for CDP evaluate),
65
+ * CLI scripts are Node.js functions that receive PTY buffer data and return structured results.
66
+ */
67
+ export interface CliScripts {
68
+ /** Full PTY buffer → ReadChatResult (messages, status, activeModal) */
69
+ parseOutput?: (input: CliScriptInput) => any;
70
+ /** Lightweight status detection (high-frequency polling) → AgentStatus string */
71
+ detectStatus?: (input: { tail: string; screenText?: string; rawBuffer?: string }) => string | null;
72
+ /** Parse approval modal from PTY output → ModalInfo | null */
73
+ parseApproval?: (input: { buffer: string; rawBuffer?: string; tail: string }) => { message: string; buttons: string[] } | null;
74
+ /** Produce a cli-specific prompt from a dashboard action payload */
75
+ resolveAction?: (data: any) => string;
76
+ /** Custom scripts */
77
+ [name: string]: ((input: any) => any) | undefined;
78
+ }
79
+
80
+ export interface CliScriptInput {
81
+ buffer: string; // Full ANSI-stripped accumulated PTY output
82
+ rawBuffer: string; // Raw PTY output (with ANSI)
83
+ recentBuffer: string; // Recent 1000 chars (ANSI-stripped)
84
+ screenText: string; // Current visible screen snapshot
85
+ messages: CliChatMessage[]; // Previously parsed messages
86
+ partialResponse: string; // Current partial response being generated
87
+ }
88
+
60
89
  export interface CliProviderModule {
61
90
  type: string;
62
91
  name: string;
63
92
  category: 'cli';
64
93
  binary: string;
94
+ sendDelayMs?: number;
95
+ sendKey?: string;
65
96
  spawn: {
66
97
  command: string;
67
98
  args: string[];
68
99
  shell: boolean;
69
100
  env: Record<string, string>;
70
101
  };
71
- patterns: {
72
- prompt: RegExp[];
73
- generating: RegExp[];
74
- approval: RegExp[];
75
- ready: RegExp[];
76
- };
77
102
  timeouts?: {
78
103
  /** PTY output batch transmit interval (default 50ms) */
79
104
  ptyFlush?: number;
@@ -89,12 +114,13 @@ export interface CliProviderModule {
89
114
  maxResponse?: number;
90
115
  /** shutdown after kill wait (default 1000ms) */
91
116
  shutdownGrace?: number;
92
- /** Output settle debounce before evaluating idle/approval (default 300ms) */
117
+ /** Output settle debounce before evaluating status (default 300ms) */
93
118
  outputSettle?: number;
94
119
  };
95
- cleanOutput(raw: string, lastUserInput?: string): string;
96
120
  }
97
121
 
122
+ // ─── Utility Functions ──────────────────────────────
123
+
98
124
  function stripAnsi(str: string): string {
99
125
  // eslint-disable-next-line no-control-regex
100
126
  return str
@@ -168,9 +194,8 @@ function shSingleQuote(arg: string): string {
168
194
  }
169
195
 
170
196
  /**
171
- * provider.json stores regex placeholders as `{}` — JSON.parse leaves plain objects,
172
- * so `.test` is missing and the PTY handler throws on first output. Coerce to RegExp
173
- * (including `{ "source": "...", "flags": "i" }`) or fall back to generic TUIs (Claude Code, etc.).
197
+ * Normalize provider.json for auto-implement approval detection.
198
+ * Kept for backward compat with dev-server auto-impl pipeline only.
174
199
  */
175
200
  function parsePatternEntry(x: unknown): RegExp | null {
176
201
  if (x instanceof RegExp) return x;
@@ -185,55 +210,26 @@ function parsePatternEntry(x: unknown): RegExp | null {
185
210
  return null;
186
211
  }
187
212
 
188
- function coercePatternArray(raw: unknown, fallbacks: RegExp[]): RegExp[] {
189
- if (!Array.isArray(raw)) return [...fallbacks];
190
- const parsed = raw.map(parsePatternEntry).filter((r): r is RegExp => r != null);
191
- return parsed.length > 0 ? parsed : [...fallbacks];
192
- }
193
-
194
- /** Defaults tuned for Claude Code / similar agent CLIs when provider.json patterns are empty. */
195
- const FALLBACK_PROMPT: RegExp[] = [
196
- /Type your message/i,
197
- /for\s*shortcuts/i, // Claude Code prompt
198
- /\?\s*for\s*help/i, // Claude Code help prompt
199
- /Press enter/i,
200
- /^[>›❯]\s*$/i, // Prompt char as the complete evaluated string
201
- /[>›❯]\s*$/, // Prompt char at the very end of evaluated string
202
- ];
203
-
204
- const FALLBACK_GENERATING: RegExp[] = [
205
- /[\u2800-\u28ff]/, // Braille spinner blocks (universal TUI)
206
- /esc to (cancel|interrupt|stop)/i, // Common TUI generation status line
207
- /generating\.\.\./i,
208
- /Claude is (?:thinking|processing|working)/i, // Specific Claude Code status
209
- ];
210
-
211
- const FALLBACK_APPROVAL: RegExp[] = [
212
- /Allow\s*once/i, // ANSI strip may remove spaces
213
- /Always\s*allow/i,
214
- /\(y\/n\)/i,
215
- /\[Y\/n\]/i,
216
- /Yes,?\s*don'?t\s*ask/i, // "Yes, don't ask again" (Claude Code)
217
- ];
218
-
219
- function defaultCleanOutput(raw: string, _lastUserInput?: string): string {
220
- return stripAnsi(raw).trim();
213
+ function coercePatternArray(raw: unknown): RegExp[] {
214
+ if (!Array.isArray(raw)) return [];
215
+ return raw.map(parsePatternEntry).filter((r): r is RegExp => r != null);
221
216
  }
222
217
 
223
- export function normalizeCliProviderForRuntime(raw: any): CliProviderModule {
218
+ /**
219
+ * Normalize raw provider JSON for auto-implement approval patterns.
220
+ * Used by dev-server only — ProviderCliAdapter itself uses scripts.
221
+ */
222
+ export function normalizeCliProviderForRuntime(raw: any): { patterns: { approval: RegExp[] } } {
224
223
  const patterns = raw?.patterns || {};
225
224
  return {
226
- ...raw,
227
225
  patterns: {
228
- prompt: coercePatternArray(patterns.prompt, FALLBACK_PROMPT),
229
- generating: coercePatternArray(patterns.generating, FALLBACK_GENERATING),
230
- approval: coercePatternArray(patterns.approval, FALLBACK_APPROVAL),
231
- ready: coercePatternArray(patterns.ready, []),
226
+ approval: coercePatternArray(patterns.approval),
232
227
  },
233
- cleanOutput: typeof raw?.cleanOutput === 'function' ? raw.cleanOutput : defaultCleanOutput,
234
228
  };
235
229
  }
236
230
 
231
+ // ─── Adapter ────────────────────────────────────────
232
+
237
233
  export class ProviderCliAdapter implements CliAdapter {
238
234
  readonly cliType: string;
239
235
  readonly cliName: string;
@@ -242,6 +238,7 @@ export class ProviderCliAdapter implements CliAdapter {
242
238
  private provider: CliProviderModule;
243
239
  private ptyProcess: any = null;
244
240
  private messages: CliChatMessage[] = [];
241
+ private structuredMessages: CliChatMessage[] = [];
245
242
  private currentStatus: CliSessionStatus['status'] = 'starting';
246
243
  private onStatusChange: (() => void) | null = null;
247
244
 
@@ -253,7 +250,6 @@ export class ProviderCliAdapter implements CliAdapter {
253
250
  private idleTimeout: NodeJS.Timeout | null = null;
254
251
  private ready = false;
255
252
  private startupBuffer = '';
256
- /** After spawn: briefly skip generating/settle so splash/redraw does not flip status; not used to gate sendMessage */
257
253
  private startupParseGate = false;
258
254
  private spawnAt = 0;
259
255
 
@@ -275,7 +271,8 @@ export class ProviderCliAdapter implements CliAdapter {
275
271
 
276
272
  // Output settle debounce — fires after PTY output goes quiet
277
273
  private settleTimer: NodeJS.Timeout | null = null;
278
- private settledBuffer: string = ''; // snapshot of recentOutputBuffer at settle time
274
+ private settledBuffer: string = '';
275
+ private submitPendingUntil = 0;
279
276
 
280
277
  // Resize redraw suppression
281
278
  private resizeSuppressUntil: number = 0;
@@ -283,6 +280,17 @@ export class ProviderCliAdapter implements CliAdapter {
283
280
  // Debug: status transition history
284
281
  private statusHistory: { status: string; at: number; trigger?: string }[] = [];
285
282
 
283
+ // ─── CLI Scripts (script-based parsing) ───
284
+ private cliScripts: CliScripts;
285
+ /** Full accumulated ANSI-stripped PTY output */
286
+ private accumulatedBuffer: string = '';
287
+ /** Full accumulated raw PTY output (with ANSI) */
288
+ private accumulatedRawBuffer: string = '';
289
+ /** Current visible terminal screen snapshot */
290
+ private terminalScreen = new TerminalScreen(40, 120);
291
+ /** Max accumulated buffer size (last 50KB) */
292
+ private static readonly MAX_ACCUMULATED_BUFFER = 50000;
293
+
286
294
  private setStatus(status: CliSessionStatus['status'], trigger?: string): void {
287
295
  const prev = this.currentStatus;
288
296
  if (prev === status) return;
@@ -292,21 +300,22 @@ export class ProviderCliAdapter implements CliAdapter {
292
300
  LOG.info('CLI', `[${this.cliType}] status: ${prev} → ${status}${trigger ? ` (${trigger})` : ''}`);
293
301
  }
294
302
 
295
- // Resolved timeouts (provider defaults + overrides)
303
+ // Resolved timeouts
296
304
  private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
297
305
 
298
- // Provider approval key mapping (e.g. { 0: '1', 1: '2', 2: '3' }) — loaded from provider.json
306
+ // Provider approval key mapping
299
307
  private readonly approvalKeys: Record<number, string>;
308
+ private readonly sendDelayMs: number;
309
+ private readonly sendKey: string;
300
310
 
301
311
  constructor(provider: CliProviderModule, workingDir: string, private extraArgs: string[] = []) {
302
- this.provider = normalizeCliProviderForRuntime(provider);
312
+ this.provider = provider;
303
313
  this.cliType = provider.type;
304
314
  this.cliName = provider.name;
305
315
  this.workingDir = workingDir.startsWith('~')
306
316
  ? workingDir.replace(/^~/, os.homedir())
307
317
  : workingDir;
308
318
 
309
- // Apply timeout overrides from Provider
310
319
  const t = provider.timeouts || {};
311
320
  this.timeouts = {
312
321
  ptyFlush: t.ptyFlush ?? 50,
@@ -319,9 +328,28 @@ export class ProviderCliAdapter implements CliAdapter {
319
328
  outputSettle: t.outputSettle ?? 300,
320
329
  };
321
330
 
322
- // Load approval key mapping from provider (e.g. approvalKeys: {"0":"1","1":"2","2":"3"})
323
331
  const rawKeys = (provider as any).approvalKeys;
324
332
  this.approvalKeys = (rawKeys && typeof rawKeys === 'object') ? rawKeys : {};
333
+ this.sendDelayMs = typeof (provider as any).sendDelayMs === 'number' ? Math.max(0, (provider as any).sendDelayMs) : 0;
334
+ this.sendKey = typeof (provider as any).sendKey === 'string' && (provider as any).sendKey.length > 0
335
+ ? (provider as any).sendKey
336
+ : '\r';
337
+
338
+ // Scripts are required — loaded by ProviderLoader via compatibility array
339
+ this.cliScripts = (provider as any).scripts || {};
340
+ const scriptNames = Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function');
341
+ if (scriptNames.length > 0) {
342
+ LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
343
+ } else {
344
+ LOG.warn('CLI', `[${this.cliType}] ⚠ No CLI scripts loaded! Provider needs scripts/{version}/scripts.js`);
345
+ }
346
+ }
347
+
348
+ /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
349
+ setCliScripts(scripts: CliScripts): void {
350
+ this.cliScripts = scripts;
351
+ const scriptNames = Object.keys(scripts).filter(k => typeof (scripts as any)[k] === 'function');
352
+ LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
325
353
  }
326
354
 
327
355
  // ─── Lifecycle ─────────────────────────────────
@@ -364,7 +392,6 @@ export class ProviderCliAdapter implements CliAdapter {
364
392
  const useShell = isWin ? !!spawnConfig.shell : useShellUnix;
365
393
 
366
394
  if (useShell) {
367
- // Execute via shell (npm shims, shebang scripts, anything that is not a bare Mach-O/ELF)
368
395
  if (!spawnConfig.shell && !isWin) {
369
396
  LOG.info('CLI', `[${this.cliType}] Using login shell (script shim or non-native binary)`);
370
397
  }
@@ -404,7 +431,6 @@ export class ProviderCliAdapter implements CliAdapter {
404
431
 
405
432
  this.ptyProcess.onData((data: string) => {
406
433
  this.handleOutput(data);
407
- // PTY output batch transmit (50ms)
408
434
  if (this.onPtyDataCallback) {
409
435
  this.ptyOutputBuffer += data;
410
436
  if (!this.ptyOutputFlushTimer) {
@@ -432,21 +458,30 @@ export class ProviderCliAdapter implements CliAdapter {
432
458
  this.spawnAt = Date.now();
433
459
  this.startupParseGate = true;
434
460
  this.startupBuffer = '';
461
+ this.terminalScreen.reset(40, 120);
435
462
  this.ready = true;
436
463
  this.setStatus('idle', 'pty_ready');
437
464
  this.onStatusChange?.();
438
465
  }
439
466
 
440
- // ─── Output state machine ────────────────────────────
467
+ // ─── Output Handling ────────────────────────────
441
468
 
442
469
  private handleOutput(rawData: string): void {
443
- // Suppress output processing briefly after resize to avoid false triggers from screen redraws
444
470
  if (Date.now() < this.resizeSuppressUntil) return;
445
471
 
472
+ if (rawData.includes('\x1b[6n') || rawData.includes('\x1b[?6n')) {
473
+ // Some TUIs probe cursor position during startup; node-pty does not answer automatically.
474
+ this.ptyProcess?.write('\x1b[1;1R');
475
+ }
476
+
477
+ this.terminalScreen.write(rawData);
446
478
  const cleanData = stripAnsi(rawData);
447
- const { patterns } = this.provider;
448
479
 
449
- // Server log forwarding
480
+ if (this.isWaitingForResponse && cleanData) {
481
+ this.responseBuffer = (this.responseBuffer + cleanData).slice(-8000);
482
+ }
483
+
484
+ // Server log forwarding
450
485
  if (cleanData.trim()) {
451
486
  if (this.serverConn) {
452
487
  this.serverConn.sendMessage('log', { message: cleanData.trim(), level: 'info' });
@@ -455,13 +490,14 @@ export class ProviderCliAdapter implements CliAdapter {
455
490
  }
456
491
  }
457
492
 
458
- // Rolling buffer (recent 1000 chars)
493
+ // Rolling buffers
459
494
  this.recentOutputBuffer = (this.recentOutputBuffer + cleanData).slice(-1000);
495
+ this.accumulatedBuffer = (this.accumulatedBuffer + cleanData).slice(-ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
496
+ this.accumulatedRawBuffer = (this.accumulatedRawBuffer + rawData).slice(-ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
460
497
 
461
- // ─── Startup window: first-run dialogs + avoid false generating during splash (sendMessage is not gated on prompt text)
498
+ // ─── Startup window: auto-proceed first-run dialogs
462
499
  if (this.startupParseGate) {
463
500
  this.startupBuffer += cleanData;
464
- LOG.info('CLI', `[${this.cliType}] startup chunk (${cleanData.length} chars): ${cleanData.slice(0, 200).replace(/\n/g, '\\n')}`);
465
501
 
466
502
  const dialogPatterns = [
467
503
  /Do you want to connect/i,
@@ -478,133 +514,63 @@ export class ProviderCliAdapter implements CliAdapter {
478
514
 
479
515
  const elapsed = Date.now() - this.spawnAt;
480
516
  const bufCap = this.startupBuffer.length > 12000;
481
- const promptMatched = patterns.prompt.some(p => p.test(this.startupBuffer));
482
- if (promptMatched || elapsed > 8000 || bufCap) {
483
- this.startupParseGate = false;
484
- if (promptMatched) {
485
- LOG.info('CLI', `[${this.cliType}] ✓ Startup gate end (prompt matched)`);
486
- } else {
487
- LOG.info('CLI', `[${this.cliType}] startup gate end (${elapsed}ms, cap=${bufCap}, prompt=${promptMatched})`);
488
- }
489
- } else {
490
- return;
491
- }
492
- }
493
517
 
494
- if (cleanData.trim().length > 5) {
495
- LOG.debug('CLI', `[${this.cliType}] output chunk (${cleanData.length}): ${cleanData.slice(0, 300).replace(/\n/g, '\\n')}`);
496
- }
497
-
498
- // ─── Phase 2: generating — immediate detection (fast UX response)
499
- if (!this.isWaitingForResponse) {
500
- if (patterns.generating.some(p => p.test(cleanData))) {
501
- if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
502
- this.isWaitingForResponse = true;
503
- this.responseBuffer = '';
504
- this.setStatus('generating', 'autonomous_gen');
505
- this.onStatusChange?.();
506
- }
507
- }
518
+ // Use detectStatus script to check if CLI is ready (shows a prompt)
519
+ const scriptStatus = this.runDetectStatus(this.startupBuffer);
520
+ const isReady = scriptStatus === 'idle' || elapsed > 8000 || bufCap;
508
521
 
509
- if (this.isWaitingForResponse) {
510
- this.responseBuffer += cleanData;
511
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
512
-
513
- // ─── Phase 3: still generating
514
- if (patterns.generating.some(p => p.test(cleanData))) {
515
- this.setStatus('generating', 'still_generating');
516
- this.idleTimeout = setTimeout(() => {
517
- if (this.isWaitingForResponse) this.finishResponse();
518
- }, this.timeouts.generatingIdle);
519
- this.onStatusChange?.();
520
- // Cancel any pending settle — we're clearly still generating
521
- if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
522
+ if (isReady) {
523
+ this.startupParseGate = false;
524
+ LOG.info('CLI', `[${this.cliType}] Startup gate end (${elapsed}ms, scriptStatus=${scriptStatus})`);
525
+ } else {
522
526
  return;
523
527
  }
524
528
  }
525
529
 
526
- // ─── Phase 4: waiting_approval — accumulate transition buffer
527
- if (this.currentStatus === 'waiting_approval') {
528
- this.approvalTransitionBuffer = (this.approvalTransitionBuffer + cleanData).slice(-500);
529
- // Schedule settle check — output may still be arriving (ANSI redraws etc)
530
- this.scheduleSettle();
531
- return;
532
- }
533
-
534
- // ─── Phase 5: settle debounce — schedule idle/approval evaluation
530
+ // ─── Script-based status detection
535
531
  this.scheduleSettle();
536
532
  }
537
533
 
538
- /**
539
- * Fired after output goes quiet for outputSettle ms.
540
- * Evaluates the stabilised buffer for approval, prompt (idle), or timeout.
541
- */
542
534
  private scheduleSettle(): void {
543
535
  if (this.settleTimer) clearTimeout(this.settleTimer);
536
+ const delay = Math.max(
537
+ this.timeouts.outputSettle,
538
+ this.submitPendingUntil > Date.now()
539
+ ? (this.submitPendingUntil - Date.now()) + this.timeouts.outputSettle
540
+ : 0,
541
+ );
544
542
  this.settleTimer = setTimeout(() => {
545
543
  this.settleTimer = null;
546
544
  this.settledBuffer = this.recentOutputBuffer;
547
545
  this.evaluateSettled();
548
- }, this.timeouts.outputSettle);
546
+ }, delay);
549
547
  }
550
548
 
551
549
  private evaluateSettled(): void {
552
- const { patterns } = this.provider;
553
- const buf = this.settledBuffer;
554
-
555
- // ─── waiting_approval: check for transition out
556
- if (this.currentStatus === 'waiting_approval') {
557
- const genResume = patterns.generating.some(p => p.test(this.approvalTransitionBuffer));
558
- const promptResume = patterns.prompt.some(p => p.test(this.approvalTransitionBuffer));
559
- if (genResume) {
560
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
561
- this.setStatus('generating', 'approval_gen_resume');
562
- this.activeModal = null;
563
- this.recentOutputBuffer = '';
564
- this.approvalTransitionBuffer = '';
565
- this.lastApprovalResolvedAt = Date.now();
566
- this.onStatusChange?.();
567
- } else if (promptResume) {
568
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
569
- this.activeModal = null;
570
- this.recentOutputBuffer = '';
571
- this.approvalTransitionBuffer = '';
572
- this.lastApprovalResolvedAt = Date.now();
573
- this.finishResponse();
574
- }
575
- // else: still waiting — approvalExitTimeout will handle the 60s safety case
576
- return;
577
- }
550
+ const tail = this.settledBuffer;
551
+ const scriptStatus = this.runDetectStatus(tail);
552
+ if (!scriptStatus) return;
578
553
 
579
- // ─── check for approval on stabilised buffer
580
- const hasApproval = patterns.approval.some(p => p.test(buf));
581
- if (hasApproval) {
554
+ const prevStatus = this.currentStatus;
555
+
556
+ if (scriptStatus === 'waiting_approval') {
582
557
  const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
583
558
  if (!inCooldown) {
584
- const ctxLines = buf.split('\n')
585
- .map(l => l.trim())
586
- .filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
587
559
  this.isWaitingForResponse = true;
588
- this.setStatus('waiting_approval', 'approval_pattern');
589
- this.recentOutputBuffer = '';
590
- this.approvalTransitionBuffer = '';
591
- this.activeModal = {
592
- message: ctxLines.slice(-5).join(' ').slice(0, 200) || 'Approval required',
593
- buttons: this.cliType === 'claude-cli'
594
- ? ['Yes (y)', 'Always allow (a)', 'Deny (Esc)']
595
- : ['Allow once', 'Always allow', 'Deny'],
596
- };
560
+ this.setStatus('waiting_approval', 'script_detect');
561
+
562
+ // Use parseApproval script for modal info
563
+ const modal = this.runParseApproval(tail);
564
+ this.activeModal = modal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
565
+
597
566
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
598
567
  if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
599
568
  this.approvalExitTimeout = setTimeout(() => {
600
569
  if (this.currentStatus === 'waiting_approval') {
601
- LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-exiting waiting_approval`);
570
+ LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
602
571
  this.activeModal = null;
603
572
  this.lastApprovalResolvedAt = Date.now();
604
- this.recentOutputBuffer = '';
605
- this.approvalTransitionBuffer = '';
606
- this.approvalExitTimeout = null;
607
- this.setStatus(this.isWaitingForResponse ? 'generating' : 'idle', 'approval_cleared');
573
+ this.setStatus('idle', 'approval_timeout');
608
574
  this.onStatusChange?.();
609
575
  }
610
576
  }, 60000);
@@ -613,22 +579,39 @@ export class ProviderCliAdapter implements CliAdapter {
613
579
  }
614
580
  }
615
581
 
616
- // ─── check for prompt (idle) on stabilised buffer
617
- if (this.isWaitingForResponse) {
618
- // Only look at the trailing portion — prompt appears at the very end
619
- const trailingLines = buf.split('\n').slice(-3).join('\n');
620
- if (patterns.prompt.some(p => p.test(trailingLines)) && !hasApproval) {
621
- this.finishResponse();
622
- } else if (!patterns.generating.some(p => p.test(buf))) {
623
- // Output has settled with no generating signal and no prompt — schedule idle finish
624
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
625
- this.idleTimeout = setTimeout(() => {
626
- if (this.isWaitingForResponse && this.responseBuffer.trim()) {
627
- this.finishResponse();
628
- }
629
- }, this.timeouts.idleFinish);
582
+ if (scriptStatus === 'generating') {
583
+ if (prevStatus === 'waiting_approval') {
584
+ // Transitioned out of approval generating
585
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
586
+ this.activeModal = null;
587
+ this.lastApprovalResolvedAt = Date.now();
588
+ }
589
+ if (!this.isWaitingForResponse) {
590
+ this.isWaitingForResponse = true;
591
+ this.responseBuffer = '';
630
592
  }
593
+ this.setStatus('generating', 'script_detect');
594
+ // Reset idle timeout
595
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
596
+ this.idleTimeout = setTimeout(() => {
597
+ if (this.isWaitingForResponse) this.finishResponse();
598
+ }, this.timeouts.generatingIdle);
631
599
  this.onStatusChange?.();
600
+ return;
601
+ }
602
+
603
+ if (scriptStatus === 'idle') {
604
+ if (prevStatus === 'waiting_approval') {
605
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
606
+ this.activeModal = null;
607
+ this.lastApprovalResolvedAt = Date.now();
608
+ }
609
+ if (this.isWaitingForResponse) {
610
+ this.finishResponse();
611
+ } else if (prevStatus !== 'idle') {
612
+ this.setStatus('idle', 'script_detect');
613
+ this.onStatusChange?.();
614
+ }
632
615
  }
633
616
  }
634
617
 
@@ -637,23 +620,6 @@ export class ProviderCliAdapter implements CliAdapter {
637
620
  if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
638
621
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
639
622
 
640
- const lastUserText = this.messages.filter(m => m.role === 'user').pop()?.content;
641
- let response = this.provider.cleanOutput(this.responseBuffer, lastUserText);
642
-
643
- // Remove user input echo
644
- if (lastUserText && response) {
645
- const userTrimmed = lastUserText.trim();
646
- response = response.split('\n')
647
- .filter(l => l.trim() !== userTrimmed)
648
- .join('\n').trim();
649
- }
650
-
651
- if (response) {
652
- this.messages.push({ role: 'assistant', content: response, timestamp: Date.now() });
653
- if (this.messages.length > 200) this.messages = this.messages.slice(-200);
654
- LOG.info('CLI', `[${this.cliType}] Response (${response.length} chars)`);
655
- }
656
-
657
623
  this.responseBuffer = '';
658
624
  this.isWaitingForResponse = false;
659
625
  this.activeModal = null;
@@ -661,9 +627,54 @@ export class ProviderCliAdapter implements CliAdapter {
661
627
  this.onStatusChange?.();
662
628
  }
663
629
 
664
- // ─── Public API (CliAdapter interface) ──────────
630
+ // ─── Script Execution ──────────────────────────
631
+
632
+ private runDetectStatus(text: string): string | null {
633
+ if (!this.cliScripts?.detectStatus) return null;
634
+ try {
635
+ return this.cliScripts.detectStatus({
636
+ tail: text.slice(-500),
637
+ screenText: this.terminalScreen.getText(),
638
+ rawBuffer: this.accumulatedRawBuffer,
639
+ });
640
+ } catch (e: any) {
641
+ LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
642
+ return null;
643
+ }
644
+ }
645
+
646
+ private runParseApproval(tail: string): { message: string; buttons: string[] } | null {
647
+ if (!this.cliScripts?.parseApproval) return null;
648
+ try {
649
+ return this.cliScripts.parseApproval({
650
+ buffer: this.terminalScreen.getText() || this.accumulatedBuffer,
651
+ rawBuffer: this.accumulatedRawBuffer,
652
+ tail,
653
+ });
654
+ } catch (e: any) {
655
+ LOG.warn('CLI', `[${this.cliType}] parseApproval error: ${e.message}`);
656
+ return null;
657
+ }
658
+ }
659
+
660
+ // ─── Public API (CliAdapter) ───────────────────
665
661
 
666
662
  getStatus(): CliSessionStatus {
663
+ // Use parseOutput script for full result when available
664
+ const scriptResult = this.getScriptParsedStatus();
665
+ if (scriptResult) {
666
+ return {
667
+ status: this.currentStatus,
668
+ messages: (scriptResult.messages || []).map((m: any) => ({
669
+ role: m.role,
670
+ content: m.content,
671
+ timestamp: m.timestamp,
672
+ })),
673
+ workingDir: this.workingDir,
674
+ activeModal: this.activeModal,
675
+ };
676
+ }
677
+
667
678
  return {
668
679
  status: this.currentStatus,
669
680
  messages: [...this.messages],
@@ -672,28 +683,97 @@ export class ProviderCliAdapter implements CliAdapter {
672
683
  };
673
684
  }
674
685
 
686
+ /**
687
+ * Script-based full parse — returns ReadChatResult.
688
+ * Called by command handler / dashboard for rich content rendering.
689
+ */
690
+ getScriptParsedStatus(): any {
691
+ if (!this.cliScripts?.parseOutput) return null;
692
+ try {
693
+ const input: CliScriptInput = {
694
+ buffer: this.accumulatedBuffer,
695
+ rawBuffer: this.accumulatedRawBuffer,
696
+ recentBuffer: this.recentOutputBuffer,
697
+ screenText: this.terminalScreen.getText(),
698
+ messages: [...(this.structuredMessages.length > 0 ? this.structuredMessages : this.messages)],
699
+ partialResponse: this.responseBuffer,
700
+ };
701
+ const result = this.cliScripts.parseOutput(input);
702
+ if (result && typeof result === 'object') {
703
+ if (Array.isArray((result as any).messages)) {
704
+ this.structuredMessages = (result as any).messages.map((m: any) => ({
705
+ role: m.role,
706
+ content: m.content,
707
+ timestamp: m.timestamp,
708
+ }));
709
+ }
710
+ return result;
711
+ }
712
+ } catch (e: any) {
713
+ LOG.warn('CLI', `[${this.cliType}] parseOutput error: ${e.message}`);
714
+ }
715
+ return null;
716
+ }
717
+
718
+ /** Whether this adapter has CLI scripts loaded */
719
+ hasCliScripts(): boolean {
720
+ return typeof this.cliScripts?.detectStatus === 'function';
721
+ }
722
+
723
+ /**
724
+ * Resolves an action (like 'fix' lint error) from the dashboard.
725
+ * Uses resolveAction script if available, otherwise falls back to standard text.
726
+ */
727
+ async resolveAction(data: any): Promise<void> {
728
+ let promptText = '';
729
+ if (this.cliScripts && typeof this.cliScripts.resolveAction === 'function') {
730
+ try {
731
+ promptText = this.cliScripts.resolveAction(data);
732
+ } catch (e: any) {
733
+ LOG.warn('CLI', `[${this.cliType}] resolveAction error: ${e.message}`);
734
+ }
735
+ }
736
+ if (!promptText && data) {
737
+ // Default fallback
738
+ promptText = `Please fix the following issue:\n${data.title || ''}\n${data.explanation || ''}\n\n${data.message || ''}`.trim();
739
+ }
740
+ if (promptText) {
741
+ await this.sendMessage(promptText);
742
+ }
743
+ }
744
+
675
745
  async sendMessage(text: string): Promise<void> {
676
746
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
677
747
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
678
748
  if (this.isWaitingForResponse) return;
679
749
 
680
750
  this.messages.push({ role: 'user', content: text, timestamp: Date.now() });
751
+ this.structuredMessages.push({ role: 'user', content: text, timestamp: Date.now() });
681
752
  this.isWaitingForResponse = true;
682
753
  this.responseBuffer = '';
683
754
  this.setStatus('generating', 'sendMessage');
684
755
  this.onStatusChange?.();
685
-
686
- this.ptyProcess.write(text + '\r');
687
-
688
- this.responseTimeout = setTimeout(() => {
689
- if (this.isWaitingForResponse) this.finishResponse();
690
- }, this.timeouts.maxResponse);
756
+ this.ptyProcess.write(text);
757
+
758
+ const submit = () => {
759
+ if (!this.ptyProcess) return;
760
+ this.submitPendingUntil = 0;
761
+ this.ptyProcess.write(this.sendKey);
762
+ this.responseTimeout = setTimeout(() => {
763
+ if (this.isWaitingForResponse) this.finishResponse();
764
+ }, this.timeouts.maxResponse);
765
+ };
766
+ if (this.sendDelayMs > 0) {
767
+ this.submitPendingUntil = Date.now() + this.sendDelayMs;
768
+ setTimeout(submit, this.sendDelayMs);
769
+ } else {
770
+ submit();
771
+ }
691
772
  }
692
773
 
693
774
  getPartialResponse(): string {
694
775
  if (!this.isWaitingForResponse) return '';
695
- const partial = this.provider.cleanOutput(this.responseBuffer);
696
- return partial || (this.isWaitingForResponse ? '(generating...)' : '');
776
+ return this.responseBuffer;
697
777
  }
698
778
 
699
779
  cancel(): void { this.shutdown(); }
@@ -717,6 +797,10 @@ export class ProviderCliAdapter implements CliAdapter {
717
797
 
718
798
  clearHistory(): void {
719
799
  this.messages = [];
800
+ this.structuredMessages = [];
801
+ this.accumulatedBuffer = '';
802
+ this.accumulatedRawBuffer = '';
803
+ this.terminalScreen.reset();
720
804
  this.onStatusChange?.();
721
805
  }
722
806
 
@@ -727,19 +811,11 @@ export class ProviderCliAdapter implements CliAdapter {
727
811
  this.ptyProcess?.write(data);
728
812
  }
729
813
 
730
- /**
731
- * Resolve an approval modal by navigating to the button at `buttonIndex` and pressing Enter.
732
- * Index 0 = first option (already selected by default — just Enter).
733
- * Index N = press Arrow Down N times, then Enter.
734
- */
735
814
  resolveModal(buttonIndex: number): void {
736
815
  if (!this.ptyProcess || this.currentStatus !== 'waiting_approval') return;
737
- // Use provider-defined approval keys if available
738
- // Each provider.json can define: { "approvalKeys": { "0": "1", "1": "2", "2": "3" } }
739
816
  if (buttonIndex in this.approvalKeys) {
740
817
  this.ptyProcess.write(this.approvalKeys[buttonIndex]);
741
818
  } else {
742
- // Generic fallback: Arrow Down to navigate + Enter to select
743
819
  const DOWN = '\x1B[B';
744
820
  const keys = DOWN.repeat(Math.max(0, buttonIndex)) + '\r';
745
821
  this.ptyProcess.write(keys);
@@ -750,27 +826,13 @@ export class ProviderCliAdapter implements CliAdapter {
750
826
  if (this.ptyProcess) {
751
827
  try {
752
828
  this.ptyProcess.resize(cols, rows);
753
- // Suppress output for 300ms after resize — PTY redraws the screen and
754
- // the redrawn content (spinners, status text) would falsely trigger generating detection
829
+ this.terminalScreen.resize(rows, cols);
755
830
  this.resizeSuppressUntil = Date.now() + 300;
756
831
  } catch { }
757
832
  }
758
833
  }
759
834
 
760
- /**
761
- * Full debug state — exposes all internal buffers, status, and patterns for debugging.
762
- * Used by DevServer /api/cli/debug endpoint.
763
- */
764
835
  getDebugState(): Record<string, any> {
765
- const sb = this.startupBuffer;
766
- const testOnStartup = (p: RegExp): boolean => {
767
- const flags = p.flags.includes('g') ? p.flags.replace(/g/g, '') : p.flags;
768
- return new RegExp(p.source, flags).test(sb);
769
- };
770
- const promptDiagnostics = this.provider.patterns.prompt.map((p) => ({
771
- pattern: p.toString(),
772
- matchedAgainstStartupBuffer: testOnStartup(p),
773
- }));
774
836
  return {
775
837
  type: this.cliType,
776
838
  name: this.cliName,
@@ -780,32 +842,24 @@ export class ProviderCliAdapter implements CliAdapter {
780
842
  spawnAt: this.spawnAt,
781
843
  workingDir: this.workingDir,
782
844
  messages: this.messages.slice(-20),
845
+ structuredMessages: this.structuredMessages.slice(-20),
783
846
  messageCount: this.messages.length,
784
- // Buffers (longer tails here than in periodic logs — for matching provider.json patterns)
785
- startupBuffer: sb.slice(-4000),
786
- startupBufferLength: sb.length,
787
- promptDiagnostics,
847
+ screenText: this.terminalScreen.getText().slice(-4000),
848
+ startupBuffer: this.startupBuffer.slice(-4000),
788
849
  recentOutputBuffer: this.recentOutputBuffer.slice(-500),
789
850
  settledBuffer: this.settledBuffer.slice(-500),
790
- responseBuffer: this.responseBuffer.slice(-500),
791
- approvalTransitionBuffer: this.approvalTransitionBuffer.slice(-500),
792
- // State
851
+ accumulatedBufferLength: this.accumulatedBuffer.length,
852
+ accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
853
+ rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
854
+ responseBuffer: this.responseBuffer.slice(-1000),
793
855
  isWaitingForResponse: this.isWaitingForResponse,
794
856
  activeModal: this.activeModal,
795
857
  lastApprovalResolvedAt: this.lastApprovalResolvedAt,
796
858
  resizeSuppressUntil: this.resizeSuppressUntil,
797
- // Provider patterns (serialized)
798
- patterns: {
799
- prompt: this.provider.patterns.prompt.map(p => p.toString()),
800
- generating: this.provider.patterns.generating.map(p => p.toString()),
801
- approval: this.provider.patterns.approval.map(p => p.toString()),
802
- ready: this.provider.patterns.ready.map(p => p.toString()),
803
- },
804
- // Status history
859
+ hasCliScripts: this.hasCliScripts(),
860
+ scriptNames: Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function'),
805
861
  statusHistory: this.statusHistory.slice(-30),
806
- // Timeouts config
807
862
  timeouts: this.timeouts,
808
- // PTY alive
809
863
  ptyAlive: !!this.ptyProcess,
810
864
  };
811
865
  }