@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.
- package/dist/assets/App-BWSqiXAT.js +220 -0
- package/dist/assets/App-DrlLKa8f.css +1 -0
- package/dist/assets/ReviewApp-nz3mbArg.js +1 -0
- package/dist/assets/{_basePickBy-CFRQvihx.js → _basePickBy-C19AekOu.js} +1 -1
- package/dist/assets/{_baseUniq-Dhh8nCvs.js → _baseUniq-JsnevLw_.js} +1 -1
- package/dist/assets/{arc-DQ0v3dU4.js → arc-BLpcuBlf.js} +1 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +36 -0
- package/dist/assets/{blockDiagram-WCTKOSBZ-Bbxhj5KC.js → blockDiagram-WCTKOSBZ-DQBLwsUS.js} +3 -3
- package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +10 -0
- package/dist/assets/channel-DkFNxV_H.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DlvtrM0q.js → chunk-4BX2VUAB-De63kbgc.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DJUSHyTa.js → chunk-55IACEB6-DtTDDdM9.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C6Ch-htf.js → chunk-FMBD7UC4-DHuwd8tw.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-DzQIht58.js → chunk-JSJVCQXG-BgytFtmO.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-C05jARMH.js → chunk-KX2RTZJC-nZdp86aN.js} +1 -1
- package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +220 -0
- package/dist/assets/{chunk-QZHKN3VN-jxti9HTX.js → chunk-QZHKN3VN-DvUQ3mnO.js} +1 -1
- package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +189 -0
- package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +1 -0
- package/dist/assets/clone-C0lCEIEO.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/{dagre-KLK3FWXG-DJ3dNSYk.js → dagre-KLK3FWXG-CHYIvW47.js} +1 -1
- package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +24 -0
- package/dist/assets/{diagram-IFDJBPK2-Da6K4aP-.js → diagram-IFDJBPK2-Dzsiln_C.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-vZZKB92A.js → diagram-P4PSJMXO-DKnGbUpE.js} +1 -1
- package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +70 -0
- package/dist/assets/{flowDiagram-PKNHOUZH-DUV13pHi.js → flowDiagram-PKNHOUZH-BAZ2-jKp.js} +4 -4
- package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +292 -0
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BZ5gW69I.js → gitGraphDiagram-K3NZZRJ6-BflpyjGy.js} +1 -1
- package/dist/assets/{graph-BbvHswRd.js → graph-suelaXFh.js} +1 -1
- package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +1 -0
- package/dist/assets/index-B01NxbUv.css +1 -0
- package/dist/assets/index-DW5pGgQ_.js +2 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-8auUIPKW.js → infoDiagram-LFFYTUFH-pfD1FA3p.js} +1 -1
- package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +70 -0
- package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +139 -0
- package/dist/assets/{kanban-definition-K7BYSVSG-Bappd2YO.js → kanban-definition-K7BYSVSG-FWinmur1.js} +5 -5
- package/dist/assets/{layout-BmbfFZKy.js → layout-vcz43XvZ.js} +1 -1
- package/dist/assets/{linear-WZnF-PT6.js → linear-le4gc0vx.js} +1 -1
- package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +870 -0
- package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +68 -0
- package/dist/assets/{pieDiagram-SKSYHLDU-uxjlAy1t.js → pieDiagram-SKSYHLDU-C7PKDh3b.js} +2 -2
- package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +7 -0
- package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +73 -0
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-2-FHHM-R.js → sankeyDiagram-WA2Y5GQK-4gulcOP4.js} +3 -3
- package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +145 -0
- package/dist/assets/{stateDiagram-RAJIS63D-DoW8U53H.js → stateDiagram-RAJIS63D-CB4Vl7qM.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +1 -0
- package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +61 -0
- package/dist/assets/{treemap-KZPCXAKY-ajdAP-72.js → treemap-KZPCXAKY-DZSEE6Hz.js} +58 -58
- package/dist/assets/vendor-codemirror-CyOKkaQZ.js +31 -0
- package/dist/assets/vendor-react-CP4yFTs7.js +8 -0
- package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
- package/dist/assets/{vennDiagram-LZ73GAT5-C9If0AT0.js → vennDiagram-LZ73GAT5-8E_G06fI.js} +4 -4
- package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +7 -0
- package/dist/favicon.png +0 -0
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/index.html +4 -5
- package/dist/logo-128.png +0 -0
- package/dist/logo-256.png +0 -0
- package/dist/logo-32.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-64.png +0 -0
- package/package.json +2 -1
- package/server/_legacy-providers/README.md +30 -0
- package/server/_legacy-providers/claude-sdk.js +956 -0
- package/server/_legacy-providers/gemini-cli.js +368 -0
- package/server/_legacy-providers/openai-codex.js +705 -0
- package/server/_legacy-providers/opencode-cli.js +674 -0
- package/server/acp-runtime/client.js +1805 -0
- package/server/acp-runtime/client.test.js +688 -0
- package/server/acp-runtime/index.js +419 -0
- package/server/acp-runtime/registry.js +45 -0
- package/server/acp-runtime/session-store.js +254 -0
- package/server/acp-runtime/session-store.test.js +89 -0
- package/server/channels/runtime/AgentRuntimeAdapter.js +21 -70
- package/server/claude-sdk.js +24 -944
- package/server/cli.js +11 -5
- package/server/external-agent/service.js +77 -63
- package/server/gemini-cli.js +23 -360
- package/server/index.js +54 -46
- package/server/openai-codex.js +24 -698
- package/server/opencode-cli.js +70 -640
- package/server/routes/agent.js +2 -0
- package/server/routes/codex.js +5 -5
- package/server/routes/git.js +3 -20
- package/server/routes/mcp.js +18 -34
- package/server/routes/session-core.js +44 -10
- package/server/session-core/abortSession.js +2 -18
- package/server/session-core/eventStore.js +5 -1
- package/server/session-core/providerAdapters.js +98 -10
- package/server/session-core/providerDiscovery.js +2 -2
- package/server/session-core/runtimeState.js +16 -17
- package/server/session-core/runtimeWriter.js +19 -12
- package/server/utils/codexPath.js +3 -1
- package/server/utils/spawnCommand.js +7 -0
- package/shared/conversationEvents.js +347 -10
- package/shared/conversationEvents.test.js +403 -0
- package/dist/assets/App-BxazfNJn.js +0 -484
- package/dist/assets/App-qxJ8_QYu.css +0 -32
- package/dist/assets/ReviewApp-CsqTAlGU.js +0 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-DmUHdvQH.js +0 -36
- package/dist/assets/c4Diagram-IC4MRINW-BOivDlQU.js +0 -10
- package/dist/assets/channel-Cj8xVD0X.js +0 -1
- package/dist/assets/chunk-NQ4KR5QH-Ci-n7jfu.js +0 -220
- package/dist/assets/chunk-WL4C6EOR-C559Mk71.js +0 -189
- package/dist/assets/classDiagram-VBA2DB6C-CI2zklxw.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-CI2zklxw.js +0 -1
- package/dist/assets/clone-BEVqubrI.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-DNO9ncXL.js +0 -1
- package/dist/assets/cytoscape.esm-2ZfV8NB5.js +0 -331
- package/dist/assets/diagram-E7M64L7V-Ba_LGLun.js +0 -24
- package/dist/assets/erDiagram-INFDFZHY-Csb8dFdP.js +0 -70
- package/dist/assets/ganttDiagram-A5KZAMGK-B5Kv9Wfz.js +0 -292
- package/dist/assets/highlighted-body-TPN3WLV5-DZJajMGm.js +0 -1
- package/dist/assets/index-BFX9lxRB.css +0 -1
- package/dist/assets/index-BiErUGrv.js +0 -2
- package/dist/assets/ishikawaDiagram-PHBUUO56-JmsNlo2I.js +0 -70
- package/dist/assets/journeyDiagram-4ABVD52K-Cuudv7Vv.js +0 -139
- package/dist/assets/mermaid-O7DHMXV3-D-2fQRvw.js +0 -988
- package/dist/assets/mindmap-definition-YRQLILUH-BQHnzzud.js +0 -68
- package/dist/assets/quadrantDiagram-337W2JSQ-DpwZU-f_.js +0 -7
- package/dist/assets/requirementDiagram-Z7DCOOCP-C_9ClOWm.js +0 -73
- package/dist/assets/sequenceDiagram-2WXFIKYE-egns-0XI.js +0 -145
- package/dist/assets/stateDiagram-v2-FVOUBMTO-BoFZZ4Ds.js +0 -1
- package/dist/assets/timeline-definition-YZTLITO2-chPa8ppH.js +0 -61
- package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
- package/dist/assets/vendor-react-Cpt6D04s.js +0 -59
- package/dist/assets/vendor-xterm-DfaPXD3y.js +0 -66
- 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 };
|