@adhdev/daemon-core 0.5.3

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 (217) hide show
  1. package/dist/index.d.ts +2662 -0
  2. package/dist/index.js +11341 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +48 -0
  5. package/providers/_builtin/.github/workflows/generate-registry.yml +57 -0
  6. package/providers/_builtin/COMPATIBILITY.md +217 -0
  7. package/providers/_builtin/CONTRIBUTING.md +200 -0
  8. package/providers/_builtin/README.md +119 -0
  9. package/providers/_builtin/_helpers/index.js +188 -0
  10. package/providers/_builtin/acp/agentpool/provider.json +54 -0
  11. package/providers/_builtin/acp/amp/provider.json +52 -0
  12. package/providers/_builtin/acp/auggie/provider.json +57 -0
  13. package/providers/_builtin/acp/autodev/provider.json +54 -0
  14. package/providers/_builtin/acp/autohand/provider.json +52 -0
  15. package/providers/_builtin/acp/blackbox-ai/provider.json +54 -0
  16. package/providers/_builtin/acp/claude-agent/provider.json +57 -0
  17. package/providers/_builtin/acp/cline-acp/provider.json +54 -0
  18. package/providers/_builtin/acp/codebuddy/provider.json +54 -0
  19. package/providers/_builtin/acp/codex-cli/provider.json +57 -0
  20. package/providers/_builtin/acp/corust-agent/provider.json +52 -0
  21. package/providers/_builtin/acp/crow-cli/provider.json +54 -0
  22. package/providers/_builtin/acp/cursor-acp/provider.json +54 -0
  23. package/providers/_builtin/acp/deepagents/provider.json +52 -0
  24. package/providers/_builtin/acp/dimcode/provider.json +54 -0
  25. package/providers/_builtin/acp/docker-cagent/provider.json +57 -0
  26. package/providers/_builtin/acp/factory-droid/provider.json +60 -0
  27. package/providers/_builtin/acp/fast-agent/provider.json +52 -0
  28. package/providers/_builtin/acp/gemini-cli/provider.json +114 -0
  29. package/providers/_builtin/acp/github-copilot/provider.json +54 -0
  30. package/providers/_builtin/acp/goose/provider.json +57 -0
  31. package/providers/_builtin/acp/junie/provider.json +52 -0
  32. package/providers/_builtin/acp/kilo/provider.json +54 -0
  33. package/providers/_builtin/acp/kimi-cli/provider.json +57 -0
  34. package/providers/_builtin/acp/minion-code/provider.json +52 -0
  35. package/providers/_builtin/acp/mistral-vibe/provider.json +57 -0
  36. package/providers/_builtin/acp/nova/provider.json +54 -0
  37. package/providers/_builtin/acp/openclaw/provider.json +54 -0
  38. package/providers/_builtin/acp/opencode/provider.json +52 -0
  39. package/providers/_builtin/acp/openhands/provider.json +54 -0
  40. package/providers/_builtin/acp/pi-acp/provider.json +52 -0
  41. package/providers/_builtin/acp/qoder/provider.json +54 -0
  42. package/providers/_builtin/acp/qwen-code/provider.json +60 -0
  43. package/providers/_builtin/acp/stakpak/provider.json +54 -0
  44. package/providers/_builtin/acp/vtcode/provider.json +54 -0
  45. package/providers/_builtin/cli/claude-cli/provider.json +100 -0
  46. package/providers/_builtin/cli/codex-cli/provider.json +89 -0
  47. package/providers/_builtin/cli/gemini-cli/provider.json +93 -0
  48. package/providers/_builtin/docs/CDP_SELECTOR_GUIDE.md +370 -0
  49. package/providers/_builtin/docs/PROVIDER_GUIDE.md +916 -0
  50. package/providers/_builtin/extension/cline/provider.json +35 -0
  51. package/providers/_builtin/extension/cline/scripts/focus_editor.js +48 -0
  52. package/providers/_builtin/extension/cline/scripts/list_chats.js +100 -0
  53. package/providers/_builtin/extension/cline/scripts/list_models.js +43 -0
  54. package/providers/_builtin/extension/cline/scripts/list_modes.js +35 -0
  55. package/providers/_builtin/extension/cline/scripts/new_session.js +85 -0
  56. package/providers/_builtin/extension/cline/scripts/open_panel.js +25 -0
  57. package/providers/_builtin/extension/cline/scripts/read_chat.js +257 -0
  58. package/providers/_builtin/extension/cline/scripts/resolve_action.js +83 -0
  59. package/providers/_builtin/extension/cline/scripts/send_message.js +95 -0
  60. package/providers/_builtin/extension/cline/scripts/set_mode.js +36 -0
  61. package/providers/_builtin/extension/cline/scripts/set_model.js +36 -0
  62. package/providers/_builtin/extension/cline/scripts/switch_session.js +206 -0
  63. package/providers/_builtin/extension/cline/scripts.js +73 -0
  64. package/providers/_builtin/extension/roo-code/provider.json +35 -0
  65. package/providers/_builtin/extension/roo-code/scripts.js +659 -0
  66. package/providers/_builtin/ide/antigravity/provider.json +68 -0
  67. package/providers/_builtin/ide/antigravity/scripts/1.106/focus_editor.js +20 -0
  68. package/providers/_builtin/ide/antigravity/scripts/1.106/list_chats.js +137 -0
  69. package/providers/_builtin/ide/antigravity/scripts/1.106/list_models.js +38 -0
  70. package/providers/_builtin/ide/antigravity/scripts/1.106/list_modes.js +48 -0
  71. package/providers/_builtin/ide/antigravity/scripts/1.106/new_session.js +75 -0
  72. package/providers/_builtin/ide/antigravity/scripts/1.106/read_chat.js +262 -0
  73. package/providers/_builtin/ide/antigravity/scripts/1.106/resolve_action.js +68 -0
  74. package/providers/_builtin/ide/antigravity/scripts/1.106/scripts.js +57 -0
  75. package/providers/_builtin/ide/antigravity/scripts/1.106/send_message.js +56 -0
  76. package/providers/_builtin/ide/antigravity/scripts/1.106/set_mode.js +34 -0
  77. package/providers/_builtin/ide/antigravity/scripts/1.106/set_model.js +47 -0
  78. package/providers/_builtin/ide/antigravity/scripts/1.106/switch_session.js +114 -0
  79. package/providers/_builtin/ide/antigravity/scripts/1.107/focus_editor.js +20 -0
  80. package/providers/_builtin/ide/antigravity/scripts/1.107/list_chats.js +137 -0
  81. package/providers/_builtin/ide/antigravity/scripts/1.107/list_models.js +61 -0
  82. package/providers/_builtin/ide/antigravity/scripts/1.107/list_modes.js +72 -0
  83. package/providers/_builtin/ide/antigravity/scripts/1.107/new_session.js +75 -0
  84. package/providers/_builtin/ide/antigravity/scripts/1.107/read_chat.js +262 -0
  85. package/providers/_builtin/ide/antigravity/scripts/1.107/resolve_action.js +68 -0
  86. package/providers/_builtin/ide/antigravity/scripts/1.107/scripts.js +67 -0
  87. package/providers/_builtin/ide/antigravity/scripts/1.107/send_message.js +56 -0
  88. package/providers/_builtin/ide/antigravity/scripts/1.107/set_mode.js +67 -0
  89. package/providers/_builtin/ide/antigravity/scripts/1.107/set_model.js +72 -0
  90. package/providers/_builtin/ide/antigravity/scripts/1.107/switch_session.js +114 -0
  91. package/providers/_builtin/ide/cursor/provider.json +70 -0
  92. package/providers/_builtin/ide/cursor/scripts/0.49/dismiss_notification.js +30 -0
  93. package/providers/_builtin/ide/cursor/scripts/0.49/focus_editor.js +13 -0
  94. package/providers/_builtin/ide/cursor/scripts/0.49/list_models.js +78 -0
  95. package/providers/_builtin/ide/cursor/scripts/0.49/list_modes.js +40 -0
  96. package/providers/_builtin/ide/cursor/scripts/0.49/list_notifications.js +23 -0
  97. package/providers/_builtin/ide/cursor/scripts/0.49/list_sessions.js +42 -0
  98. package/providers/_builtin/ide/cursor/scripts/0.49/new_session.js +20 -0
  99. package/providers/_builtin/ide/cursor/scripts/0.49/open_panel.js +23 -0
  100. package/providers/_builtin/ide/cursor/scripts/0.49/read_chat.js +75 -0
  101. package/providers/_builtin/ide/cursor/scripts/0.49/resolve_action.js +19 -0
  102. package/providers/_builtin/ide/cursor/scripts/0.49/scripts.js +78 -0
  103. package/providers/_builtin/ide/cursor/scripts/0.49/send_message.js +23 -0
  104. package/providers/_builtin/ide/cursor/scripts/0.49/set_mode.js +38 -0
  105. package/providers/_builtin/ide/cursor/scripts/0.49/set_model.js +81 -0
  106. package/providers/_builtin/ide/cursor/scripts/0.49/switch_session.js +28 -0
  107. package/providers/_builtin/ide/kiro/provider.json +67 -0
  108. package/providers/_builtin/ide/kiro/scripts/focus_editor.js +20 -0
  109. package/providers/_builtin/ide/kiro/scripts/open_panel.js +47 -0
  110. package/providers/_builtin/ide/kiro/scripts/resolve_action.js +54 -0
  111. package/providers/_builtin/ide/kiro/scripts/send_message.js +29 -0
  112. package/providers/_builtin/ide/kiro/scripts/webview_list_models.js +39 -0
  113. package/providers/_builtin/ide/kiro/scripts/webview_list_modes.js +39 -0
  114. package/providers/_builtin/ide/kiro/scripts/webview_list_sessions.js +21 -0
  115. package/providers/_builtin/ide/kiro/scripts/webview_new_session.js +34 -0
  116. package/providers/_builtin/ide/kiro/scripts/webview_read_chat.js +68 -0
  117. package/providers/_builtin/ide/kiro/scripts/webview_send_message.js +72 -0
  118. package/providers/_builtin/ide/kiro/scripts/webview_set_mode.js +15 -0
  119. package/providers/_builtin/ide/kiro/scripts/webview_set_model.js +15 -0
  120. package/providers/_builtin/ide/kiro/scripts/webview_switch_session.js +26 -0
  121. package/providers/_builtin/ide/kiro/scripts.js +62 -0
  122. package/providers/_builtin/ide/pearai/provider.json +67 -0
  123. package/providers/_builtin/ide/pearai/scripts/focus_editor.js +20 -0
  124. package/providers/_builtin/ide/pearai/scripts/list_sessions.js +38 -0
  125. package/providers/_builtin/ide/pearai/scripts/new_session.js +55 -0
  126. package/providers/_builtin/ide/pearai/scripts/open_panel.js +46 -0
  127. package/providers/_builtin/ide/pearai/scripts/resolve_action.js +54 -0
  128. package/providers/_builtin/ide/pearai/scripts/send_message.js +29 -0
  129. package/providers/_builtin/ide/pearai/scripts/webview_list_models.js +43 -0
  130. package/providers/_builtin/ide/pearai/scripts/webview_list_modes.js +35 -0
  131. package/providers/_builtin/ide/pearai/scripts/webview_list_sessions.js +62 -0
  132. package/providers/_builtin/ide/pearai/scripts/webview_new_session.js +49 -0
  133. package/providers/_builtin/ide/pearai/scripts/webview_read_chat.js +92 -0
  134. package/providers/_builtin/ide/pearai/scripts/webview_resolve_action.js +59 -0
  135. package/providers/_builtin/ide/pearai/scripts/webview_send_message.js +72 -0
  136. package/providers/_builtin/ide/pearai/scripts/webview_set_mode.js +36 -0
  137. package/providers/_builtin/ide/pearai/scripts/webview_set_model.js +36 -0
  138. package/providers/_builtin/ide/pearai/scripts/webview_switch_session.js +34 -0
  139. package/providers/_builtin/ide/pearai/scripts.js +74 -0
  140. package/providers/_builtin/ide/trae/provider.json +66 -0
  141. package/providers/_builtin/ide/trae/scripts/focus_editor.js +20 -0
  142. package/providers/_builtin/ide/trae/scripts/list_chats.js +24 -0
  143. package/providers/_builtin/ide/trae/scripts/list_models.js +39 -0
  144. package/providers/_builtin/ide/trae/scripts/list_modes.js +39 -0
  145. package/providers/_builtin/ide/trae/scripts/new_session.js +30 -0
  146. package/providers/_builtin/ide/trae/scripts/open_panel.js +44 -0
  147. package/providers/_builtin/ide/trae/scripts/read_chat.js +113 -0
  148. package/providers/_builtin/ide/trae/scripts/resolve_action.js +54 -0
  149. package/providers/_builtin/ide/trae/scripts/send_message.js +69 -0
  150. package/providers/_builtin/ide/trae/scripts/set_mode.js +15 -0
  151. package/providers/_builtin/ide/trae/scripts/set_model.js +15 -0
  152. package/providers/_builtin/ide/trae/scripts/switch_session.js +23 -0
  153. package/providers/_builtin/ide/trae/scripts.js +57 -0
  154. package/providers/_builtin/ide/vscode/provider.json +64 -0
  155. package/providers/_builtin/ide/vscode-insiders/provider.json +62 -0
  156. package/providers/_builtin/ide/vscodium/provider.json +63 -0
  157. package/providers/_builtin/ide/windsurf/provider.json +53 -0
  158. package/providers/_builtin/ide/windsurf/scripts/focus_editor.js +30 -0
  159. package/providers/_builtin/ide/windsurf/scripts/list_chats.js +117 -0
  160. package/providers/_builtin/ide/windsurf/scripts/list_models.js +39 -0
  161. package/providers/_builtin/ide/windsurf/scripts/list_modes.js +39 -0
  162. package/providers/_builtin/ide/windsurf/scripts/new_session.js +69 -0
  163. package/providers/_builtin/ide/windsurf/scripts/open_panel.js +58 -0
  164. package/providers/_builtin/ide/windsurf/scripts/read_chat.js +297 -0
  165. package/providers/_builtin/ide/windsurf/scripts/resolve_action.js +68 -0
  166. package/providers/_builtin/ide/windsurf/scripts/send_message.js +87 -0
  167. package/providers/_builtin/ide/windsurf/scripts/set_mode.js +15 -0
  168. package/providers/_builtin/ide/windsurf/scripts/set_model.js +15 -0
  169. package/providers/_builtin/ide/windsurf/scripts/switch_session.js +58 -0
  170. package/providers/_builtin/ide/windsurf/scripts.js +57 -0
  171. package/providers/_builtin/registry.json +266 -0
  172. package/providers/_builtin/validate.js +156 -0
  173. package/src/agent-stream/index.ts +6 -0
  174. package/src/agent-stream/manager.ts +286 -0
  175. package/src/agent-stream/poller.ts +154 -0
  176. package/src/agent-stream/provider-adapter.ts +138 -0
  177. package/src/agent-stream/types.ts +61 -0
  178. package/src/boot/daemon-lifecycle.ts +252 -0
  179. package/src/cdp/devtools.ts +335 -0
  180. package/src/cdp/initializer.ts +191 -0
  181. package/src/cdp/manager.ts +897 -0
  182. package/src/cdp/scanner.ts +185 -0
  183. package/src/cdp/setup.ts +150 -0
  184. package/src/cli-adapter-types.ts +25 -0
  185. package/src/cli-adapters/provider-cli-adapter.ts +448 -0
  186. package/src/commands/cdp-commands.ts +208 -0
  187. package/src/commands/chat-commands.ts +675 -0
  188. package/src/commands/cli-manager.ts +353 -0
  189. package/src/commands/handler.ts +328 -0
  190. package/src/commands/router.ts +258 -0
  191. package/src/commands/stream-commands.ts +325 -0
  192. package/src/config/chat-history.ts +211 -0
  193. package/src/config/config.ts +219 -0
  194. package/src/daemon/dev-server.ts +2378 -0
  195. package/src/daemon/scaffold-template.ts +394 -0
  196. package/src/daemon-core.ts +50 -0
  197. package/src/detection/cli-detector.ts +89 -0
  198. package/src/detection/ide-detector.ts +157 -0
  199. package/src/index.ts +103 -0
  200. package/src/installer.ts +263 -0
  201. package/src/ipc-protocol.ts +133 -0
  202. package/src/launch.ts +433 -0
  203. package/src/logging/command-log.ts +180 -0
  204. package/src/logging/logger.ts +316 -0
  205. package/src/providers/acp-provider-instance.ts +1140 -0
  206. package/src/providers/cli-provider-instance.ts +207 -0
  207. package/src/providers/contracts.ts +524 -0
  208. package/src/providers/extension-provider-instance.ts +156 -0
  209. package/src/providers/ide-provider-instance.ts +377 -0
  210. package/src/providers/index.ts +18 -0
  211. package/src/providers/provider-instance-manager.ts +182 -0
  212. package/src/providers/provider-instance.ts +112 -0
  213. package/src/providers/provider-loader.ts +1031 -0
  214. package/src/providers/status-monitor.ts +125 -0
  215. package/src/providers/version-archive.ts +266 -0
  216. package/src/status/reporter.ts +294 -0
  217. package/src/types.ts +206 -0
