@axhub/genie 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/assets/App-BWSqiXAT.js +220 -0
  2. package/dist/assets/App-DrlLKa8f.css +1 -0
  3. package/dist/assets/ReviewApp-nz3mbArg.js +1 -0
  4. package/dist/assets/{_basePickBy-CFRQvihx.js → _basePickBy-C19AekOu.js} +1 -1
  5. package/dist/assets/{_baseUniq-Dhh8nCvs.js → _baseUniq-JsnevLw_.js} +1 -1
  6. package/dist/assets/{arc-DQ0v3dU4.js → arc-BLpcuBlf.js} +1 -1
  7. package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +36 -0
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-Bbxhj5KC.js → blockDiagram-WCTKOSBZ-DQBLwsUS.js} +3 -3
  9. package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +10 -0
  10. package/dist/assets/channel-DkFNxV_H.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB-DlvtrM0q.js → chunk-4BX2VUAB-De63kbgc.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-DJUSHyTa.js → chunk-55IACEB6-DtTDDdM9.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-C6Ch-htf.js → chunk-FMBD7UC4-DHuwd8tw.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-DzQIht58.js → chunk-JSJVCQXG-BgytFtmO.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-C05jARMH.js → chunk-KX2RTZJC-nZdp86aN.js} +1 -1
  16. package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +220 -0
  17. package/dist/assets/{chunk-QZHKN3VN-jxti9HTX.js → chunk-QZHKN3VN-DvUQ3mnO.js} +1 -1
  18. package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +189 -0
  19. package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +1 -0
  21. package/dist/assets/clone-C0lCEIEO.js +1 -0
  22. package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +1 -0
  23. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  24. package/dist/assets/{dagre-KLK3FWXG-DJ3dNSYk.js → dagre-KLK3FWXG-CHYIvW47.js} +1 -1
  25. package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +24 -0
  26. package/dist/assets/{diagram-IFDJBPK2-Da6K4aP-.js → diagram-IFDJBPK2-Dzsiln_C.js} +1 -1
  27. package/dist/assets/{diagram-P4PSJMXO-vZZKB92A.js → diagram-P4PSJMXO-DKnGbUpE.js} +1 -1
  28. package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +70 -0
  29. package/dist/assets/{flowDiagram-PKNHOUZH-DUV13pHi.js → flowDiagram-PKNHOUZH-BAZ2-jKp.js} +4 -4
  30. package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +292 -0
  31. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BZ5gW69I.js → gitGraphDiagram-K3NZZRJ6-BflpyjGy.js} +1 -1
  32. package/dist/assets/{graph-BbvHswRd.js → graph-suelaXFh.js} +1 -1
  33. package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +1 -0
  34. package/dist/assets/index-B01NxbUv.css +1 -0
  35. package/dist/assets/index-DW5pGgQ_.js +2 -0
  36. package/dist/assets/{infoDiagram-LFFYTUFH-8auUIPKW.js → infoDiagram-LFFYTUFH-pfD1FA3p.js} +1 -1
  37. package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +70 -0
  38. package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +139 -0
  39. package/dist/assets/{kanban-definition-K7BYSVSG-Bappd2YO.js → kanban-definition-K7BYSVSG-FWinmur1.js} +5 -5
  40. package/dist/assets/{layout-BmbfFZKy.js → layout-vcz43XvZ.js} +1 -1
  41. package/dist/assets/{linear-WZnF-PT6.js → linear-le4gc0vx.js} +1 -1
  42. package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +870 -0
  43. package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +68 -0
  44. package/dist/assets/{pieDiagram-SKSYHLDU-uxjlAy1t.js → pieDiagram-SKSYHLDU-C7PKDh3b.js} +2 -2
  45. package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +7 -0
  46. package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +73 -0
  47. package/dist/assets/{sankeyDiagram-WA2Y5GQK-2-FHHM-R.js → sankeyDiagram-WA2Y5GQK-4gulcOP4.js} +3 -3
  48. package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +145 -0
  49. package/dist/assets/{stateDiagram-RAJIS63D-DoW8U53H.js → stateDiagram-RAJIS63D-CB4Vl7qM.js} +1 -1
  50. package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +1 -0
  51. package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +61 -0
  52. package/dist/assets/{treemap-KZPCXAKY-ajdAP-72.js → treemap-KZPCXAKY-DZSEE6Hz.js} +58 -58
  53. package/dist/assets/vendor-codemirror-CyOKkaQZ.js +31 -0
  54. package/dist/assets/vendor-react-CP4yFTs7.js +8 -0
  55. package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
  56. package/dist/assets/{vennDiagram-LZ73GAT5-C9If0AT0.js → vennDiagram-LZ73GAT5-8E_G06fI.js} +4 -4
  57. package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +7 -0
  58. package/dist/favicon.png +0 -0
  59. package/dist/icons/icon-128x128.png +0 -0
  60. package/dist/icons/icon-144x144.png +0 -0
  61. package/dist/icons/icon-152x152.png +0 -0
  62. package/dist/icons/icon-192x192.png +0 -0
  63. package/dist/icons/icon-384x384.png +0 -0
  64. package/dist/icons/icon-512x512.png +0 -0
  65. package/dist/icons/icon-72x72.png +0 -0
  66. package/dist/icons/icon-96x96.png +0 -0
  67. package/dist/index.html +4 -5
  68. package/dist/logo-128.png +0 -0
  69. package/dist/logo-256.png +0 -0
  70. package/dist/logo-32.png +0 -0
  71. package/dist/logo-512.png +0 -0
  72. package/dist/logo-64.png +0 -0
  73. package/package.json +2 -1
  74. package/server/_legacy-providers/README.md +30 -0
  75. package/server/_legacy-providers/claude-sdk.js +956 -0
  76. package/server/_legacy-providers/gemini-cli.js +368 -0
  77. package/server/_legacy-providers/openai-codex.js +705 -0
  78. package/server/_legacy-providers/opencode-cli.js +674 -0
  79. package/server/acp-runtime/client.js +1805 -0
  80. package/server/acp-runtime/client.test.js +688 -0
  81. package/server/acp-runtime/index.js +419 -0
  82. package/server/acp-runtime/registry.js +45 -0
  83. package/server/acp-runtime/session-store.js +254 -0
  84. package/server/acp-runtime/session-store.test.js +89 -0
  85. package/server/channels/runtime/AgentRuntimeAdapter.js +21 -70
  86. package/server/claude-sdk.js +24 -944
  87. package/server/cli.js +11 -5
  88. package/server/external-agent/service.js +77 -63
  89. package/server/gemini-cli.js +23 -360
  90. package/server/index.js +54 -46
  91. package/server/openai-codex.js +24 -698
  92. package/server/opencode-cli.js +70 -640
  93. package/server/routes/agent.js +2 -0
  94. package/server/routes/codex.js +5 -5
  95. package/server/routes/git.js +3 -20
  96. package/server/routes/mcp.js +18 -34
  97. package/server/routes/session-core.js +44 -10
  98. package/server/session-core/abortSession.js +2 -18
  99. package/server/session-core/eventStore.js +5 -1
  100. package/server/session-core/providerAdapters.js +98 -10
  101. package/server/session-core/providerDiscovery.js +2 -2
  102. package/server/session-core/runtimeState.js +16 -17
  103. package/server/session-core/runtimeWriter.js +19 -12
  104. package/server/utils/codexPath.js +3 -1
  105. package/server/utils/spawnCommand.js +7 -0
  106. package/shared/conversationEvents.js +347 -10
  107. package/shared/conversationEvents.test.js +403 -0
  108. package/dist/assets/App-BxazfNJn.js +0 -484
  109. package/dist/assets/App-qxJ8_QYu.css +0 -32
  110. package/dist/assets/ReviewApp-CsqTAlGU.js +0 -1
  111. package/dist/assets/architectureDiagram-2XIMDMQ5-DmUHdvQH.js +0 -36
  112. package/dist/assets/c4Diagram-IC4MRINW-BOivDlQU.js +0 -10
  113. package/dist/assets/channel-Cj8xVD0X.js +0 -1
  114. package/dist/assets/chunk-NQ4KR5QH-Ci-n7jfu.js +0 -220
  115. package/dist/assets/chunk-WL4C6EOR-C559Mk71.js +0 -189
  116. package/dist/assets/classDiagram-VBA2DB6C-CI2zklxw.js +0 -1
  117. package/dist/assets/classDiagram-v2-RAHNMMFH-CI2zklxw.js +0 -1
  118. package/dist/assets/clone-BEVqubrI.js +0 -1
  119. package/dist/assets/cose-bilkent-S5V4N54A-DNO9ncXL.js +0 -1
  120. package/dist/assets/cytoscape.esm-2ZfV8NB5.js +0 -331
  121. package/dist/assets/diagram-E7M64L7V-Ba_LGLun.js +0 -24
  122. package/dist/assets/erDiagram-INFDFZHY-Csb8dFdP.js +0 -70
  123. package/dist/assets/ganttDiagram-A5KZAMGK-B5Kv9Wfz.js +0 -292
  124. package/dist/assets/highlighted-body-TPN3WLV5-DZJajMGm.js +0 -1
  125. package/dist/assets/index-BFX9lxRB.css +0 -1
  126. package/dist/assets/index-BiErUGrv.js +0 -2
  127. package/dist/assets/ishikawaDiagram-PHBUUO56-JmsNlo2I.js +0 -70
  128. package/dist/assets/journeyDiagram-4ABVD52K-Cuudv7Vv.js +0 -139
  129. package/dist/assets/mermaid-O7DHMXV3-D-2fQRvw.js +0 -988
  130. package/dist/assets/mindmap-definition-YRQLILUH-BQHnzzud.js +0 -68
  131. package/dist/assets/quadrantDiagram-337W2JSQ-DpwZU-f_.js +0 -7
  132. package/dist/assets/requirementDiagram-Z7DCOOCP-C_9ClOWm.js +0 -73
  133. package/dist/assets/sequenceDiagram-2WXFIKYE-egns-0XI.js +0 -145
  134. package/dist/assets/stateDiagram-v2-FVOUBMTO-BoFZZ4Ds.js +0 -1
  135. package/dist/assets/timeline-definition-YZTLITO2-chPa8ppH.js +0 -61
  136. package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
  137. package/dist/assets/vendor-react-Cpt6D04s.js +0 -59
  138. package/dist/assets/vendor-xterm-DfaPXD3y.js +0 -66
  139. package/dist/assets/xychartDiagram-JWTSCODW-DD42U6Or.js +0 -7