@@ -0,0 +1,1140 @@
1
+ /**
2
+ * AcpProviderInstance — ACP (Agent Client Protocol) Provider runtime instance
3
+ *
4
+ * Spawns ACP agent process and communicates via the official ACP SDK.
5
+ * Uses ClientSideConnection + ndJsonStream for structured protocol communication.
6
+ *
7
+ * ACP spec: https://agentclientprotocol.com
8
+ * ACP SDK: @agentclientprotocol/sdk@0.16.1
9
+ *
10
+ * lifecycle:
11
+ * 1. init() → Spawn agent process + ACP initialize handshake
12
+ * 2. onTick() → no-op (ACP event based)
13
+ * 3. getState() → ProviderState return (dashboard for display)
14
+ * 4. onEvent('send_message') → session/prompt transmit
15
+ * 5. dispose() → kill process
16
+ */
17
+
18
+ import { Readable, Writable } from 'stream';
19
+ import { spawn, type ChildProcess } from 'child_process';
20
+ import {
21
+ ClientSideConnection,
22
+ ndJsonStream,
23
+ RequestError,
24
+ PROTOCOL_VERSION,
25
+ type Client,
26
+ type Agent,
27
+ type SessionNotification,
28
+ type RequestPermissionRequest,
29
+ type RequestPermissionResponse,
30
+ type WriteTextFileRequest,
31
+ type WriteTextFileResponse,
32
+ type ReadTextFileRequest,
33
+ type ReadTextFileResponse,
34
+ type CreateTerminalRequest,
35
+ type CreateTerminalResponse,
36
+ type TerminalOutputRequest,
37
+ type TerminalOutputResponse,
38
+ type ReleaseTerminalRequest,
39
+ type ReleaseTerminalResponse,
40
+ type WaitForTerminalExitRequest,
41
+ type WaitForTerminalExitResponse,
42
+ type KillTerminalRequest,
43
+ type KillTerminalResponse,
44
+ type SessionUpdate,
45
+ type ToolCallStatus,
46
+ } from '@agentclientprotocol/sdk';
47
+ import type { ProviderModule, ContentBlock, ToolCallInfo, ToolCallContent as TCC, ToolKind, ToolCallStatus as TCS } from './contracts.js';
48
+ import { normalizeContent, flattenContent } from './contracts.js';
49
+ import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
50
+ import { StatusMonitor } from './status-monitor.js';
51
+ import { LOG } from '../logging/logger.js';
52
+
53
+ // ─── Internal Display Types (dashboard용) ────────────────────────────
54
+
55
+ interface AcpMessage {
56
+ role: 'user' | 'assistant' | 'system';
57
+ /** Rich content blocks (ACP standard) or plain text (legacy) */
58
+ content: string | ContentBlock[];
59
+ timestamp?: number;
60
+ /** Tool calls associated with this message */
61
+ toolCalls?: ToolCallInfo[];
62
+ }
63
+
64
+ interface AcpToolCall {
65
+ id: string;
66
+ name: string;
67
+ status: 'running' | 'completed' | 'failed';
68
+ input?: string;
69
+ output?: string;
70
+ }
71
+
72
+ interface AcpConfigOption {
73
+ category: 'model' | 'mode' | 'thought_level' | 'other';
74
+ configId: string;
75
+ currentValue?: string;
76
+ options: { value: string; name: string; description?: string; group?: string }[];
77
+ }
78
+
79
+ interface AcpMode {
80
+ id: string;
81
+ name: string;
82
+ description?: string;
83
+ }
84
+
85
+ // ─── AcpProviderInstance ───────────────────────────
86
+
87
+ export class AcpProviderInstance implements ProviderInstance {
88
+ readonly type: string;
89
+ readonly category = 'acp' as const;
90
+ private readonly log = LOG.forComponent('ACP');
91
+
92
+ private provider: ProviderModule;
93
+ private context: InstanceContext | null = null;
94
+ private settings: Record<string, any> = {};
95
+ private events: ProviderEvent[] = [];
96
+ private monitor: StatusMonitor;
97
+
98
+ // Process
99
+ private process: ChildProcess | null = null;
100
+ private connection: ClientSideConnection | null = null;
101
+
102
+ // State
103
+ private sessionId: string | null = null;
104
+ private messages: AcpMessage[] = [];
105
+ private currentStatus: ProviderState['status'] = 'starting';
106
+ private lastStatus: string = 'starting';
107
+ private generatingStartedAt = 0;
108
+ private agentCapabilities: Record<string, any> = {};
109
+ private currentModel: string | undefined;
110
+ private currentMode: string | undefined;
111
+ private activeToolCalls: AcpToolCall[] = [];
112
+ private stopReason: string | null = null;
113
+ private partialContent = '';
114
+ /** Rich content blocks accumulated during streaming */
115
+ private partialBlocks: ContentBlock[] = [];
116
+ /** Tool calls collected during current turn */
117
+ private turnToolCalls: ToolCallInfo[] = [];
118
+
119
+ // Error tracking
120
+ private errorMessage: string | null = null;
121
+ private errorReason: 'not_installed' | 'auth_failed' | 'spawn_error' | 'init_failed' | 'crash' | null = null;
122
+ private stderrBuffer: string[] = [];
123
+ private spawnedAt = 0;
124
+
125
+ // ACP ConfigOptions & Modes (from session/new response or static fallback)
126
+ private configOptions: AcpConfigOption[] = [];
127
+ private availableModes: AcpMode[] = [];
128
+ /** Static config mode — agent doesn't support config/* methods */
129
+ private useStaticConfig = false;
130
+ /** Current config selections (for spawnArgBuilder) */
131
+ private selectedConfig: Record<string, string> = {};
132
+
133
+ // Config
134
+ private workingDir: string;
135
+ private instanceId: string;
136
+
137
+ constructor(
138
+ provider: ProviderModule,
139
+ workingDir: string,
140
+ private cliArgs: string[] = [],
141
+ ) {
142
+ this.type = provider.type;
143
+ this.provider = provider;
144
+ this.workingDir = workingDir;
145
+ this.instanceId = crypto.randomUUID();
146
+
147
+ this.monitor = new StatusMonitor();
148
+ }
149
+
150
+ // ─── Lifecycle ─────────────────────────────────
151
+
152
+ async init(context: InstanceContext): Promise<void> {
153
+ this.context = context;
154
+ this.settings = context.settings || {};
155
+ this.monitor.updateConfig({
156
+ approvalAlert: this.settings.approvalAlert !== false,
157
+ longGeneratingAlert: this.settings.longGeneratingAlert !== false,
158
+ longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180,
159
+ });
160
+
161
+ await this.spawnAgent();
162
+ }
163
+
164
+ async onTick(): Promise<void> {
165
+ // ACP event based — tick unnecessary
166
+ // Run process health check only
167
+ if (this.process && this.process.exitCode !== null) {
168
+ this.currentStatus = 'stopped';
169
+ this.detectStatusTransition();
170
+ }
171
+ }
172
+
173
+ getState(): ProviderState {
174
+ const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
175
+
176
+ // Recent 50 messages
177
+ const recentMessages = this.messages.slice(-50).map(m => {
178
+ const content = this.truncateContent(m.content);
179
+ return {
180
+ role: m.role,
181
+ content,
182
+ timestamp: m.timestamp,
183
+ toolCalls: m.toolCalls,
184
+ };
185
+ });
186
+
187
+ // generating during partial response add
188
+ if (this.currentStatus === 'generating' && (this.partialContent || this.partialBlocks.length > 0)) {
189
+ const blocks = this.buildPartialBlocks();
190
+ if (blocks.length > 0) {
191
+ recentMessages.push({
192
+ role: 'assistant',
193
+ content: blocks,
194
+ timestamp: Date.now(),
195
+ toolCalls: this.turnToolCalls.length > 0 ? [...this.turnToolCalls] : undefined,
196
+ });
197
+ }
198
+ }
199
+
200
+ return {
201
+ type: this.type,
202
+ name: this.provider.name,
203
+ category: 'acp',
204
+ status: this.currentStatus,
205
+ mode: 'chat',
206
+ activeChat: {
207
+ id: this.sessionId || `${this.type}_${this.workingDir}`,
208
+ title: `${this.provider.name} · ${dirName}`,
209
+ status: this.currentStatus,
210
+ messages: recentMessages,
211
+ activeModal: this.currentStatus === 'waiting_approval' ? {
212
+ message: this.activeToolCalls.find(t => t.status === 'running')?.name || 'Permission requested',
213
+ buttons: ['Approve', 'Reject'],
214
+ } : null,
215
+ inputContent: '',
216
+ },
217
+ workingDir: this.workingDir,
218
+ currentModel: this.currentModel,
219
+ currentPlan: this.currentMode,
220
+ instanceId: this.instanceId,
221
+ lastUpdated: Date.now(),
222
+ settings: this.settings,
223
+ pendingEvents: this.flushEvents(),
224
+ // ACP-specific: expose available models/modes for dashboard
225
+ acpConfigOptions: this.configOptions,
226
+ acpModes: this.availableModes,
227
+ // Error details for dashboard display
228
+ errorMessage: this.errorMessage,
229
+ errorReason: this.errorReason,
230
+ } as any;
231
+ }
232
+
233
+ onEvent(event: string, data?: any): void {
234
+ if (event === 'send_message' && data?.text) {
235
+ this.sendPrompt(data.text).catch(e =>
236
+ this.log.warn(`[${this.type}] sendPrompt error: ${e?.message}`)
237
+ );
238
+ } else if (event === 'resolve_action') {
239
+ const action = data?.action || 'approve';
240
+ this.resolvePermission(action === 'approve' || action === 'accept')
241
+ .catch(e => this.log.warn(`[${this.type}] resolvePermission error: ${e?.message}`));
242
+ } else if (event === 'cancel') {
243
+ this.cancelSession().catch(e =>
244
+ this.log.warn(`[${this.type}] cancel error: ${e?.message}`)
245
+ );
246
+ } else if (event === 'change_model' && data?.model) {
247
+ this.setConfigOption('model', data.model).catch(e =>
248
+ this.log.warn(`[${this.type}] change_model error: ${e?.message}`)
249
+ );
250
+ } else if (event === 'set_mode' && data?.mode) {
251
+ this.setMode(data.mode).catch(e =>
252
+ this.log.warn(`[${this.type}] set_mode error: ${e?.message}`)
253
+ );
254
+ } else if (event === 'set_thought_level' && data?.level) {
255
+ this.setConfigOption('thought_level', data.level).catch(e =>
256
+ this.log.warn(`[${this.type}] set_thought_level error: ${e?.message}`)
257
+ );
258
+ }
259
+ }
260
+
261
+ // ─── ACP Config Options & Modes ─────────────────────
262
+
263
+ private parseConfigOptions(raw: any): void {
264
+ if (!Array.isArray(raw)) return;
265
+ this.configOptions = [];
266
+ for (const opt of raw) {
267
+ const category = opt.category || 'other';
268
+ const configId = opt.configId || opt.id || '';
269
+ const currentValue = opt.currentValue ?? opt.select?.currentValue;
270
+
271
+ // flatten options (ungrouped + grouped)
272
+ const flatOptions: AcpConfigOption['options'] = [];
273
+ const selectOpts = opt.select?.options || opt.options;
274
+ if (selectOpts) {
275
+ // ungrouped options
276
+ if (Array.isArray(selectOpts.ungrouped)) {
277
+ for (const o of selectOpts.ungrouped) {
278
+ flatOptions.push({ value: o.value, name: o.name || o.value, description: o.description });
279
+ }
280
+ }
281
+ // grouped options
282
+ if (Array.isArray(selectOpts.grouped)) {
283
+ for (const g of selectOpts.grouped) {
284
+ const groupName = g.name || g.group || '';
285
+ for (const o of (Array.isArray(g.options?.ungrouped) ? g.options.ungrouped : (g.options || []))) {
286
+ flatOptions.push({ value: o.value, name: o.name || o.value, description: o.description, group: groupName });
287
+ }
288
+ }
289
+ }
290
+ // direct array
291
+ if (Array.isArray(selectOpts)) {
292
+ for (const o of selectOpts) {
293
+ if (o.value) flatOptions.push({ value: o.value, name: o.name || o.value, description: o.description });
294
+ }
295
+ }
296
+ }
297
+
298
+ this.configOptions.push({ category: category as any, configId, currentValue, options: flatOptions });
299
+
300
+ // Auto-set currentModel/currentMode from config
301
+ if (category === 'model' && currentValue) this.currentModel = currentValue;
302
+ }
303
+ }
304
+
305
+ private parseModes(raw: any): void {
306
+ if (!raw) return;
307
+ // modes: { currentModeId, availableModes: [{ id, name, description }] }
308
+ if (raw.currentModeId) this.currentMode = raw.currentModeId;
309
+ if (Array.isArray(raw.availableModes)) {
310
+ this.availableModes = raw.availableModes.map((m: any) => ({
311
+ id: m.id, name: m.name || m.id, description: m.description,
312
+ }));
313
+ }
314
+ }
315
+
316
+ async setConfigOption(category: string, value: string): Promise<void> {
317
+ // Find configId for this category
318
+ const opt = this.configOptions.find(c => c.category === category);
319
+ if (!opt) {
320
+ this.log.warn(`[${this.type}] No config option for category: ${category}`);
321
+ return;
322
+ }
323
+
324
+ // Static config mode: update selection and restart process
325
+ if (this.useStaticConfig) {
326
+ opt.currentValue = value;
327
+ this.selectedConfig[opt.configId] = value;
328
+ if (category === 'model') this.currentModel = value;
329
+ if (category === 'mode') this.currentMode = value;
330
+ this.log.info(`[${this.type}] Static config ${category} set to: ${value} — restarting agent`);
331
+ await this.restartWithNewConfig();
332
+ return;
333
+ }
334
+
335
+ if (!this.connection || !this.sessionId) {
336
+ this.log.warn(`[${this.type}] Cannot set config: no active connection/session`);
337
+ return;
338
+ }
339
+
340
+ try {
341
+ this.log.info(`[${this.type}] Sending session/set_config_option: configId=${opt.configId} value=${value} sessionId=${this.sessionId}`);
342
+ const result = await this.connection.setSessionConfigOption({
343
+ sessionId: this.sessionId,
344
+ configId: opt.configId,
345
+ value,
346
+ });
347
+ // Update local state
348
+ opt.currentValue = value;
349
+ if (category === 'model') this.currentModel = value;
350
+ // Response may include updated configOptions
351
+ if (result?.configOptions) this.parseConfigOptions(result.configOptions);
352
+ this.log.info(`[${this.type}] Config ${category} set to: ${value} | response: ${JSON.stringify(result)?.slice(0, 300)}`);
353
+ } catch (e: any) {
354
+ this.log.warn(`[${this.type}] set_config_option failed: ${e?.message}`);
355
+ }
356
+ }
357
+
358
+ async setMode(modeId: string): Promise<void> {
359
+ // Static config: mode changes via restart
360
+ if (this.useStaticConfig) {
361
+ const opt = this.configOptions.find(c => c.category === 'mode');
362
+ if (opt) {
363
+ opt.currentValue = modeId;
364
+ this.selectedConfig[opt.configId] = modeId;
365
+ }
366
+ this.currentMode = modeId;
367
+ this.log.info(`[${this.type}] Static mode set to: ${modeId} — restarting agent`);
368
+ await this.restartWithNewConfig();
369
+ return;
370
+ }
371
+
372
+ if (!this.connection || !this.sessionId) {
373
+ this.log.warn(`[${this.type}] Cannot set mode: no active connection/session`);
374
+ return;
375
+ }
376
+
377
+ try {
378
+ await this.connection.setSessionMode({
379
+ sessionId: this.sessionId,
380
+ modeId,
381
+ });
382
+ this.currentMode = modeId;
383
+ this.log.info(`[${this.type}] Mode set to: ${modeId}`);
384
+ } catch (e: any) {
385
+ this.log.warn(`[${this.type}] set_mode failed: ${e?.message}`);
386
+ }
387
+ }
388
+
389
+ /** Static config: kill process and restart with new args */
390
+ private async restartWithNewConfig(): Promise<void> {
391
+ // Build new args from spawnArgBuilder
392
+ if (this.provider.spawnArgBuilder) {
393
+ this.cliArgs = []; // clear previous extra args
394
+ }
395
+
396
+ // Kill existing process
397
+ if (this.process) {
398
+ try { this.process.kill('SIGTERM'); } catch { }
399
+ this.process = null;
400
+ }
401
+ this.connection = null;
402
+ this.sessionId = null;
403
+
404
+ this.currentStatus = 'starting';
405
+ this.detectStatusTransition();
406
+
407
+ // Re-spawn with updated config
408
+ await this.spawnAgent();
409
+ }
410
+
411
+ /** Update settings at runtime (called when user changes settings from dashboard) */
412
+ updateSettings(newSettings: Record<string, any>): void {
413
+ this.settings = { ...this.settings, ...newSettings };
414
+ this.monitor.updateConfig({
415
+ approvalAlert: this.settings.approvalAlert !== false,
416
+ longGeneratingAlert: this.settings.longGeneratingAlert !== false,
417
+ longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180,
418
+ });
419
+ this.log.info(`[${this.type}] Settings updated: ${Object.keys(newSettings).join(', ')}`);
420
+ }
421
+
422
+ dispose(): void {
423
+ // kill process
424
+ if (this.process) {
425
+ try { this.process.kill('SIGTERM'); } catch { }
426
+ this.process = null;
427
+ }
428
+ this.connection = null;
429
+ this.monitor.reset();
430
+ }
431
+
432
+ // ─── ACP Process Management ──────────────────────
433
+
434
+ private async spawnAgent(): Promise<void> {
435
+ const spawnConfig = this.provider.spawn;
436
+ if (!spawnConfig) {
437
+ throw new Error(`[ACP:${this.type}] No spawn config defined`);
438
+ }
439
+
440
+ const command = spawnConfig.command;
441
+ // Static config: create args via spawnArgBuilder (when provider defines it)
442
+ let baseArgs = spawnConfig.args || [];
443
+ if (this.provider.spawnArgBuilder && Object.keys(this.selectedConfig).length > 0) {
444
+ baseArgs = this.provider.spawnArgBuilder(this.selectedConfig);
445
+ }
446
+ const args = [...baseArgs, ...this.cliArgs];
447
+
448
+ // Auth: each CLI/ACP tool manages its own authentication.
449
+ // ADHDev does NOT inject API keys — tools read their own env vars or config files.
450
+
451
+ const env = { ...process.env, ...(spawnConfig.env || {}) };
452
+
453
+ this.log.info(`[${this.type}] Spawning: ${command} ${args.join(' ')} in ${this.workingDir}`);
454
+
455
+ this.spawnedAt = Date.now();
456
+ this.errorMessage = null;
457
+ this.errorReason = null;
458
+ this.stderrBuffer = [];
459
+
460
+ this.process = spawn(command, args, {
461
+ cwd: this.workingDir,
462
+ env,
463
+ stdio: ['pipe', 'pipe', 'pipe'],
464
+ shell: spawnConfig.shell || false,
465
+ });
466
+
467
+ // stderr → log + auth failure detection
468
+ const AUTH_ERROR_PATTERNS = [
469
+ /unauthorized|unauthenticated/i,
470
+ /invalid.*(?:api[_ ]?key|token|credential)/i,
471
+ /auth(?:entication|orization).*(?:fail|error|denied|invalid|expired)/i,
472
+ /(?:api[_ ]?key|token).*(?:missing|required|not set|not found|invalid|expired)/i,
473
+ /ENOENT|command not found|not recognized/i,
474
+ /permission denied/i,
475
+ /rate.?limit|quota.?exceeded/i,
476
+ /login.*required|please.*(?:login|authenticate|sign.?in)/i,
477
+ ];
478
+
479
+ this.process.stderr?.on('data', (data) => {
480
+ const text = data.toString().trim();
481
+ if (!text) return;
482
+ this.log.debug(`[${this.type}:stderr] ${text.slice(0, 300)}`);
483
+
484
+ // Maintain stderr buffer (recent 20 lines)
485
+ this.stderrBuffer.push(text);
486
+ if (this.stderrBuffer.length > 20) this.stderrBuffer.shift();
487
+
488
+ // Auth failure detection
489
+ for (const pattern of AUTH_ERROR_PATTERNS) {
490
+ if (pattern.test(text)) {
491
+ if (/ENOENT|command not found|not recognized/i.test(text)) {
492
+ this.errorReason = 'not_installed';
493
+ this.errorMessage = `Command '${command}' not found. Install: ${(this.provider as any).install || 'check documentation'}`;
494
+ } else {
495
+ this.errorReason = 'auth_failed';
496
+ this.errorMessage = text.slice(0, 300);
497
+ }
498
+ this.log.warn(`[${this.type}] Error detected (${this.errorReason}): ${this.errorMessage?.slice(0, 100)}`);
499
+ break;
500
+ }
501
+ }
502
+ });
503
+
504
+ // kill process detect
505
+ this.process.on('exit', (code, signal) => {
506
+ const elapsed = Date.now() - this.spawnedAt;
507
+ this.log.info(`[${this.type}] Process exited: code=${code} signal=${signal} elapsed=${elapsed}ms`);
508
+
509
+ // Exit code analysis
510
+ if (code !== 0 && code !== null) {
511
+ if (!this.errorReason) {
512
+ if (code === 127) {
513
+ this.errorReason = 'not_installed';
514
+ this.errorMessage = `Command '${command}' not found (exit code 127). Install: ${(this.provider as any).install || 'check documentation'}`;
515
+ } else if (elapsed < 3000) {
516
+ // 3-second crash → likely install/auth issue
517
+ this.errorReason = this.stderrBuffer.length > 0 ? 'crash' : 'spawn_error';
518
+ this.errorMessage = this.stderrBuffer.length > 0
519
+ ? `Agent crashed immediately (exit code ${code}): ${this.stderrBuffer.slice(-3).join(' | ').slice(0, 300)}`
520
+ : `Agent exited immediately with code ${code}. The agent may not be installed correctly.`;
521
+ } else {
522
+ this.errorReason = 'crash';
523
+ this.errorMessage = `Agent exited with code ${code}${this.stderrBuffer.length > 0 ? ': ' + this.stderrBuffer.slice(-1)[0]?.slice(0, 200) : ''}`;
524
+ }
525
+ }
526
+ }
527
+
528
+ this.currentStatus = this.errorReason ? 'error' : 'stopped';
529
+ this.detectStatusTransition();
530
+ });
531
+
532
+ this.process.on('error', (err) => {
533
+ this.log.error(`[${this.type}] Process spawn error: ${err.message}`);
534
+ if (err.message.includes('ENOENT')) {
535
+ this.errorReason = 'not_installed';
536
+ this.errorMessage = `Command '${command}' not found. Install: ${(this.provider as any).install || 'check documentation'}`;
537
+ } else {
538
+ this.errorReason = 'spawn_error';
539
+ this.errorMessage = err.message;
540
+ }
541
+ this.currentStatus = 'error';
542
+ this.detectStatusTransition();
543
+ });
544
+
545
+ // ─── SDK Connection Setup ────────────────────────
546
+ // Convert Node.js streams to Web Streams for ndJsonStream
547
+ const webStdin = Writable.toWeb(this.process.stdin!) as WritableStream<Uint8Array>;
548
+ const webStdout = Readable.toWeb(this.process.stdout!) as ReadableStream<Uint8Array>;
549
+ const stream = ndJsonStream(webStdin, webStdout);
550
+
551
+ // Create ClientSideConnection with our Client implementation
552
+ this.connection = new ClientSideConnection((_agent: Agent) => this.createClient(), stream);
553
+
554
+ // Listen for connection close
555
+ this.connection.signal.addEventListener('abort', () => {
556
+ this.log.info(`[${this.type}] ACP connection closed`);
557
+ });
558
+
559
+ // ACP initialize handshake
560
+ await this.initialize();
561
+ }
562
+
563
+ // ─── Client Interface Implementation ────────────────────
564
+
565
+ private createClient(): Client {
566
+ return {
567
+ requestPermission: async (params: RequestPermissionRequest): Promise<RequestPermissionResponse> => {
568
+ // Update active tool calls from the request
569
+ const tc = params.toolCall;
570
+ const existing = this.activeToolCalls.find(t => t.id === tc.toolCallId);
571
+ if (existing) {
572
+ existing.status = 'running';
573
+ if (tc.title) existing.name = tc.title;
574
+ } else {
575
+ this.activeToolCalls.push({
576
+ id: tc.toolCallId,
577
+ name: tc.title || 'unknown',
578
+ status: 'running',
579
+ input: tc.rawInput ? (typeof tc.rawInput === 'string' ? tc.rawInput : JSON.stringify(tc.rawInput)) : undefined,
580
+ });
581
+ }
582
+
583
+ // ─── Auto-approve: skip user confirmation ───
584
+ if (this.settings.autoApprove) {
585
+ this.log.info(`[${this.type}] Auto-approving: ${tc.title || tc.toolCallId}`);
586
+ const allowOption = params.options.find(o => o.kind === 'allow_once') || params.options.find(o => o.kind === 'allow_always');
587
+ if (allowOption) {
588
+ return { outcome: { outcome: 'selected', optionId: allowOption.optionId } };
589
+ }
590
+ return { outcome: { outcome: 'selected', optionId: params.options[0]?.optionId || '' } };
591
+ }
592
+
593
+ // Approval request → switch to waiting_approval status
594
+ this.currentStatus = 'waiting_approval';
595
+ this.detectStatusTransition();
596
+
597
+ // Wait for user approval
598
+ const approved = await new Promise<boolean>((resolve) => {
599
+ this.permissionResolvers.push(resolve);
600
+ // 5-minute timeout → auto-reject
601
+ setTimeout(() => {
602
+ const idx = this.permissionResolvers.indexOf(resolve);
603
+ if (idx >= 0) {
604
+ this.permissionResolvers.splice(idx, 1);
605
+ resolve(false);
606
+ }
607
+ }, 300_000);
608
+ });
609
+
610
+ if (approved) {
611
+ // Find the "allow" option (allow_once or allow_always)
612
+ const allowOption = params.options.find(o => o.kind === 'allow_once') || params.options.find(o => o.kind === 'allow_always');
613
+ if (allowOption) {
614
+ return { outcome: { outcome: 'selected', optionId: allowOption.optionId } };
615
+ }
616
+ // Fallback: use first option
617
+ return { outcome: { outcome: 'selected', optionId: params.options[0]?.optionId || '' } };
618
+ } else {
619
+ // Find the "reject" option
620
+ const rejectOption = params.options.find(o => o.kind === 'reject_once') || params.options.find(o => o.kind === 'reject_always');
621
+ if (rejectOption) {
622
+ return { outcome: { outcome: 'selected', optionId: rejectOption.optionId } };
623
+ }
624
+ return { outcome: { outcome: 'cancelled' } };
625
+ }
626
+ },
627
+
628
+ sessionUpdate: async (params: SessionNotification): Promise<void> => {
629
+ this.handleSessionUpdate(params);
630
+ },
631
+
632
+ // File system — not supported
633
+ readTextFile: async (_params: ReadTextFileRequest): Promise<ReadTextFileResponse> => {
634
+ throw RequestError.methodNotFound('fs/read_text_file');
635
+ },
636
+ writeTextFile: async (_params: WriteTextFileRequest): Promise<WriteTextFileResponse> => {
637
+ throw RequestError.methodNotFound('fs/write_text_file');
638
+ },
639
+
640
+ // Terminal — not supported
641
+ createTerminal: async (_params: CreateTerminalRequest): Promise<CreateTerminalResponse> => {
642
+ throw RequestError.methodNotFound('terminal/create');
643
+ },
644
+ terminalOutput: async (_params: TerminalOutputRequest): Promise<TerminalOutputResponse> => {
645
+ throw RequestError.methodNotFound('terminal/output');
646
+ },
647
+ releaseTerminal: async (_params: ReleaseTerminalRequest): Promise<ReleaseTerminalResponse> => {
648
+ throw RequestError.methodNotFound('terminal/release');
649
+ },
650
+ waitForTerminalExit: async (_params: WaitForTerminalExitRequest): Promise<WaitForTerminalExitResponse> => {
651
+ throw RequestError.methodNotFound('terminal/wait_for_exit');
652
+ },
653
+ killTerminal: async (_params: KillTerminalRequest): Promise<KillTerminalResponse> => {
654
+ throw RequestError.methodNotFound('terminal/kill');
655
+ },
656
+ };
657
+ }
658
+
659
+ // ─── ACP Protocol (via SDK) ────────────────────────────
660
+
661
+ private async initialize(): Promise<void> {
662
+ if (!this.connection) return;
663
+
664
+ try {
665
+ const result = await this.connection.initialize({
666
+ protocolVersion: PROTOCOL_VERSION,
667
+ clientCapabilities: {},
668
+ });
669
+
670
+ this.agentCapabilities = result?.agentCapabilities || {};
671
+ this.log.info(`[${this.type}] Initialized. Agent capabilities: ${JSON.stringify(this.agentCapabilities)}`);
672
+
673
+ // new session create
674
+ await this.createSession();
675
+ } catch (e: any) {
676
+ this.log.error(`[${this.type}] Initialize failed: ${e?.message}`);
677
+ if (!this.errorReason) {
678
+ this.errorReason = 'init_failed';
679
+ this.errorMessage = `ACP handshake failed: ${e?.message}${this.stderrBuffer.length > 0 ? '\n' + this.stderrBuffer.slice(-2).join('\n').slice(0, 200) : ''}`;
680
+ }
681
+ this.currentStatus = 'error';
682
+ }
683
+ }
684
+
685
+ private async createSession(): Promise<void> {
686
+ if (!this.connection) return;
687
+
688
+ try {
689
+ const result = await this.connection.newSession({
690
+ cwd: this.workingDir,
691
+ mcpServers: [],
692
+ });
693
+ this.sessionId = result?.sessionId || null;
694
+ this.currentStatus = 'idle';
695
+ this.messages = [];
696
+
697
+ // DEBUG: session/new response key check
698
+ this.log.info(`[${this.type}] session/new result keys: ${result ? Object.keys(result).join(', ') : 'null'}`);
699
+ if (result?.configOptions) this.log.debug(`[${this.type}] configOptions: ${JSON.stringify(result.configOptions).slice(0, 500)}`);
700
+ if (result?.modes) this.log.debug(`[${this.type}] modes: ${JSON.stringify(result.modes).slice(0, 300)}`);
701
+
702
+ // ACP configOptions parsing (model, thought_level etc)
703
+ this.parseConfigOptions(result?.configOptions);
704
+
705
+ // ACP modes parsing
706
+ this.parseModes(result?.modes);
707
+
708
+ // Legacy: models.currentModelId (some agent compat)
709
+ if (!this.currentModel && result?.models?.currentModelId) {
710
+ this.currentModel = result.models.currentModelId;
711
+ }
712
+
713
+ // ─── Static config fallback (for agents without config/* support) ───
714
+ if (this.configOptions.length === 0 && this.provider.staticConfigOptions?.length) {
715
+ this.useStaticConfig = true;
716
+ for (const sc of this.provider.staticConfigOptions) {
717
+ const defaultVal = this.selectedConfig[sc.configId] || sc.defaultValue || sc.options[0]?.value;
718
+ this.configOptions.push({
719
+ category: sc.category,
720
+ configId: sc.configId,
721
+ currentValue: defaultVal,
722
+ options: sc.options.map(o => ({ ...o })),
723
+ });
724
+ if (defaultVal) {
725
+ this.selectedConfig[sc.configId] = defaultVal;
726
+ if (sc.category === 'model') this.currentModel = defaultVal;
727
+ if (sc.category === 'mode') this.currentMode = defaultVal;
728
+ }
729
+ }
730
+ this.log.info(`[${this.type}] Using static configOptions (${this.configOptions.length} options)`);
731
+ }
732
+
733
+ this.log.info(`[${this.type}] Session created: ${this.sessionId}${this.currentModel ? ` (model: ${this.currentModel})` : ''}${this.currentMode ? ` (mode: ${this.currentMode})` : ''}`);
734
+ if (this.configOptions.length > 0) {
735
+ this.log.info(`[${this.type}] Config options: ${this.configOptions.map(c => `${c.category}(${c.options.length})`).join(', ')}`);
736
+ }
737
+ } catch (e: any) {
738
+ this.log.warn(`[${this.type}] session/new failed: ${e?.message}`);
739
+ this.currentStatus = 'idle';
740
+ }
741
+ }
742
+
743
+ async sendPrompt(text: string, contentBlocks?: ContentBlock[]): Promise<void> {
744
+ if (!this.connection || !this.sessionId) {
745
+ this.log.warn(`[${this.type}] Cannot send prompt: no active connection/session`);
746
+ return;
747
+ }
748
+
749
+ // Build prompt content
750
+ let promptParts: any[];
751
+ if (contentBlocks && contentBlocks.length > 0) {
752
+ // Rich content — forward ContentBlock[] as ACP prompt parts
753
+ promptParts = contentBlocks.map(b => {
754
+ if (b.type === 'text') return { type: 'text', text: (b as any).text };
755
+ if (b.type === 'image') return { type: 'image', data: (b as any).data, mimeType: (b as any).mimeType };
756
+ if (b.type === 'resource_link') return { type: 'resource_link', uri: (b as any).uri, name: (b as any).name };
757
+ if (b.type === 'resource') return { type: 'resource', resource: (b as any).resource };
758
+ return { type: 'text', text: flattenContent([b]) };
759
+ });
760
+ } else {
761
+ promptParts = [{ type: 'text', text }];
762
+ }
763
+
764
+ // Add user message locally (store as ContentBlock[])
765
+ this.messages.push({
766
+ role: 'user',
767
+ content: contentBlocks && contentBlocks.length > 0 ? contentBlocks : text,
768
+ timestamp: Date.now(),
769
+ });
770
+
771
+ this.currentStatus = 'generating';
772
+ this.partialContent = '';
773
+ this.partialBlocks = [];
774
+ this.turnToolCalls = [];
775
+ this.detectStatusTransition();
776
+ this.log.info(`[${this.type}] Sending prompt: "${text.slice(0, 100)}" (${promptParts.length} parts)`);
777
+
778
+ try {
779
+ const result = await this.connection.prompt({
780
+ sessionId: this.sessionId,
781
+ prompt: promptParts,
782
+ });
783
+
784
+ // Prompt complete → reflect final message
785
+ if (result?.stopReason) {
786
+ this.stopReason = result.stopReason;
787
+ }
788
+ this.log.info(`[${this.type}] Prompt completed: stopReason=${result?.stopReason} partialContent=${this.partialContent.length} chars partialBlocks=${this.partialBlocks.length}`);
789
+
790
+ // Build final assistant message with rich content
791
+ this.finalizeAssistantMessage();
792
+
793
+ this.currentStatus = 'idle';
794
+ this.detectStatusTransition();
795
+ } catch (e: any) {
796
+ this.log.warn(`[${this.type}] prompt error: ${e?.message}`);
797
+ this.finalizeAssistantMessage();
798
+ this.currentStatus = 'idle';
799
+ this.detectStatusTransition();
800
+ }
801
+ }
802
+
803
+ private async cancelSession(): Promise<void> {
804
+ if (!this.connection || !this.sessionId) return;
805
+
806
+ await this.connection.cancel({
807
+ sessionId: this.sessionId,
808
+ });
809
+ this.currentStatus = 'idle';
810
+ this.detectStatusTransition();
811
+ }
812
+
813
+ private permissionResolvers: ((approved: boolean) => void)[] = [];
814
+
815
+ private async resolvePermission(approved: boolean): Promise<void> {
816
+ const resolver = this.permissionResolvers.shift();
817
+ if (resolver) {
818
+ resolver(approved);
819
+ }
820
+ if (this.currentStatus === 'waiting_approval') {
821
+ this.currentStatus = 'generating';
822
+ this.detectStatusTransition();
823
+ }
824
+ }
825
+
826
+ // ─── ACP session/update handle ─────────────────────
827
+
828
+ private handleSessionUpdate(params: SessionNotification): void {
829
+ if (!params) return;
830
+
831
+ const update = params.update as SessionUpdate & Record<string, any>;
832
+ this.log.debug(`[${this.type}] sessionUpdate: ${update.sessionUpdate} | keys=${Object.keys(update).join(',')}`);
833
+
834
+ switch (update.sessionUpdate) {
835
+ case 'agent_message_chunk': {
836
+ const content = update.content;
837
+ if (content?.type === 'text' && (content as any).text) {
838
+ this.partialContent += (content as any).text;
839
+ } else if (content?.type === 'image') {
840
+ // Collect image block
841
+ this.partialBlocks.push({
842
+ type: 'image',
843
+ data: (content as any).data || '',
844
+ mimeType: (content as any).mimeType || 'image/png',
845
+ uri: (content as any).uri,
846
+ });
847
+ } else if (content?.type === 'resource_link') {
848
+ this.partialBlocks.push({
849
+ type: 'resource_link',
850
+ uri: (content as any).uri || '',
851
+ name: (content as any).name || 'resource',
852
+ title: (content as any).title,
853
+ mimeType: (content as any).mimeType,
854
+ size: (content as any).size,
855
+ });
856
+ } else if (content?.type === 'resource') {
857
+ this.partialBlocks.push({
858
+ type: 'resource',
859
+ resource: (content as any).resource,
860
+ });
861
+ }
862
+ this.currentStatus = 'generating';
863
+ break;
864
+ }
865
+ case 'agent_thought_chunk':
866
+ case 'user_message_chunk': {
867
+ // Track but don't display thought chunks as main content
868
+ break;
869
+ }
870
+ case 'tool_call': {
871
+ // New tool call — collect as ToolCallInfo
872
+ const tcId = (update as any).toolCallId || `tc_${Date.now()}`;
873
+ const tcTitle = (update as any).title || 'unknown';
874
+ const tcKind = (update as any).kind as ToolKind | undefined;
875
+ const tcStatus = this.mapToolCallStatus((update as any).status);
876
+
877
+ this.activeToolCalls.push({
878
+ id: tcId,
879
+ name: tcTitle,
880
+ status: tcStatus,
881
+ input: (update as any).rawInput ? (typeof (update as any).rawInput === 'string' ? (update as any).rawInput : JSON.stringify((update as any).rawInput)) : undefined,
882
+ });
883
+
884
+ // Also collect as ToolCallInfo for rich content
885
+ const acpStatus = (update as any).status as string || 'in_progress';
886
+ this.turnToolCalls.push({
887
+ toolCallId: tcId,
888
+ title: tcTitle,
889
+ kind: tcKind,
890
+ status: acpStatus as TCS,
891
+ rawInput: (update as any).rawInput,
892
+ content: this.convertToolCallContent((update as any).content),
893
+ locations: (update as any).locations,
894
+ });
895
+ break;
896
+ }
897
+ case 'tool_call_update': {
898
+ // Update existing tool call
899
+ const toolCallId = (update as any).toolCallId;
900
+ const existing = this.activeToolCalls.find(t => t.id === toolCallId);
901
+ if (existing) {
902
+ if ((update as any).status) existing.status = this.mapToolCallStatus((update as any).status);
903
+ if ((update as any).rawOutput) existing.output = typeof (update as any).rawOutput === 'string' ? (update as any).rawOutput : JSON.stringify((update as any).rawOutput);
904
+ }
905
+ // Update ToolCallInfo too
906
+ const tcInfo = this.turnToolCalls.find(t => t.toolCallId === toolCallId);
907
+ if (tcInfo) {
908
+ if ((update as any).status) tcInfo.status = (update as any).status as TCS;
909
+ if ((update as any).rawOutput) tcInfo.rawOutput = (update as any).rawOutput;
910
+ if ((update as any).content) tcInfo.content = this.convertToolCallContent((update as any).content);
911
+ if ((update as any).locations) tcInfo.locations = (update as any).locations;
912
+ }
913
+ break;
914
+ }
915
+ case 'current_mode_update': {
916
+ this.currentMode = (update as any).currentModeId;
917
+ break;
918
+ }
919
+ case 'config_option_update': {
920
+ if ((update as any).configOptions) {
921
+ this.parseConfigOptions((update as any).configOptions);
922
+ }
923
+ break;
924
+ }
925
+ case 'plan':
926
+ case 'available_commands_update':
927
+ case 'session_info_update':
928
+ case 'usage_update':
929
+ // Noted but no specific handling needed
930
+ break;
931
+ default:
932
+ // Unknown update type — try legacy parsing for backward compatibility
933
+ this.handleLegacyUpdate(update as any);
934
+ break;
935
+ }
936
+ }
937
+
938
+ /** Handle legacy session/update formats (pre-standardization compat) */
939
+ private handleLegacyUpdate(params: any): void {
940
+ // Legacy: messageDelta format
941
+ if (params.messageDelta) {
942
+ const delta = params.messageDelta;
943
+ if (delta.content) {
944
+ for (const part of Array.isArray(delta.content) ? delta.content : [delta.content]) {
945
+ if (part.type === 'text' && part.text) {
946
+ this.partialContent += part.text;
947
+ }
948
+ }
949
+ }
950
+ this.currentStatus = 'generating';
951
+ }
952
+
953
+ // Legacy: message complete
954
+ if (params.message) {
955
+ const m = params.message;
956
+ let content = '';
957
+ if (typeof m.content === 'string') {
958
+ content = m.content;
959
+ } else if (Array.isArray(m.content)) {
960
+ content = m.content
961
+ .filter((p: any) => p.type === 'text')
962
+ .map((p: any) => p.text || '')
963
+ .join('\n');
964
+ }
965
+
966
+ if (content.trim()) {
967
+ this.messages.push({
968
+ role: m.role || 'assistant',
969
+ content: content.trim(),
970
+ timestamp: Date.now(),
971
+ });
972
+ this.partialContent = '';
973
+ }
974
+ }
975
+
976
+ // Legacy: toolCallUpdate
977
+ if (params.toolCallUpdate) {
978
+ const tc = params.toolCallUpdate;
979
+ const existing = this.activeToolCalls.find(t => t.id === tc.id);
980
+ if (existing) {
981
+ if (tc.status) existing.status = tc.status;
982
+ if (tc.output) existing.output = tc.output;
983
+ } else {
984
+ this.activeToolCalls.push({
985
+ id: tc.id || `tc_${Date.now()}`,
986
+ name: tc.name || 'unknown',
987
+ status: tc.status || 'running',
988
+ input: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input),
989
+ });
990
+ }
991
+ }
992
+
993
+ // Legacy: stopReason
994
+ if (params.stopReason) {
995
+ this.stopReason = params.stopReason;
996
+ if (params.stopReason !== 'cancelled') {
997
+ this.currentStatus = 'idle';
998
+ }
999
+ this.activeToolCalls = [];
1000
+ this.detectStatusTransition();
1001
+ }
1002
+
1003
+ // Legacy: model info
1004
+ if (params.model) {
1005
+ this.currentModel = params.model;
1006
+ }
1007
+ }
1008
+
1009
+ /** Map SDK ToolCallStatus to internal status */
1010
+ private mapToolCallStatus(status?: ToolCallStatus | string): 'running' | 'completed' | 'failed' {
1011
+ switch (status) {
1012
+ case 'completed': return 'completed';
1013
+ case 'failed': return 'failed';
1014
+ case 'pending':
1015
+ case 'in_progress':
1016
+ default: return 'running';
1017
+ }
1018
+ }
1019
+
1020
+ // ─── Rich Content Helpers ────────────────────────────
1021
+
1022
+ /** Truncate content for transport (text: 2000 chars, images preserved) */
1023
+ private truncateContent(content: string | ContentBlock[]): string | ContentBlock[] {
1024
+ if (typeof content === 'string') {
1025
+ return content.length > 2000 ? content.slice(0, 2000) + '\n... (truncated)' : content;
1026
+ }
1027
+ return content.map(b => {
1028
+ if (b.type === 'text' && b.text.length > 2000) {
1029
+ return { ...b, text: b.text.slice(0, 2000) + '\n... (truncated)' };
1030
+ }
1031
+ return b;
1032
+ });
1033
+ }
1034
+
1035
+ /** Build ContentBlock[] from current partial state */
1036
+ private buildPartialBlocks(): ContentBlock[] {
1037
+ const blocks: ContentBlock[] = [];
1038
+ if (this.partialContent.trim()) {
1039
+ blocks.push({ type: 'text', text: this.partialContent.trim() + '...' });
1040
+ }
1041
+ blocks.push(...this.partialBlocks);
1042
+ return blocks;
1043
+ }
1044
+
1045
+ /** Finalize streaming content into an assistant message */
1046
+ private finalizeAssistantMessage(): void {
1047
+ const blocks = this.buildPartialBlocks();
1048
+ // Remove trailing '...' from text blocks for final message
1049
+ const finalBlocks = blocks.map(b => {
1050
+ if (b.type === 'text' && b.text.endsWith('...')) {
1051
+ return { ...b, text: b.text.slice(0, -3) };
1052
+ }
1053
+ return b;
1054
+ }).filter(b => b.type !== 'text' || (b as any).text.trim());
1055
+
1056
+ if (finalBlocks.length > 0) {
1057
+ this.messages.push({
1058
+ role: 'assistant',
1059
+ content: finalBlocks.length === 1 && finalBlocks[0].type === 'text'
1060
+ ? (finalBlocks[0] as any).text // single text → string (backward compat)
1061
+ : finalBlocks,
1062
+ timestamp: Date.now(),
1063
+ toolCalls: this.turnToolCalls.length > 0 ? [...this.turnToolCalls] : undefined,
1064
+ });
1065
+ }
1066
+ this.partialContent = '';
1067
+ this.partialBlocks = [];
1068
+ this.turnToolCalls = [];
1069
+ }
1070
+
1071
+ /** Convert ACP ToolCallContent[] to our ToolCallContent[] */
1072
+ private convertToolCallContent(acpContent?: any[]): TCC[] | undefined {
1073
+ if (!acpContent || !Array.isArray(acpContent)) return undefined;
1074
+ return acpContent.map((c: any) => {
1075
+ if (c.type === 'diff') {
1076
+ return { type: 'diff' as const, path: c.path || '', oldText: c.oldText, newText: c.newText || '' };
1077
+ }
1078
+ if (c.type === 'terminal') {
1079
+ return { type: 'terminal' as const, terminalId: c.terminalId || '' };
1080
+ }
1081
+ // type: 'content' or unknown
1082
+ return { type: 'content' as const, content: c.content || { type: 'text' as const, text: JSON.stringify(c) } };
1083
+ });
1084
+ }
1085
+
1086
+ // ─── status transition detect ────────────────────────────
1087
+
1088
+ private detectStatusTransition(): void {
1089
+ const now = Date.now();
1090
+ const newStatus = this.currentStatus;
1091
+ const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
1092
+ const chatTitle = `${this.provider.name} · ${dirName}`;
1093
+
1094
+ if (newStatus !== this.lastStatus) {
1095
+ if (this.lastStatus === 'idle' && newStatus === 'generating') {
1096
+ this.generatingStartedAt = now;
1097
+ this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now });
1098
+ } else if (newStatus === 'waiting_approval') {
1099
+ if (!this.generatingStartedAt) this.generatingStartedAt = now;
1100
+ this.pushEvent({
1101
+ event: 'agent:waiting_approval', chatTitle, timestamp: now,
1102
+ modalMessage: this.activeToolCalls.find(t => t.status === 'running')?.name,
1103
+ });
1104
+ } else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
1105
+ const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
1106
+ this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now });
1107
+ this.generatingStartedAt = 0;
1108
+ } else if (newStatus === 'stopped') {
1109
+ this.pushEvent({ event: 'agent:stopped', chatTitle, timestamp: now });
1110
+ }
1111
+ this.lastStatus = newStatus;
1112
+ }
1113
+
1114
+ // Monitor check
1115
+ const agentKey = `${this.type}:acp`;
1116
+ const monitorEvents = this.monitor.check(agentKey, newStatus, now);
1117
+ for (const me of monitorEvents) {
1118
+ this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
1119
+ }
1120
+ }
1121
+
1122
+ private pushEvent(event: ProviderEvent): void {
1123
+ this.events.push(event);
1124
+ if (this.events.length > 50) this.events = this.events.slice(-50);
1125
+ }
1126
+
1127
+ private flushEvents(): ProviderEvent[] {
1128
+ const events = [...this.events];
1129
+ this.events = [];
1130
+ return events;
1131
+ }
1132
+
1133
+ // ─── external access ─────────────────────────────────
1134
+
1135
+ get cliType(): string { return this.type; }
1136
+ get cliName(): string { return this.provider.name; }
1137
+
1138
+ /** ACP Agent capabilities (available after initialize) */
1139
+ getCapabilities(): Record<string, any> { return this.agentCapabilities; }
1140
+ }