@@ -0,0 +1,1805 @@
1
+ import fs from 'fs/promises';
2
+ import { randomUUID } from 'crypto';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { spawn } from 'child_process';
6
+ import { Readable, Writable } from 'stream';
7
+
8
+ import {
9
+ ClientSideConnection,
10
+ PROTOCOL_VERSION,
11
+ ndJsonStream
12
+ } from '@agentclientprotocol/sdk';
13
+
14
+ import {
15
+ CONVERSATION_EVENT_KINDS,
16
+ createConversationEvent
17
+ } from '../../shared/conversationEvents.js';
18
+ import { parseDataUrl } from '../utils/agentImages.js';
19
+ import { resolveAgentCommand } from './registry.js';
20
+
21
+ const DEFAULT_TERMINAL_OUTPUT_LIMIT = 256 * 1024;
22
+ const DEFAULT_CLOSE_GRACE_MS = 1000;
23
+ const DEFAULT_TERMINAL_MAX_LIFETIME_MS = parseInt(process.env.ACP_TERMINAL_MAX_LIFETIME_MS, 10) || (5 * 60 * 1000);
24
+ const FILTERED_CHILD_PROCESS_ENV_KEYS = new Set(['NODE_OPTIONS']);
25
+
26
+ const pendingPermissionRequests = new Map();
27
+
28
+ function getDisabledClaudeAcpMcpServers() {
29
+ const configured = typeof process.env.CLAUDE_SDK_DISABLED_MCP_SERVERS === 'string'
30
+ ? process.env.CLAUDE_SDK_DISABLED_MCP_SERVERS
31
+ : 'pencil';
32
+
33
+ return new Set(
34
+ configured
35
+ .split(',')
36
+ .map((value) => value.trim())
37
+ .filter(Boolean)
38
+ );
39
+ }
40
+
41
+ function cloneJsonValue(value) {
42
+ return JSON.parse(JSON.stringify(value));
43
+ }
44
+
45
+ function filterClaudeMcpServers(config, disabledServers) {
46
+ if (!config || typeof config !== 'object' || Array.isArray(config) || disabledServers.size === 0) {
47
+ return { changed: false, config };
48
+ }
49
+
50
+ const nextConfig = cloneJsonValue(config);
51
+ let changed = false;
52
+
53
+ if (nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object' && !Array.isArray(nextConfig.mcpServers)) {
54
+ for (const serverName of disabledServers) {
55
+ if (Object.prototype.hasOwnProperty.call(nextConfig.mcpServers, serverName)) {
56
+ delete nextConfig.mcpServers[serverName];
57
+ changed = true;
58
+ }
59
+ }
60
+ }
61
+
62
+ if (nextConfig.projects && typeof nextConfig.projects === 'object' && !Array.isArray(nextConfig.projects)) {
63
+ for (const projectState of Object.values(nextConfig.projects)) {
64
+ if (!projectState || typeof projectState !== 'object' || Array.isArray(projectState)) {
65
+ continue;
66
+ }
67
+
68
+ if (projectState.mcpServers && typeof projectState.mcpServers === 'object' && !Array.isArray(projectState.mcpServers)) {
69
+ for (const serverName of disabledServers) {
70
+ if (Object.prototype.hasOwnProperty.call(projectState.mcpServers, serverName)) {
71
+ delete projectState.mcpServers[serverName];
72
+ changed = true;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ return {
80
+ changed,
81
+ config: nextConfig
82
+ };
83
+ }
84
+
85
+ async function prepareClaudeSpawnEnvironment() {
86
+ const disabledServers = getDisabledClaudeAcpMcpServers();
87
+ if (disabledServers.size === 0) {
88
+ return { env: sanitizeProcessEnv(process.env), cleanup: null };
89
+ }
90
+
91
+ const homeDir = os.homedir();
92
+ const claudeConfigPath = path.join(homeDir, '.claude.json');
93
+ let rawConfig;
94
+
95
+ try {
96
+ rawConfig = JSON.parse(await fs.readFile(claudeConfigPath, 'utf8'));
97
+ } catch {
98
+ return { env: sanitizeProcessEnv(process.env), cleanup: null };
99
+ }
100
+
101
+ const { changed, config } = filterClaudeMcpServers(rawConfig, disabledServers);
102
+ if (!changed) {
103
+ return { env: sanitizeProcessEnv(process.env), cleanup: null };
104
+ }
105
+
106
+ const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), 'axhub-genie-claude-home-'));
107
+ const sourceClaudeDir = path.join(homeDir, '.claude');
108
+ const targetClaudeDir = path.join(tempHome, '.claude');
109
+
110
+ try {
111
+ await fs.symlink(sourceClaudeDir, targetClaudeDir, process.platform === 'win32' ? 'junction' : 'dir');
112
+ } catch {
113
+ // Claude can still use the temporary config even if the data directory symlink is unavailable.
114
+ }
115
+
116
+ await fs.writeFile(
117
+ path.join(tempHome, '.claude.json'),
118
+ `${JSON.stringify(config, null, 2)}\n`,
119
+ 'utf8'
120
+ );
121
+
122
+ return {
123
+ env: sanitizeProcessEnv(process.env, {
124
+ HOME: tempHome,
125
+ USERPROFILE: tempHome
126
+ }),
127
+ cleanup: async () => {
128
+ await fs.rm(tempHome, { recursive: true, force: true });
129
+ }
130
+ };
131
+ }
132
+
133
+ async function prepareSpawnEnvironment(agentKey) {
134
+ if (String(agentKey || '').trim().toLowerCase() === 'claude') {
135
+ return prepareClaudeSpawnEnvironment();
136
+ }
137
+
138
+ return { env: sanitizeProcessEnv(process.env), cleanup: null };
139
+ }
140
+
141
+ function nowIso() {
142
+ return new Date().toISOString();
143
+ }
144
+
145
+ function normalizeText(value) {
146
+ return typeof value === 'string' ? value : String(value ?? '');
147
+ }
148
+
149
+ function normalizeComparableValue(value) {
150
+ try {
151
+ return JSON.stringify(value ?? null);
152
+ } catch {
153
+ return String(value ?? '');
154
+ }
155
+ }
156
+
157
+ function sanitizeProcessEnv(baseEnv = process.env, overrides = {}) {
158
+ const sanitized = {};
159
+
160
+ Object.entries(baseEnv || {}).forEach(([key, value]) => {
161
+ if (!key || value == null || FILTERED_CHILD_PROCESS_ENV_KEYS.has(key)) {
162
+ return;
163
+ }
164
+ sanitized[key] = String(value);
165
+ });
166
+
167
+ Object.entries(overrides || {}).forEach(([key, value]) => {
168
+ if (!key || value == null) {
169
+ return;
170
+ }
171
+ sanitized[key] = String(value);
172
+ });
173
+
174
+ return sanitized;
175
+ }
176
+
177
+ function normalizeTerminalCommandForSpawn(command, args = []) {
178
+ const normalizedArgs = Array.isArray(args) ? [...args] : [];
179
+ const normalizedCommand = typeof command === 'string' ? command : String(command || '');
180
+ const commandName = path.basename(normalizedCommand).toLowerCase();
181
+
182
+ if (
183
+ commandName === 'zsh'
184
+ && normalizedArgs[0] === '-lc'
185
+ && typeof normalizedArgs[1] === 'string'
186
+ && !normalizedArgs[1].includes('setopt no_nomatch')
187
+ ) {
188
+ normalizedArgs[1] = `setopt no_nomatch; ${normalizedArgs[1]}`;
189
+ }
190
+
191
+ return {
192
+ command: normalizedCommand,
193
+ args: normalizedArgs
194
+ };
195
+ }
196
+
197
+ function detectFatalAgentDiagnostic(agentKey, text) {
198
+ const normalizedAgent = String(agentKey || '').trim().toLowerCase();
199
+ const normalizedText = String(text || '').trim();
200
+
201
+ if (!normalizedText) {
202
+ return null;
203
+ }
204
+
205
+ if (normalizedAgent === 'gemini') {
206
+ const lowerText = normalizedText.toLowerCase();
207
+ const isRateLimited = (
208
+ lowerText.includes('status 429') ||
209
+ lowerText.includes('ratelimitexceeded') ||
210
+ lowerText.includes('resource_exhausted') ||
211
+ lowerText.includes('resource has been exhausted')
212
+ );
213
+
214
+ if (isRateLimited) {
215
+ return new Error('Gemini request failed because the logged-in account is currently rate limited (429 RESOURCE_EXHAUSTED). Please wait and try again later.');
216
+ }
217
+ }
218
+
219
+ return null;
220
+ }
221
+
222
+ function createEventMessageId(prefix, provider, sessionId, turnId = null) {
223
+ return `${prefix}:${provider}:${sessionId || 'pending'}:${turnId || randomUUID()}`;
224
+ }
225
+
226
+ function pickPermissionOption(options = [], kinds = []) {
227
+ for (const kind of kinds) {
228
+ const match = options.find((option) => option?.kind === kind);
229
+ if (match) {
230
+ return match;
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+
236
+ function createSelectedPermissionResponse(optionId) {
237
+ return {
238
+ outcome: {
239
+ outcome: 'selected',
240
+ optionId
241
+ }
242
+ };
243
+ }
244
+
245
+ function createCancelledPermissionResponse() {
246
+ return {
247
+ outcome: {
248
+ outcome: 'cancelled'
249
+ }
250
+ };
251
+ }
252
+
253
+ function inferToolKind(toolCall = {}) {
254
+ if (toolCall.kind) {
255
+ return toolCall.kind;
256
+ }
257
+
258
+ const title = String(toolCall.title || '').trim().toLowerCase();
259
+ if (!title) {
260
+ return 'other';
261
+ }
262
+
263
+ if (title.includes('read') || title.includes('cat')) return 'read';
264
+ if (title.includes('search') || title.includes('find') || title.includes('grep')) return 'search';
265
+ if (title.includes('write') || title.includes('edit') || title.includes('patch')) return 'edit';
266
+ if (title.includes('delete') || title.includes('remove')) return 'delete';
267
+ if (title.includes('move') || title.includes('rename')) return 'move';
268
+ if (title.includes('run') || title.includes('bash') || title.includes('shell') || title.includes('exec')) return 'execute';
269
+ if (title.includes('fetch') || title.includes('http') || title.includes('url')) return 'fetch';
270
+ if (title.includes('think')) return 'think';
271
+
272
+ return 'other';
273
+ }
274
+
275
+ function classifyPermissionStatus(response, request) {
276
+ if (!response || response.outcome?.outcome !== 'selected') {
277
+ return 'cancelled';
278
+ }
279
+
280
+ const selected = request?.options?.find((option) => option.optionId === response.outcome.optionId);
281
+ if (!selected) {
282
+ return 'cancelled';
283
+ }
284
+
285
+ if (selected.kind === 'allow_once' || selected.kind === 'allow_always') {
286
+ return 'approved';
287
+ }
288
+
289
+ return 'denied';
290
+ }
291
+
292
+ function choosePermissionResponse(request, decision = {}) {
293
+ const options = Array.isArray(request?.options) ? request.options : [];
294
+ if (options.length === 0) {
295
+ return createCancelledPermissionResponse();
296
+ }
297
+
298
+ const explicitOutcome = decision?.outcome;
299
+ const explicitOptionId = explicitOutcome?.outcome === 'selected'
300
+ ? explicitOutcome.optionId
301
+ : decision?.optionId;
302
+
303
+ if (explicitOutcome?.outcome === 'cancelled') {
304
+ return createCancelledPermissionResponse();
305
+ }
306
+
307
+ if (explicitOptionId) {
308
+ return options.some((option) => option?.optionId === explicitOptionId)
309
+ ? createSelectedPermissionResponse(explicitOptionId)
310
+ : createCancelledPermissionResponse();
311
+ }
312
+
313
+ if (decision?.cancelled) {
314
+ return createCancelledPermissionResponse();
315
+ }
316
+
317
+ if (decision?.allow) {
318
+ const match = pickPermissionOption(
319
+ options,
320
+ decision?.rememberEntry ? ['allow_always', 'allow_once'] : ['allow_once', 'allow_always']
321
+ );
322
+ return match ? createSelectedPermissionResponse(match.optionId) : createCancelledPermissionResponse();
323
+ }
324
+
325
+ const rejectMatch = pickPermissionOption(
326
+ options,
327
+ decision?.rememberEntry ? ['reject_always', 'reject_once'] : ['reject_once', 'reject_always']
328
+ );
329
+
330
+ return rejectMatch ? createSelectedPermissionResponse(rejectMatch.optionId) : createCancelledPermissionResponse();
331
+ }
332
+
333
+ function readLines(content, line = null, limit = null) {
334
+ if (line == null && limit == null) {
335
+ return content;
336
+ }
337
+
338
+ const lines = String(content || '').split(/\r?\n/);
339
+ const startIndex = Math.max(0, Number(line || 1) - 1);
340
+ const endIndex = limit == null ? lines.length : startIndex + Math.max(0, Number(limit) || 0);
341
+ return lines.slice(startIndex, endIndex).join('\n');
342
+ }
343
+
344
+ function toContentBlockText(content) {
345
+ if (!content || typeof content !== 'object') {
346
+ return '';
347
+ }
348
+
349
+ if (content.type === 'text') {
350
+ return String(content.text || '');
351
+ }
352
+
353
+ if (content.type === 'image') {
354
+ return `[image:${content.mimeType || 'unknown'}]`;
355
+ }
356
+
357
+ if (content.type === 'resource_link') {
358
+ return String(content.uri || content.name || '');
359
+ }
360
+
361
+ if (content.type === 'resource') {
362
+ const resource = content.resource || {};
363
+ if (typeof resource.text === 'string') {
364
+ return resource.text;
365
+ }
366
+ }
367
+
368
+ if (content.type === 'audio') {
369
+ return `[audio:${content.mimeType || 'unknown'}]`;
370
+ }
371
+
372
+ try {
373
+ return JSON.stringify(content);
374
+ } catch {
375
+ return String(content);
376
+ }
377
+ }
378
+
379
+ function toToolContentText(toolCall = {}) {
380
+ if (toolCall.rawOutput != null) {
381
+ return typeof toolCall.rawOutput === 'string'
382
+ ? toolCall.rawOutput
383
+ : normalizeComparableValue(toolCall.rawOutput);
384
+ }
385
+
386
+ if (!Array.isArray(toolCall.content) || toolCall.content.length === 0) {
387
+ return '';
388
+ }
389
+
390
+ return toolCall.content
391
+ .map((item) => {
392
+ if (!item || typeof item !== 'object') {
393
+ return '';
394
+ }
395
+
396
+ if (item.type === 'content') {
397
+ return toContentBlockText(item.content);
398
+ }
399
+
400
+ if (item.type === 'diff') {
401
+ return `Diff ${item.path || ''}\n${item.newText || ''}`.trim();
402
+ }
403
+
404
+ if (item.type === 'terminal') {
405
+ return `Terminal ${item.terminalId || ''}`.trim();
406
+ }
407
+
408
+ return normalizeComparableValue(item);
409
+ })
410
+ .filter(Boolean)
411
+ .join('\n\n');
412
+ }
413
+
414
+ function createPermissionMessage(toolCall = {}) {
415
+ const title = String(toolCall.title || 'tool execution').trim();
416
+ return `Approval required for ${title}`;
417
+ }
418
+
419
+ function buildToolCallConversationPayload(update = {}, toolName = 'Tool') {
420
+ const payload = {
421
+ toolCallId: update.toolCallId,
422
+ toolName
423
+ };
424
+
425
+ if (update.title != null) {
426
+ payload.title = String(update.title);
427
+ }
428
+
429
+ if (update.kind != null) {
430
+ payload.kind = String(update.kind);
431
+ }
432
+
433
+ if (update.status != null) {
434
+ payload.status = String(update.status);
435
+ }
436
+
437
+ if (update.rawInput !== undefined) {
438
+ payload.rawInput = cloneJsonValue(update.rawInput);
439
+ }
440
+
441
+ if (update.rawOutput !== undefined) {
442
+ payload.rawOutput = cloneJsonValue(update.rawOutput);
443
+ }
444
+
445
+ if (Array.isArray(update.content)) {
446
+ payload.contentBlocks = cloneJsonValue(update.content);
447
+ }
448
+
449
+ if (Array.isArray(update.locations)) {
450
+ payload.locations = cloneJsonValue(update.locations);
451
+ }
452
+
453
+ return payload;
454
+ }
455
+
456
+ function normalizeSessionModeState(modes) {
457
+ if (!modes || typeof modes !== 'object') {
458
+ return null;
459
+ }
460
+
461
+ const availableModes = Array.isArray(modes.availableModes)
462
+ ? modes.availableModes
463
+ .map((mode) => {
464
+ if (!mode || typeof mode !== 'object') {
465
+ return null;
466
+ }
467
+
468
+ const id = String(mode.id || '').trim();
469
+ if (!id) {
470
+ return null;
471
+ }
472
+
473
+ return {
474
+ id,
475
+ name: String(mode.name || id),
476
+ description: mode.description == null ? null : String(mode.description)
477
+ };
478
+ })
479
+ .filter(Boolean)
480
+ : [];
481
+
482
+ const currentModeId = String(modes.currentModeId || '').trim();
483
+
484
+ if (!currentModeId && availableModes.length === 0) {
485
+ return null;
486
+ }
487
+
488
+ return {
489
+ availableModes,
490
+ currentModeId: currentModeId || null
491
+ };
492
+ }
493
+
494
+ function normalizeAvailableCommandsUpdate(update = {}) {
495
+ const availableCommands = Array.isArray(update.availableCommands)
496
+ ? update.availableCommands
497
+ .map((command) => {
498
+ if (!command || typeof command !== 'object') {
499
+ return null;
500
+ }
501
+
502
+ const name = String(command.name || '').trim();
503
+ if (!name) {
504
+ return null;
505
+ }
506
+
507
+ return {
508
+ name,
509
+ description: command.description == null ? '' : String(command.description),
510
+ input: command.input ? cloneJsonValue(command.input) : null
511
+ };
512
+ })
513
+ .filter(Boolean)
514
+ : [];
515
+
516
+ return {
517
+ availableCommands
518
+ };
519
+ }
520
+
521
+ function normalizePromptResourceLinks(resourceLinks = []) {
522
+ return Array.isArray(resourceLinks)
523
+ ? resourceLinks
524
+ .map((entry) => {
525
+ if (typeof entry === 'string') {
526
+ const uri = entry.trim();
527
+ if (!uri) return null;
528
+ return {
529
+ type: 'resource_link',
530
+ uri,
531
+ name: path.basename(uri) || uri
532
+ };
533
+ }
534
+
535
+ if (!entry || typeof entry !== 'object') {
536
+ return null;
537
+ }
538
+
539
+ const uri = String(entry.uri || entry.path || entry.url || '').trim();
540
+ if (!uri) {
541
+ return null;
542
+ }
543
+
544
+ return {
545
+ type: 'resource_link',
546
+ uri,
547
+ name: entry.name == null ? (path.basename(uri) || uri) : String(entry.name)
548
+ };
549
+ })
550
+ .filter(Boolean)
551
+ : [];
552
+ }
553
+
554
+ function buildConversationContentBlocks(images = [], resourceLinks = []) {
555
+ const blocks = [];
556
+
557
+ for (const image of images || []) {
558
+ if (!image?.data) {
559
+ continue;
560
+ }
561
+
562
+ blocks.push({
563
+ type: 'image',
564
+ data: image.data,
565
+ mimeType: image.mimeType || 'application/octet-stream',
566
+ name: image.name || null
567
+ });
568
+ }
569
+
570
+ blocks.push(...normalizePromptResourceLinks(resourceLinks));
571
+ return blocks;
572
+ }
573
+
574
+ function normalizeAssistantContentBlock(content = {}) {
575
+ if (!content || typeof content !== 'object' || !content.type) {
576
+ return null;
577
+ }
578
+
579
+ if (content.type === 'text') {
580
+ return null;
581
+ }
582
+
583
+ if (content.type === 'resource' && content.resource && typeof content.resource === 'object') {
584
+ return {
585
+ type: 'resource',
586
+ resource: cloneJsonValue(content.resource)
587
+ };
588
+ }
589
+
590
+ return cloneJsonValue(content);
591
+ }
592
+
593
+ export function splitCommandLine(value) {
594
+ const source = String(value || '').trim();
595
+ if (!source) {
596
+ throw new Error('ACP agent command cannot be empty');
597
+ }
598
+
599
+ const parts = [];
600
+ let current = '';
601
+ let quote = null;
602
+ let escaping = false;
603
+
604
+ for (const character of source) {
605
+ if (escaping) {
606
+ current += character;
607
+ escaping = false;
608
+ continue;
609
+ }
610
+
611
+ if (character === '\\' && quote !== '\'') {
612
+ escaping = true;
613
+ continue;
614
+ }
615
+
616
+ if (quote) {
617
+ if (character === quote) {
618
+ quote = null;
619
+ } else {
620
+ current += character;
621
+ }
622
+ continue;
623
+ }
624
+
625
+ if (character === '\'' || character === '"') {
626
+ quote = character;
627
+ continue;
628
+ }
629
+
630
+ if (/\s/.test(character)) {
631
+ if (current) {
632
+ parts.push(current);
633
+ current = '';
634
+ }
635
+ continue;
636
+ }
637
+
638
+ current += character;
639
+ }
640
+
641
+ if (current) {
642
+ parts.push(current);
643
+ }
644
+
645
+ if (parts.length === 0) {
646
+ throw new Error('ACP agent command cannot be empty');
647
+ }
648
+
649
+ return {
650
+ command: parts[0],
651
+ args: parts.slice(1)
652
+ };
653
+ }
654
+
655
+ function compareVersionParts(left, right) {
656
+ for (let index = 0; index < 3; index += 1) {
657
+ const leftValue = Number(left[index] || 0);
658
+ const rightValue = Number(right[index] || 0);
659
+ if (leftValue > rightValue) return 1;
660
+ if (leftValue < rightValue) return -1;
661
+ }
662
+ return 0;
663
+ }
664
+
665
+ async function resolveGeminiCommand(commandLine) {
666
+ const parsed = splitCommandLine(commandLine);
667
+
668
+ if (parsed.command !== 'gemini') {
669
+ return commandLine;
670
+ }
671
+
672
+ try {
673
+ const versionOutput = await new Promise((resolve, reject) => {
674
+ let stdout = '';
675
+ const childProcess = spawn(parsed.command, ['--version'], {
676
+ stdio: ['ignore', 'pipe', 'ignore']
677
+ });
678
+
679
+ childProcess.stdout.on('data', (chunk) => {
680
+ stdout += chunk.toString();
681
+ });
682
+
683
+ childProcess.on('close', () => resolve(stdout.trim()));
684
+ childProcess.on('error', reject);
685
+ });
686
+
687
+ const match = String(versionOutput || '').match(/(\d+)\.(\d+)\.(\d+)/);
688
+ if (!match) {
689
+ return commandLine;
690
+ }
691
+
692
+ const versionParts = [Number(match[1]), Number(match[2]), Number(match[3])];
693
+ if (compareVersionParts(versionParts, [0, 33, 0]) >= 0) {
694
+ return commandLine;
695
+ }
696
+
697
+ const nextArgs = parsed.args.map((argument) => (
698
+ argument === '--acp' ? '--experimental-acp' : argument
699
+ ));
700
+ return [parsed.command, ...nextArgs].join(' ');
701
+ } catch {
702
+ return commandLine;
703
+ }
704
+ }
705
+
706
+ async function resolveLaunchCommand(agentKey, overrides = {}) {
707
+ const commandLine = resolveAgentCommand(agentKey, overrides);
708
+ if (!commandLine) {
709
+ throw new Error(`Unsupported ACP agent: ${agentKey}`);
710
+ }
711
+
712
+ if (String(agentKey || '').trim().toLowerCase() === 'gemini') {
713
+ return resolveGeminiCommand(commandLine);
714
+ }
715
+
716
+ return commandLine;
717
+ }
718
+
719
+ function buildPromptBlocks(promptText, attachments = {}) {
720
+ const normalizedAttachments = Array.isArray(attachments)
721
+ ? { images: attachments }
722
+ : (attachments && typeof attachments === 'object' ? attachments : {});
723
+ const blocks = [];
724
+ const normalizedPromptText = String(promptText || '').trim() || 'Continue.';
725
+ blocks.push({
726
+ type: 'text',
727
+ text: normalizedPromptText
728
+ });
729
+
730
+ for (const image of normalizedAttachments.images || []) {
731
+ const parsed = parseDataUrl(image?.data);
732
+ if (!parsed) {
733
+ continue;
734
+ }
735
+
736
+ blocks.push({
737
+ type: 'image',
738
+ data: parsed.base64Data,
739
+ mimeType: parsed.mimeType
740
+ });
741
+ }
742
+
743
+ for (const resourceLink of normalizePromptResourceLinks(normalizedAttachments.resourceLinks || [])) {
744
+ blocks.push(resourceLink);
745
+ }
746
+
747
+ return {
748
+ blocks,
749
+ text: normalizedPromptText,
750
+ contentBlocks: buildConversationContentBlocks(
751
+ normalizedAttachments.images || [],
752
+ normalizedAttachments.resourceLinks || []
753
+ )
754
+ };
755
+ }
756
+
757
+ class LocalTerminalProcess {
758
+ constructor({
759
+ sessionId,
760
+ command,
761
+ args = [],
762
+ cwd = null,
763
+ env = [],
764
+ outputByteLimit = DEFAULT_TERMINAL_OUTPUT_LIMIT,
765
+ maxLifetimeMs = DEFAULT_TERMINAL_MAX_LIFETIME_MS
766
+ }) {
767
+ const normalizedSpawn = normalizeTerminalCommandForSpawn(command, args);
768
+
769
+ this.sessionId = sessionId;
770
+ this.command = normalizedSpawn.command;
771
+ this.args = normalizedSpawn.args;
772
+ this.cwd = cwd || process.cwd();
773
+ this.outputByteLimit = Number(outputByteLimit) > 0 ? Number(outputByteLimit) : DEFAULT_TERMINAL_OUTPUT_LIMIT;
774
+ this.maxLifetimeMs = Number(maxLifetimeMs) > 0 ? Number(maxLifetimeMs) : DEFAULT_TERMINAL_MAX_LIFETIME_MS;
775
+ this.output = '';
776
+ this.exitStatus = null;
777
+ this.waiters = [];
778
+ this.maxLifetimeTimer = null;
779
+
780
+ const envEntries = Array.isArray(env)
781
+ ? env.reduce((accumulator, entry) => {
782
+ if (entry?.name) {
783
+ accumulator[entry.name] = String(entry.value ?? '');
784
+ }
785
+ return accumulator;
786
+ }, {})
787
+ : {};
788
+
789
+ this.child = spawn(this.command, this.args, {
790
+ cwd: this.cwd,
791
+ env: sanitizeProcessEnv(process.env, envEntries),
792
+ stdio: ['ignore', 'pipe', 'pipe']
793
+ });
794
+
795
+ this.maxLifetimeTimer = setTimeout(() => {
796
+ void this.kill();
797
+ }, this.maxLifetimeMs);
798
+ this.maxLifetimeTimer.unref?.();
799
+
800
+ const appendOutput = (chunk) => {
801
+ this.output += chunk.toString();
802
+ if (Buffer.byteLength(this.output, 'utf8') > this.outputByteLimit) {
803
+ const sliced = Buffer.from(this.output, 'utf8').subarray(-this.outputByteLimit);
804
+ this.output = sliced.toString('utf8');
805
+ }
806
+ };
807
+
808
+ this.child.stdout.on('data', appendOutput);
809
+ this.child.stderr.on('data', appendOutput);
810
+ this.child.on('close', (exitCode, signal) => {
811
+ if (this.maxLifetimeTimer) {
812
+ clearTimeout(this.maxLifetimeTimer);
813
+ this.maxLifetimeTimer = null;
814
+ }
815
+ this.exitStatus = {
816
+ exitCode,
817
+ signal
818
+ };
819
+ this.waiters.splice(0).forEach((resolve) => resolve(this.exitStatus));
820
+ });
821
+ }
822
+
823
+ currentOutput() {
824
+ return {
825
+ output: this.output,
826
+ exitStatus: this.exitStatus || undefined
827
+ };
828
+ }
829
+
830
+ async waitForExit() {
831
+ if (this.exitStatus) {
832
+ return this.exitStatus;
833
+ }
834
+
835
+ return new Promise((resolve) => {
836
+ this.waiters.push(resolve);
837
+ });
838
+ }
839
+
840
+ async kill() {
841
+ if (!this.exitStatus) {
842
+ try {
843
+ this.child.kill('SIGTERM');
844
+ } catch {}
845
+ }
846
+
847
+ return {};
848
+ }
849
+
850
+ async release() {
851
+ await this.kill();
852
+ return {};
853
+ }
854
+ }
855
+
856
+ export class AcpClient {
857
+ constructor({
858
+ agentKey,
859
+ writer,
860
+ projectPath,
861
+ agentCommandOverrides = {},
862
+ model = null,
863
+ permissionMode = 'default'
864
+ }) {
865
+ this.agentKey = String(agentKey || '').trim().toLowerCase();
866
+ this.writer = writer;
867
+ this.projectPath = path.resolve(projectPath || process.cwd());
868
+ this.agentCommandOverrides = agentCommandOverrides || {};
869
+ this.model = model || null;
870
+ this.permissionMode = permissionMode || 'default';
871
+
872
+ this.childProcess = null;
873
+ this.connection = null;
874
+ this.sessionId = null;
875
+ this.initializeResult = null;
876
+ this.suppressSessionUpdates = false;
877
+ this.pendingPermissionIds = new Set();
878
+ this.terminalProcesses = new Map();
879
+ this.spawnCleanup = null;
880
+ this.turnQueue = Promise.resolve();
881
+ this.isPromptInFlight = false;
882
+ this.pendingPromptDiagnostic = null;
883
+ this.hasReportedPromptFatalError = false;
884
+ this.streamState = {
885
+ turnId: null,
886
+ assistantSegmentIndex: 0,
887
+ assistantMessageId: null,
888
+ assistantTextStarted: false,
889
+ reasoningMessageId: null,
890
+ toolCalls: new Map()
891
+ };
892
+ }
893
+
894
+ emitPayload(payload) {
895
+ if (!this.writer || typeof this.writer.send !== 'function') {
896
+ return;
897
+ }
898
+
899
+ this.writer.send({
900
+ provider: this.agentKey,
901
+ sessionId: this.sessionId,
902
+ runtime: 'acp',
903
+ timestamp: nowIso(),
904
+ ...payload
905
+ });
906
+ }
907
+
908
+ attachWriter(writer) {
909
+ this.writer = writer || null;
910
+ if (this.sessionId) {
911
+ this.writer?.setSessionId?.(this.sessionId);
912
+ }
913
+ }
914
+
915
+ async configureForTurn({ writer, permissionMode = null, model = null } = {}) {
916
+ if (writer !== undefined) {
917
+ this.attachWriter(writer);
918
+ }
919
+
920
+ if (permissionMode) {
921
+ this.permissionMode = permissionMode;
922
+ }
923
+
924
+ if (model) {
925
+ const shouldUpdateModel = model !== this.model;
926
+ this.model = model;
927
+ if (shouldUpdateModel && this.connection && this.sessionId) {
928
+ await this.setModel(model);
929
+ }
930
+ }
931
+ }
932
+
933
+ emitConversationEvent(kind, payload = {}, options = {}) {
934
+ const event = createConversationEvent({
935
+ kind,
936
+ provider: this.agentKey,
937
+ sessionId: this.sessionId,
938
+ timestamp: options.timestamp || nowIso(),
939
+ payload,
940
+ extensions: {
941
+ runtimeSource: 'acp',
942
+ ...(options.extensions || {})
943
+ },
944
+ rawRef: {
945
+ runtime: 'acp',
946
+ sourceType: options.sourceType || 'acp-session-update'
947
+ }
948
+ });
949
+
950
+ this.emitPayload({
951
+ type: 'conversation-event',
952
+ event
953
+ });
954
+ }
955
+
956
+ emitSystemNotice(message, options = {}) {
957
+ if (!message) {
958
+ return;
959
+ }
960
+
961
+ this.emitConversationEvent(
962
+ CONVERSATION_EVENT_KINDS.SYSTEM_NOTICE,
963
+ { message },
964
+ {
965
+ sourceType: 'acp-system-notice',
966
+ ...options
967
+ }
968
+ );
969
+ }
970
+
971
+ reportPromptFailure(error, { emitRawError = false } = {}) {
972
+ if (this.hasReportedPromptFatalError) {
973
+ return;
974
+ }
975
+
976
+ this.hasReportedPromptFatalError = true;
977
+
978
+ const failedAt = nowIso();
979
+ this.flushOpenTextStreams(failedAt);
980
+ this.emitConversationEvent(
981
+ CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED,
982
+ {
983
+ state: 'errored'
984
+ },
985
+ {
986
+ timestamp: failedAt,
987
+ sourceType: 'acp-prompt-error'
988
+ }
989
+ );
990
+ this.emitPayload({
991
+ type: 'session-status',
992
+ isProcessing: false
993
+ });
994
+
995
+ if (emitRawError) {
996
+ this.emitPayload({
997
+ type: 'error',
998
+ error: error?.message || 'Agent prompt failed.'
999
+ });
1000
+ }
1001
+ }
1002
+
1003
+ handleAgentDiagnostic(text) {
1004
+ const fatalError = detectFatalAgentDiagnostic(this.agentKey, text);
1005
+ if (!fatalError || !this.isPromptInFlight) {
1006
+ return;
1007
+ }
1008
+
1009
+ const pendingDiagnostic = this.pendingPromptDiagnostic;
1010
+ if (pendingDiagnostic?.handled) {
1011
+ return;
1012
+ }
1013
+
1014
+ if (pendingDiagnostic) {
1015
+ pendingDiagnostic.handled = true;
1016
+ this.reportPromptFailure(fatalError, {
1017
+ emitRawError: true
1018
+ });
1019
+ pendingDiagnostic.reject(fatalError);
1020
+ }
1021
+
1022
+ if (this.childProcess && !this.childProcess.killed) {
1023
+ try {
1024
+ this.childProcess.kill('SIGTERM');
1025
+ } catch {}
1026
+ }
1027
+ }
1028
+
1029
+ async runExclusive(operation) {
1030
+ const previousTurn = this.turnQueue.catch(() => {});
1031
+ let releaseTurn;
1032
+
1033
+ this.turnQueue = new Promise((resolve) => {
1034
+ releaseTurn = resolve;
1035
+ });
1036
+
1037
+ await previousTurn;
1038
+
1039
+ try {
1040
+ return await operation();
1041
+ } finally {
1042
+ releaseTurn?.();
1043
+ }
1044
+ }
1045
+
1046
+ ensureTurnId() {
1047
+ if (!this.streamState.turnId) {
1048
+ this.streamState.turnId = randomUUID();
1049
+ }
1050
+ return this.streamState.turnId;
1051
+ }
1052
+
1053
+ createAssistantMessageId(prefix = 'assistant_text') {
1054
+ const turnId = this.ensureTurnId();
1055
+ return createEventMessageId(
1056
+ prefix,
1057
+ this.agentKey,
1058
+ this.sessionId,
1059
+ `${turnId}:segment-${this.streamState.assistantSegmentIndex}`
1060
+ );
1061
+ }
1062
+
1063
+ closeAssistantMessageBoundary(timestamp = nowIso(), sourceType = 'acp-message-boundary') {
1064
+ const hadAssistantBoundary = Boolean(
1065
+ this.streamState.assistantMessageId || this.streamState.assistantTextStarted
1066
+ );
1067
+
1068
+ if (this.streamState.assistantMessageId && this.streamState.assistantTextStarted) {
1069
+ this.emitConversationEvent(
1070
+ CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_END,
1071
+ { messageId: this.streamState.assistantMessageId },
1072
+ {
1073
+ timestamp,
1074
+ sourceType
1075
+ }
1076
+ );
1077
+ }
1078
+
1079
+ this.streamState.assistantMessageId = null;
1080
+ this.streamState.assistantTextStarted = false;
1081
+ if (hadAssistantBoundary) {
1082
+ this.streamState.assistantSegmentIndex += 1;
1083
+ }
1084
+ }
1085
+
1086
+ flushOpenTextStreams(timestamp = nowIso()) {
1087
+ this.closeAssistantMessageBoundary(timestamp, 'acp-turn-complete');
1088
+
1089
+ if (this.streamState.reasoningMessageId) {
1090
+ this.emitConversationEvent(
1091
+ CONVERSATION_EVENT_KINDS.REASONING_END,
1092
+ { messageId: this.streamState.reasoningMessageId },
1093
+ {
1094
+ timestamp,
1095
+ sourceType: 'acp-turn-complete'
1096
+ }
1097
+ );
1098
+ this.streamState.reasoningMessageId = null;
1099
+ }
1100
+ }
1101
+
1102
+ resetTurnState({ nextTurnId = randomUUID() } = {}) {
1103
+ this.flushOpenTextStreams(nowIso());
1104
+ this.streamState.toolCalls.clear();
1105
+ this.streamState.turnId = nextTurnId;
1106
+ this.streamState.assistantSegmentIndex = 0;
1107
+ }
1108
+
1109
+ async handleRequestPermission(request) {
1110
+ const toolKind = inferToolKind(request?.toolCall);
1111
+ const allowAllOption = pickPermissionOption(request?.options, ['allow_once', 'allow_always']);
1112
+ const rejectOption = pickPermissionOption(request?.options, ['reject_once', 'reject_always']);
1113
+
1114
+ if (this.permissionMode === 'bypassPermissions' && allowAllOption) {
1115
+ return createSelectedPermissionResponse(allowAllOption.optionId);
1116
+ }
1117
+
1118
+ if (this.permissionMode === 'acceptEdits' && ['read', 'search', 'edit', 'move', 'think', 'switch_mode'].includes(toolKind)) {
1119
+ return allowAllOption
1120
+ ? createSelectedPermissionResponse(allowAllOption.optionId)
1121
+ : createCancelledPermissionResponse();
1122
+ }
1123
+
1124
+ if (this.permissionMode === 'plan') {
1125
+ if (['read', 'search', 'fetch', 'think', 'switch_mode'].includes(toolKind) && allowAllOption) {
1126
+ return createSelectedPermissionResponse(allowAllOption.optionId);
1127
+ }
1128
+ if (rejectOption) {
1129
+ return createSelectedPermissionResponse(rejectOption.optionId);
1130
+ }
1131
+ return createCancelledPermissionResponse();
1132
+ }
1133
+
1134
+ const requestId = `acp-permission:${randomUUID()}`;
1135
+ const timestamp = nowIso();
1136
+
1137
+ this.pendingPermissionIds.add(requestId);
1138
+
1139
+ this.emitConversationEvent(
1140
+ CONVERSATION_EVENT_KINDS.APPROVAL_REQUEST,
1141
+ {
1142
+ requestId,
1143
+ toolName: request?.toolCall?.title || 'tool execution',
1144
+ input: request?.toolCall?.rawInput ?? null,
1145
+ options: cloneJsonValue(request?.options || []),
1146
+ toolCall: cloneJsonValue(request?.toolCall || null),
1147
+ context: {
1148
+ kind: toolKind,
1149
+ options: request?.options || []
1150
+ },
1151
+ sessionId: this.sessionId,
1152
+ message: createPermissionMessage(request?.toolCall)
1153
+ },
1154
+ {
1155
+ timestamp,
1156
+ extensions: {
1157
+ requestId
1158
+ },
1159
+ sourceType: 'acp-permission-request'
1160
+ }
1161
+ );
1162
+
1163
+ const response = await new Promise((resolve) => {
1164
+ pendingPermissionRequests.set(requestId, {
1165
+ request,
1166
+ sessionId: this.sessionId,
1167
+ provider: this.agentKey,
1168
+ resolve
1169
+ });
1170
+ });
1171
+
1172
+ this.pendingPermissionIds.delete(requestId);
1173
+ pendingPermissionRequests.delete(requestId);
1174
+
1175
+ const status = classifyPermissionStatus(response, request);
1176
+ this.emitConversationEvent(
1177
+ CONVERSATION_EVENT_KINDS.APPROVAL_RESOLVED,
1178
+ {
1179
+ requestId,
1180
+ status,
1181
+ toolName: request?.toolCall?.title || null,
1182
+ message: status === 'approved'
1183
+ ? `Approved ${request?.toolCall?.title || 'tool execution'}`
1184
+ : status === 'denied'
1185
+ ? `Denied ${request?.toolCall?.title || 'tool execution'}`
1186
+ : `Cancelled ${request?.toolCall?.title || 'tool execution'}`
1187
+ },
1188
+ {
1189
+ timestamp: nowIso(),
1190
+ extensions: {
1191
+ requestId
1192
+ },
1193
+ sourceType: 'acp-permission-resolution'
1194
+ }
1195
+ );
1196
+
1197
+ return response;
1198
+ }
1199
+
1200
+ async handleSessionUpdate(notification) {
1201
+ if (this.suppressSessionUpdates) {
1202
+ return;
1203
+ }
1204
+
1205
+ const update = notification?.update;
1206
+ const timestamp = nowIso();
1207
+ if (!update || typeof update !== 'object') {
1208
+ return;
1209
+ }
1210
+
1211
+ if (update.sessionUpdate === 'agent_message_chunk') {
1212
+ if (update.content?.type === 'text' && typeof update.content.text === 'string') {
1213
+ const messageId = this.streamState.assistantMessageId
1214
+ || this.createAssistantMessageId('assistant_text');
1215
+
1216
+ if (!this.streamState.assistantMessageId) {
1217
+ this.streamState.assistantMessageId = messageId;
1218
+ }
1219
+
1220
+ if (!this.streamState.assistantTextStarted) {
1221
+ this.emitConversationEvent(
1222
+ CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_START,
1223
+ { messageId },
1224
+ { timestamp, sourceType: 'acp-agent-message' }
1225
+ );
1226
+ this.streamState.assistantTextStarted = true;
1227
+ }
1228
+
1229
+ this.emitConversationEvent(
1230
+ CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_DELTA,
1231
+ {
1232
+ messageId,
1233
+ text: update.content.text
1234
+ },
1235
+ {
1236
+ timestamp,
1237
+ sourceType: 'acp-agent-message'
1238
+ }
1239
+ );
1240
+ return;
1241
+ }
1242
+
1243
+ const contentBlock = normalizeAssistantContentBlock(update.content);
1244
+ if (contentBlock) {
1245
+ const messageId = this.streamState.assistantMessageId
1246
+ || this.createAssistantMessageId('assistant_content');
1247
+
1248
+ if (!this.streamState.assistantMessageId) {
1249
+ this.streamState.assistantMessageId = messageId;
1250
+ }
1251
+
1252
+ this.emitConversationEvent(
1253
+ CONVERSATION_EVENT_KINDS.ASSISTANT_CONTENT_BLOCK,
1254
+ {
1255
+ messageId,
1256
+ contentBlock
1257
+ },
1258
+ {
1259
+ timestamp,
1260
+ sourceType: 'acp-agent-message'
1261
+ }
1262
+ );
1263
+ }
1264
+ return;
1265
+ }
1266
+
1267
+ if (update.sessionUpdate === 'agent_thought_chunk') {
1268
+ if (update.content?.type === 'text' && typeof update.content.text === 'string') {
1269
+ const messageId = this.streamState.reasoningMessageId
1270
+ || createEventMessageId('reasoning', this.agentKey, this.sessionId, this.ensureTurnId());
1271
+
1272
+ if (!this.streamState.reasoningMessageId) {
1273
+ this.emitConversationEvent(
1274
+ CONVERSATION_EVENT_KINDS.REASONING_START,
1275
+ { messageId },
1276
+ { timestamp, sourceType: 'acp-agent-thought' }
1277
+ );
1278
+ this.streamState.reasoningMessageId = messageId;
1279
+ }
1280
+
1281
+ this.emitConversationEvent(
1282
+ CONVERSATION_EVENT_KINDS.REASONING_DELTA,
1283
+ {
1284
+ messageId,
1285
+ text: update.content.text
1286
+ },
1287
+ {
1288
+ timestamp,
1289
+ sourceType: 'acp-agent-thought'
1290
+ }
1291
+ );
1292
+ }
1293
+ return;
1294
+ }
1295
+
1296
+ if (update.sessionUpdate === 'end_turn') {
1297
+ this.flushOpenTextStreams(timestamp);
1298
+ return;
1299
+ }
1300
+
1301
+ if (update.sessionUpdate === 'plan') {
1302
+ this.emitConversationEvent(
1303
+ CONVERSATION_EVENT_KINDS.PLAN_UPDATE,
1304
+ {
1305
+ entries: cloneJsonValue(update.entries || []),
1306
+ message: update.message || 'Implementation plan updated'
1307
+ },
1308
+ {
1309
+ timestamp,
1310
+ sourceType: 'acp-plan'
1311
+ }
1312
+ );
1313
+ return;
1314
+ }
1315
+
1316
+ if (update.sessionUpdate === 'current_mode_update') {
1317
+ const payload = {
1318
+ currentModeId: String(update.currentModeId || '').trim() || null
1319
+ };
1320
+
1321
+ this.emitConversationEvent(
1322
+ CONVERSATION_EVENT_KINDS.MODE_UPDATE,
1323
+ payload,
1324
+ {
1325
+ timestamp,
1326
+ sourceType: 'acp-mode-update'
1327
+ }
1328
+ );
1329
+ return;
1330
+ }
1331
+
1332
+ if (update.sessionUpdate === 'available_commands_update') {
1333
+ this.emitConversationEvent(
1334
+ CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE,
1335
+ normalizeAvailableCommandsUpdate(update),
1336
+ {
1337
+ timestamp,
1338
+ sourceType: 'acp-available-commands-update'
1339
+ }
1340
+ );
1341
+ return;
1342
+ }
1343
+
1344
+ if (update.sessionUpdate === 'tool_call' || update.sessionUpdate === 'tool_call_update') {
1345
+ const existing = this.streamState.toolCalls.get(update.toolCallId) || {
1346
+ snapshotSignature: null,
1347
+ resultSignature: null,
1348
+ ended: false,
1349
+ started: false,
1350
+ toolName: null
1351
+ };
1352
+ const toolName = update.title || existing.toolName || 'Tool';
1353
+ const toolPayload = buildToolCallConversationPayload(update, toolName);
1354
+
1355
+ if (!existing.started) {
1356
+ // Tool calls should visually split assistant prose into before/after segments.
1357
+ // Closing the current assistant message here lets post-tool text start a fresh block.
1358
+ this.closeAssistantMessageBoundary(timestamp, 'acp-tool-boundary');
1359
+ this.emitConversationEvent(
1360
+ CONVERSATION_EVENT_KINDS.TOOL_CALL_START,
1361
+ toolPayload,
1362
+ {
1363
+ timestamp,
1364
+ sourceType: 'acp-tool-call'
1365
+ }
1366
+ );
1367
+ existing.started = true;
1368
+ }
1369
+
1370
+ const snapshotPayload = update.rawInput === undefined
1371
+ ? toolPayload
1372
+ : {
1373
+ ...toolPayload,
1374
+ input: cloneJsonValue(update.rawInput)
1375
+ };
1376
+ const snapshotSignature = normalizeComparableValue(snapshotPayload);
1377
+ if (snapshotSignature !== existing.snapshotSignature) {
1378
+ this.emitConversationEvent(
1379
+ CONVERSATION_EVENT_KINDS.TOOL_CALL_INPUT,
1380
+ snapshotPayload,
1381
+ {
1382
+ timestamp,
1383
+ sourceType: 'acp-tool-call'
1384
+ }
1385
+ );
1386
+ existing.snapshotSignature = snapshotSignature;
1387
+ }
1388
+
1389
+ const isTerminalStatus = update.status === 'completed' || update.status === 'failed';
1390
+ if (isTerminalStatus && !existing.ended) {
1391
+ this.emitConversationEvent(
1392
+ CONVERSATION_EVENT_KINDS.TOOL_CALL_END,
1393
+ toolPayload,
1394
+ {
1395
+ timestamp,
1396
+ sourceType: 'acp-tool-call'
1397
+ }
1398
+ );
1399
+
1400
+ const resultText = toToolContentText(update);
1401
+ const resultSignature = normalizeComparableValue({
1402
+ text: resultText,
1403
+ rawOutput: update.rawOutput,
1404
+ status: update.status,
1405
+ content: update.content,
1406
+ locations: update.locations
1407
+ });
1408
+
1409
+ if (resultSignature !== existing.resultSignature) {
1410
+ this.emitConversationEvent(
1411
+ CONVERSATION_EVENT_KINDS.TOOL_RESULT,
1412
+ {
1413
+ ...toolPayload,
1414
+ content: resultText || '',
1415
+ isError: update.status === 'failed',
1416
+ rawOutput: cloneJsonValue(update.rawOutput),
1417
+ contentBlocks: Array.isArray(update.content) ? cloneJsonValue(update.content) : []
1418
+ },
1419
+ {
1420
+ timestamp,
1421
+ sourceType: 'acp-tool-call'
1422
+ }
1423
+ );
1424
+ existing.resultSignature = resultSignature;
1425
+ }
1426
+
1427
+ existing.ended = true;
1428
+ }
1429
+
1430
+ existing.toolName = toolName;
1431
+ this.streamState.toolCalls.set(update.toolCallId, existing);
1432
+ }
1433
+ }
1434
+
1435
+ async start() {
1436
+ if (this.connection) {
1437
+ return this.initializeResult;
1438
+ }
1439
+
1440
+ const launchCommand = await resolveLaunchCommand(this.agentKey, this.agentCommandOverrides);
1441
+ const { command, args } = splitCommandLine(launchCommand);
1442
+ const spawnEnvironment = await prepareSpawnEnvironment(this.agentKey);
1443
+ this.spawnCleanup = spawnEnvironment.cleanup;
1444
+
1445
+ this.childProcess = spawn(command, args, {
1446
+ cwd: this.projectPath,
1447
+ stdio: ['pipe', 'pipe', 'pipe'],
1448
+ windowsHide: true,
1449
+ env: spawnEnvironment.env
1450
+ });
1451
+
1452
+ this.childProcess.stderr.on('data', (chunk) => {
1453
+ const text = String(chunk || '').trim();
1454
+ if (text) {
1455
+ console.warn(`[ACP:${this.agentKey}] ${text}`);
1456
+ this.handleAgentDiagnostic(text);
1457
+ }
1458
+ });
1459
+
1460
+ const stream = ndJsonStream(
1461
+ Writable.toWeb(this.childProcess.stdin),
1462
+ Readable.toWeb(this.childProcess.stdout)
1463
+ );
1464
+
1465
+ this.connection = new ClientSideConnection(() => ({
1466
+ requestPermission: (params) => this.handleRequestPermission(params),
1467
+ sessionUpdate: (params) => this.handleSessionUpdate(params),
1468
+ readTextFile: async (params) => {
1469
+ const resolvedPath = path.isAbsolute(params.path)
1470
+ ? params.path
1471
+ : path.resolve(this.projectPath, params.path);
1472
+ const content = await fs.readFile(resolvedPath, 'utf8');
1473
+ return {
1474
+ content: readLines(content, params.line, params.limit)
1475
+ };
1476
+ },
1477
+ writeTextFile: async (params) => {
1478
+ const resolvedPath = path.isAbsolute(params.path)
1479
+ ? params.path
1480
+ : path.resolve(this.projectPath, params.path);
1481
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
1482
+ await fs.writeFile(resolvedPath, params.content, 'utf8');
1483
+ return {};
1484
+ },
1485
+ createTerminal: async (params) => {
1486
+ const terminalId = `terminal:${randomUUID()}`;
1487
+ const terminal = new LocalTerminalProcess({
1488
+ sessionId: params.sessionId,
1489
+ command: params.command,
1490
+ args: params.args,
1491
+ cwd: params.cwd || this.projectPath,
1492
+ env: params.env,
1493
+ outputByteLimit: params.outputByteLimit
1494
+ });
1495
+ this.terminalProcesses.set(terminalId, terminal);
1496
+ return { terminalId };
1497
+ },
1498
+ terminalOutput: async (params) => {
1499
+ return this.terminalProcesses.get(params.terminalId)?.currentOutput() || { output: '' };
1500
+ },
1501
+ waitForTerminalExit: async (params) => {
1502
+ return this.terminalProcesses.get(params.terminalId)?.waitForExit() || {};
1503
+ },
1504
+ killTerminal: async (params) => {
1505
+ const terminal = this.terminalProcesses.get(params.terminalId);
1506
+ if (terminal) {
1507
+ await terminal.kill();
1508
+ }
1509
+ return {};
1510
+ },
1511
+ releaseTerminal: async (params) => {
1512
+ const terminal = this.terminalProcesses.get(params.terminalId);
1513
+ if (terminal) {
1514
+ await terminal.release();
1515
+ this.terminalProcesses.delete(params.terminalId);
1516
+ }
1517
+ return {};
1518
+ }
1519
+ }), stream);
1520
+
1521
+ this.initializeResult = await this.connection.initialize({
1522
+ protocolVersion: PROTOCOL_VERSION,
1523
+ clientCapabilities: {
1524
+ fs: {
1525
+ readTextFile: true,
1526
+ writeTextFile: true
1527
+ },
1528
+ terminal: true
1529
+ },
1530
+ clientInfo: {
1531
+ name: '@axhub/genie',
1532
+ title: 'Axhub Genie',
1533
+ version: process.env.APP_VERSION || 'unknown'
1534
+ }
1535
+ });
1536
+
1537
+ return this.initializeResult;
1538
+ }
1539
+
1540
+ async createSession() {
1541
+ const response = await this.connection.newSession({
1542
+ cwd: this.projectPath,
1543
+ mcpServers: []
1544
+ });
1545
+
1546
+ this.sessionId = response.sessionId;
1547
+ this.attachWriter(this.writer);
1548
+ const normalizedModes = normalizeSessionModeState(response?.modes);
1549
+ this.emitPayload({
1550
+ type: 'session-created',
1551
+ modes: normalizedModes
1552
+ });
1553
+
1554
+ if (normalizedModes) {
1555
+ this.emitConversationEvent(
1556
+ CONVERSATION_EVENT_KINDS.MODE_UPDATE,
1557
+ normalizedModes,
1558
+ {
1559
+ timestamp: nowIso(),
1560
+ sourceType: 'acp-session-created-modes'
1561
+ }
1562
+ );
1563
+ }
1564
+
1565
+ if (this.model) {
1566
+ await this.setModel(this.model);
1567
+ }
1568
+
1569
+ return response;
1570
+ }
1571
+
1572
+ async loadSession(sessionId, { suppressReplayUpdates = true } = {}) {
1573
+ this.sessionId = String(sessionId || '').trim();
1574
+ this.attachWriter(this.writer);
1575
+ this.suppressSessionUpdates = Boolean(suppressReplayUpdates);
1576
+ try {
1577
+ const response = await this.connection.loadSession({
1578
+ cwd: this.projectPath,
1579
+ mcpServers: [],
1580
+ sessionId: this.sessionId
1581
+ });
1582
+
1583
+ const normalizedModes = normalizeSessionModeState(response?.modes);
1584
+ if (normalizedModes) {
1585
+ this.emitConversationEvent(
1586
+ CONVERSATION_EVENT_KINDS.MODE_UPDATE,
1587
+ normalizedModes,
1588
+ {
1589
+ timestamp: nowIso(),
1590
+ sourceType: 'acp-session-loaded-modes'
1591
+ }
1592
+ );
1593
+ }
1594
+
1595
+ if (this.model) {
1596
+ await this.setModel(this.model);
1597
+ }
1598
+
1599
+ return response;
1600
+ } finally {
1601
+ this.suppressSessionUpdates = false;
1602
+ }
1603
+ }
1604
+
1605
+ async setModel(modelId) {
1606
+ if (!modelId) {
1607
+ return;
1608
+ }
1609
+
1610
+ try {
1611
+ await this.connection.setSessionModel({
1612
+ sessionId: this.sessionId,
1613
+ modelId
1614
+ });
1615
+ } catch {}
1616
+ }
1617
+
1618
+ async setMode(modeId) {
1619
+ const normalizedModeId = String(modeId || '').trim();
1620
+ if (!normalizedModeId || !this.sessionId || !this.connection?.setSessionMode) {
1621
+ return false;
1622
+ }
1623
+
1624
+ await this.connection.setSessionMode({
1625
+ sessionId: this.sessionId,
1626
+ modeId: normalizedModeId
1627
+ });
1628
+ return true;
1629
+ }
1630
+
1631
+ async sendPrompt(promptText, attachments = {}) {
1632
+ this.resetTurnState();
1633
+ this.hasReportedPromptFatalError = false;
1634
+ const promptPayload = buildPromptBlocks(promptText, attachments);
1635
+ const clientRequestId = typeof attachments?.clientRequestId === 'string' && attachments.clientRequestId.trim()
1636
+ ? attachments.clientRequestId.trim()
1637
+ : null;
1638
+
1639
+ this.emitConversationEvent(
1640
+ CONVERSATION_EVENT_KINDS.USER_MESSAGE,
1641
+ {
1642
+ text: promptPayload.text,
1643
+ contentBlocks: promptPayload.contentBlocks
1644
+ },
1645
+ {
1646
+ timestamp: nowIso(),
1647
+ sourceType: 'acp-user-prompt',
1648
+ extensions: clientRequestId ? { clientRequestId } : {}
1649
+ }
1650
+ );
1651
+
1652
+ this.emitConversationEvent(
1653
+ CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED,
1654
+ {
1655
+ state: 'queued'
1656
+ },
1657
+ {
1658
+ timestamp: nowIso(),
1659
+ sourceType: 'acp-prompt-start'
1660
+ }
1661
+ );
1662
+
1663
+ this.emitPayload({
1664
+ type: 'session-status',
1665
+ isProcessing: true
1666
+ });
1667
+
1668
+ this.isPromptInFlight = true;
1669
+ let diagnosticGuard = null;
1670
+ const diagnosticPromise = new Promise((_, reject) => {
1671
+ diagnosticGuard = {
1672
+ handled: false,
1673
+ reject
1674
+ };
1675
+ this.pendingPromptDiagnostic = diagnosticGuard;
1676
+ });
1677
+
1678
+ const promptPromise = this.connection.prompt({
1679
+ sessionId: this.sessionId,
1680
+ prompt: promptPayload.blocks
1681
+ });
1682
+ promptPromise.catch(() => {});
1683
+
1684
+ try {
1685
+ const response = await Promise.race([promptPromise, diagnosticPromise]);
1686
+ const finishedAt = nowIso();
1687
+ this.flushOpenTextStreams(finishedAt);
1688
+ this.emitConversationEvent(
1689
+ CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED,
1690
+ {
1691
+ state: response?.stopReason === 'cancelled' ? 'aborted' : 'completed',
1692
+ stopReason: response?.stopReason || null
1693
+ },
1694
+ {
1695
+ timestamp: finishedAt,
1696
+ sourceType: 'acp-prompt-complete'
1697
+ }
1698
+ );
1699
+
1700
+ this.emitPayload({
1701
+ type: 'session-status',
1702
+ isProcessing: false
1703
+ });
1704
+
1705
+ return response;
1706
+ } catch (error) {
1707
+ this.reportPromptFailure(error);
1708
+ throw error;
1709
+ } finally {
1710
+ this.isPromptInFlight = false;
1711
+ if (this.pendingPromptDiagnostic === diagnosticGuard) {
1712
+ this.pendingPromptDiagnostic = null;
1713
+ }
1714
+ }
1715
+ }
1716
+
1717
+ async cancel(sessionId = this.sessionId) {
1718
+ const normalizedSessionId = String(sessionId || this.sessionId || '').trim();
1719
+ if (!normalizedSessionId) {
1720
+ return false;
1721
+ }
1722
+
1723
+ for (const requestId of this.pendingPermissionIds) {
1724
+ AcpClient.resolvePermissionRequest(requestId, { cancelled: true });
1725
+ }
1726
+
1727
+ try {
1728
+ await this.connection.cancel({
1729
+ sessionId: normalizedSessionId
1730
+ });
1731
+ this.emitPayload({
1732
+ type: 'session-aborted',
1733
+ success: true
1734
+ });
1735
+ return true;
1736
+ } catch {
1737
+ return false;
1738
+ }
1739
+ }
1740
+
1741
+ async close() {
1742
+ for (const requestId of this.pendingPermissionIds) {
1743
+ AcpClient.resolvePermissionRequest(requestId, { cancelled: true });
1744
+ }
1745
+
1746
+ for (const [terminalId, terminal] of this.terminalProcesses.entries()) {
1747
+ try {
1748
+ await terminal.release();
1749
+ } catch {}
1750
+ this.terminalProcesses.delete(terminalId);
1751
+ }
1752
+
1753
+ if (this.childProcess && !this.childProcess.killed) {
1754
+ if (!this.childProcess.stdin.destroyed) {
1755
+ this.childProcess.stdin.end();
1756
+ }
1757
+
1758
+ const closed = await new Promise((resolve) => {
1759
+ let settled = false;
1760
+ const finalize = (value) => {
1761
+ if (settled) {
1762
+ return;
1763
+ }
1764
+ settled = true;
1765
+ resolve(value);
1766
+ };
1767
+
1768
+ const timer = setTimeout(() => finalize(false), DEFAULT_CLOSE_GRACE_MS);
1769
+ this.childProcess.once('close', () => {
1770
+ clearTimeout(timer);
1771
+ finalize(true);
1772
+ });
1773
+ });
1774
+
1775
+ if (!closed) {
1776
+ try {
1777
+ this.childProcess.kill('SIGTERM');
1778
+ } catch {}
1779
+ }
1780
+ }
1781
+
1782
+ if (this.spawnCleanup) {
1783
+ try {
1784
+ await this.spawnCleanup();
1785
+ } catch {}
1786
+ this.spawnCleanup = null;
1787
+ }
1788
+
1789
+ this.connection = null;
1790
+ this.childProcess = null;
1791
+ this.initializeResult = null;
1792
+ }
1793
+
1794
+ static resolvePermissionRequest(requestId, decision = {}) {
1795
+ const pending = pendingPermissionRequests.get(requestId);
1796
+ if (!pending) {
1797
+ return false;
1798
+ }
1799
+
1800
+ pending.resolve(choosePermissionResponse(pending.request, decision));
1801
+ return true;
1802
+ }
1803
+ }
1804
+
1805
+ export { normalizeTerminalCommandForSpawn };