tabminal 2.0.14 → 2.0.16
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/ACP_PLANING.md +184 -0
- package/AGENTS.md +360 -209
- package/README.md +238 -105
- package/package.json +3 -1
- package/public/app.js +8481 -553
- package/public/index.html +150 -2
- package/public/styles.css +1977 -84
- package/shell/tabminal-hooks.bash +10 -0
- package/src/acp-manager.mjs +3469 -0
- package/src/acp-test-agent.mjs +691 -0
- package/src/persistence.mjs +153 -0
- package/src/server.mjs +298 -10
- package/src/terminal-manager.mjs +140 -64
- package/src/terminal-session.mjs +131 -8
|
@@ -0,0 +1,3469 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
+
import { Readable, Writable } from 'node:stream';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
11
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
12
|
+
import * as persistence from './persistence.mjs';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 2 * 60 * 1000;
|
|
15
|
+
const DEFAULT_TERMINAL_OUTPUT_LIMIT = 256 * 1024;
|
|
16
|
+
const DEFAULT_AVAILABILITY_OVERRIDE_TTL_MS = 30 * 1000;
|
|
17
|
+
const DEFAULT_PROBE_CACHE_TTL_MS = 15 * 1000;
|
|
18
|
+
const DEFAULT_TRANSCRIPT_PERSIST_DELAY_MS = 250;
|
|
19
|
+
const TEXT_ATTACHMENT_EXTENSIONS = new Set([
|
|
20
|
+
'txt', 'md', 'markdown', 'json', 'jsonl', 'yaml', 'yml', 'toml',
|
|
21
|
+
'ini', 'env', 'xml', 'html', 'htm', 'css', 'scss', 'less', 'csv',
|
|
22
|
+
'tsv', 'log', 'js', 'mjs', 'cjs', 'ts', 'tsx', 'jsx', 'py', 'rb',
|
|
23
|
+
'go', 'rs', 'java', 'kt', 'swift', 'c', 'cc', 'cpp', 'h', 'hpp',
|
|
24
|
+
'sh', 'bash', 'zsh', 'fish', 'sql', 'graphql', 'gql', 'diff', 'patch'
|
|
25
|
+
]);
|
|
26
|
+
const NPX_COMMAND = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
27
|
+
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const TEST_AGENT_PATH = path.join(CURRENT_DIR, 'acp-test-agent.mjs');
|
|
29
|
+
const AGENT_CONFIG_ENV_KEYS = {
|
|
30
|
+
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
31
|
+
claude: [
|
|
32
|
+
'ANTHROPIC_API_KEY',
|
|
33
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
34
|
+
'ANTHROPIC_VERTEX_PROJECT_ID',
|
|
35
|
+
'GCLOUD_PROJECT',
|
|
36
|
+
'GOOGLE_CLOUD_PROJECT',
|
|
37
|
+
'CLOUD_ML_REGION',
|
|
38
|
+
'GOOGLE_APPLICATION_CREDENTIALS'
|
|
39
|
+
],
|
|
40
|
+
copilot: [
|
|
41
|
+
'COPILOT_GITHUB_TOKEN',
|
|
42
|
+
'GH_TOKEN',
|
|
43
|
+
'GITHUB_TOKEN'
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function getAllowedAgentEnvKeys(agentId) {
|
|
48
|
+
return AGENT_CONFIG_ENV_KEYS[agentId] || [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeConfiguredEnv(agentId, env) {
|
|
52
|
+
const allowedKeys = new Set(getAllowedAgentEnvKeys(agentId));
|
|
53
|
+
if (!allowedKeys.size || !env || typeof env !== 'object') {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
const normalized = {};
|
|
57
|
+
for (const [key, value] of Object.entries(env)) {
|
|
58
|
+
if (!allowedKeys.has(key)) continue;
|
|
59
|
+
normalized[key] = typeof value === 'string' ? value : '';
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasConfiguredValue(value) {
|
|
65
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildAgentConfigSummary(agentId, config = {}) {
|
|
69
|
+
const env = normalizeConfiguredEnv(agentId, config.env);
|
|
70
|
+
switch (agentId) {
|
|
71
|
+
case 'gemini':
|
|
72
|
+
return {
|
|
73
|
+
hasGeminiApiKey: hasConfiguredValue(env.GEMINI_API_KEY),
|
|
74
|
+
hasGoogleApiKey: hasConfiguredValue(env.GOOGLE_API_KEY)
|
|
75
|
+
};
|
|
76
|
+
case 'claude':
|
|
77
|
+
return {
|
|
78
|
+
hasAnthropicApiKey: hasConfiguredValue(env.ANTHROPIC_API_KEY),
|
|
79
|
+
useVertex: env.CLAUDE_CODE_USE_VERTEX === '1',
|
|
80
|
+
hasVertexProjectId: hasConfiguredValue(
|
|
81
|
+
env.ANTHROPIC_VERTEX_PROJECT_ID
|
|
82
|
+
),
|
|
83
|
+
vertexProjectId:
|
|
84
|
+
env.ANTHROPIC_VERTEX_PROJECT_ID || '',
|
|
85
|
+
gcloudProject:
|
|
86
|
+
env.GCLOUD_PROJECT || env.GOOGLE_CLOUD_PROJECT || '',
|
|
87
|
+
cloudMlRegion: env.CLOUD_ML_REGION || '',
|
|
88
|
+
hasGoogleCredentials: hasConfiguredValue(
|
|
89
|
+
env.GOOGLE_APPLICATION_CREDENTIALS
|
|
90
|
+
)
|
|
91
|
+
};
|
|
92
|
+
case 'copilot':
|
|
93
|
+
return {
|
|
94
|
+
hasCopilotToken: hasConfiguredValue(
|
|
95
|
+
env.COPILOT_GITHUB_TOKEN
|
|
96
|
+
|| env.GH_TOKEN
|
|
97
|
+
|| env.GITHUB_TOKEN
|
|
98
|
+
)
|
|
99
|
+
};
|
|
100
|
+
default:
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let ghCopilotCliInstalledCache = null;
|
|
106
|
+
let ghAuthTokenCache = null;
|
|
107
|
+
const availabilityProbeCache = new Map();
|
|
108
|
+
|
|
109
|
+
function getCachedProbeValue(key) {
|
|
110
|
+
if (!key) return null;
|
|
111
|
+
const entry = availabilityProbeCache.get(key);
|
|
112
|
+
if (!entry) return null;
|
|
113
|
+
if (entry.expiresAt <= Date.now()) {
|
|
114
|
+
availabilityProbeCache.delete(key);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return entry.value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function setCachedProbeValue(key, value) {
|
|
121
|
+
if (!key) return value;
|
|
122
|
+
availabilityProbeCache.set(key, {
|
|
123
|
+
value,
|
|
124
|
+
expiresAt: Date.now() + DEFAULT_PROBE_CACHE_TTL_MS
|
|
125
|
+
});
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getAvailabilityCacheScopeKey(runtimeEnv = {}) {
|
|
130
|
+
return String(
|
|
131
|
+
runtimeEnv.HOME
|
|
132
|
+
|| process.env.HOME
|
|
133
|
+
|| process.cwd()
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function probeCodexAuth(runtimeEnv = {}) {
|
|
138
|
+
if (!commandExists('codex', runtimeEnv)) {
|
|
139
|
+
return { available: true, reason: '' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const cacheKey = `codex-auth:${getAvailabilityCacheScopeKey(runtimeEnv)}`;
|
|
143
|
+
const cached = getCachedProbeValue(cacheKey);
|
|
144
|
+
if (cached) {
|
|
145
|
+
return cached;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const result = spawnSync('codex', ['login', 'status'], {
|
|
149
|
+
encoding: 'utf8',
|
|
150
|
+
timeout: 1500,
|
|
151
|
+
env: withAgentPath(runtimeEnv)
|
|
152
|
+
});
|
|
153
|
+
if (result.status === 0) {
|
|
154
|
+
return setCachedProbeValue(cacheKey, {
|
|
155
|
+
available: true,
|
|
156
|
+
reason: ''
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const output = [
|
|
160
|
+
result.stdout,
|
|
161
|
+
result.stderr,
|
|
162
|
+
result.error?.message
|
|
163
|
+
].filter(Boolean).join('\n');
|
|
164
|
+
if (/not logged in|authentication required|login/i.test(output)) {
|
|
165
|
+
return setCachedProbeValue(cacheKey, {
|
|
166
|
+
available: false,
|
|
167
|
+
reason: 'Run `codex login` on this host'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return setCachedProbeValue(cacheKey, {
|
|
171
|
+
available: true,
|
|
172
|
+
reason: ''
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function probeGhAuth(runtimeEnv = {}) {
|
|
177
|
+
const explicitToken = String(
|
|
178
|
+
runtimeEnv.COPILOT_GITHUB_TOKEN
|
|
179
|
+
|| runtimeEnv.GH_TOKEN
|
|
180
|
+
|| runtimeEnv.GITHUB_TOKEN
|
|
181
|
+
|| ''
|
|
182
|
+
).trim();
|
|
183
|
+
if (explicitToken) {
|
|
184
|
+
return { available: true, reason: '' };
|
|
185
|
+
}
|
|
186
|
+
if (!commandExists('gh', runtimeEnv)) {
|
|
187
|
+
return {
|
|
188
|
+
available: false,
|
|
189
|
+
reason: 'Run `gh auth login` or set `COPILOT_GITHUB_TOKEN`'
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const cacheKey = `gh-auth:${getAvailabilityCacheScopeKey(runtimeEnv)}`;
|
|
194
|
+
const cached = getCachedProbeValue(cacheKey);
|
|
195
|
+
if (cached) {
|
|
196
|
+
return cached;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const result = spawnSync('gh', ['auth', 'status'], {
|
|
200
|
+
encoding: 'utf8',
|
|
201
|
+
timeout: 1500,
|
|
202
|
+
env: withAgentPath(runtimeEnv)
|
|
203
|
+
});
|
|
204
|
+
if (result.status === 0) {
|
|
205
|
+
return setCachedProbeValue(cacheKey, {
|
|
206
|
+
available: true,
|
|
207
|
+
reason: ''
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const output = [
|
|
211
|
+
result.stdout,
|
|
212
|
+
result.stderr,
|
|
213
|
+
result.error?.message
|
|
214
|
+
].filter(Boolean).join('\n');
|
|
215
|
+
if (/not logged|not logged into any github hosts/i.test(output)) {
|
|
216
|
+
return setCachedProbeValue(cacheKey, {
|
|
217
|
+
available: false,
|
|
218
|
+
reason: 'Run `gh auth login` or set `COPILOT_GITHUB_TOKEN`'
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return setCachedProbeValue(cacheKey, {
|
|
222
|
+
available: true,
|
|
223
|
+
reason: ''
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const DEFAULT_AVAILABILITY_PROBES = {
|
|
228
|
+
commandExists,
|
|
229
|
+
hasGhCopilotWrapper,
|
|
230
|
+
hasGhCopilotCliInstalled,
|
|
231
|
+
probeCodexAuth,
|
|
232
|
+
probeGhAuth
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
function hasGhCopilotCliInstalled() {
|
|
236
|
+
if (typeof ghCopilotCliInstalledCache === 'boolean') {
|
|
237
|
+
return ghCopilotCliInstalledCache;
|
|
238
|
+
}
|
|
239
|
+
if (!commandExists('gh')) {
|
|
240
|
+
ghCopilotCliInstalledCache = false;
|
|
241
|
+
return ghCopilotCliInstalledCache;
|
|
242
|
+
}
|
|
243
|
+
const result = spawnSync('gh', ['copilot', '--', '--version'], {
|
|
244
|
+
encoding: 'utf8',
|
|
245
|
+
env: withAgentPath(process.env)
|
|
246
|
+
});
|
|
247
|
+
ghCopilotCliInstalledCache = result.status === 0;
|
|
248
|
+
return ghCopilotCliInstalledCache;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function readGhAuthToken() {
|
|
252
|
+
if (typeof ghAuthTokenCache === 'string') {
|
|
253
|
+
return ghAuthTokenCache;
|
|
254
|
+
}
|
|
255
|
+
if (!commandExists('gh')) {
|
|
256
|
+
ghAuthTokenCache = '';
|
|
257
|
+
return ghAuthTokenCache;
|
|
258
|
+
}
|
|
259
|
+
const result = spawnSync('gh', ['auth', 'token'], {
|
|
260
|
+
encoding: 'utf8',
|
|
261
|
+
env: withAgentPath(process.env)
|
|
262
|
+
});
|
|
263
|
+
ghAuthTokenCache = result.status === 0
|
|
264
|
+
? String(result.stdout || '').trim()
|
|
265
|
+
: '';
|
|
266
|
+
return ghAuthTokenCache;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildAugmentedPath(env = {}) {
|
|
270
|
+
const delimiter = path.delimiter;
|
|
271
|
+
const home = String(
|
|
272
|
+
env.HOME
|
|
273
|
+
|| process.env.HOME
|
|
274
|
+
|| ''
|
|
275
|
+
).trim();
|
|
276
|
+
const existingEntries = String(
|
|
277
|
+
env.PATH
|
|
278
|
+
|| process.env.PATH
|
|
279
|
+
|| ''
|
|
280
|
+
)
|
|
281
|
+
.split(delimiter)
|
|
282
|
+
.map((entry) => entry.trim())
|
|
283
|
+
.filter(Boolean);
|
|
284
|
+
const extraEntries = [
|
|
285
|
+
home ? path.join(home, '.local', 'bin') : '',
|
|
286
|
+
home ? path.join(home, 'bin') : '',
|
|
287
|
+
'/opt/homebrew/bin',
|
|
288
|
+
'/opt/homebrew/sbin',
|
|
289
|
+
'/usr/local/bin'
|
|
290
|
+
].filter(Boolean);
|
|
291
|
+
const seen = new Set();
|
|
292
|
+
return [...existingEntries, ...extraEntries]
|
|
293
|
+
.filter((entry) => {
|
|
294
|
+
if (seen.has(entry)) return false;
|
|
295
|
+
seen.add(entry);
|
|
296
|
+
return true;
|
|
297
|
+
})
|
|
298
|
+
.join(delimiter);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function withAgentPath(env = {}) {
|
|
302
|
+
return {
|
|
303
|
+
...env,
|
|
304
|
+
PATH: buildAugmentedPath(env)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function mergeDefinitionEnv(definition, agentConfig = {}) {
|
|
309
|
+
const env = withAgentPath({
|
|
310
|
+
...process.env,
|
|
311
|
+
...normalizeConfiguredEnv(definition.id, agentConfig.env)
|
|
312
|
+
});
|
|
313
|
+
if (definition.id === 'copilot') {
|
|
314
|
+
const hasToken = Boolean(
|
|
315
|
+
env.COPILOT_GITHUB_TOKEN
|
|
316
|
+
|| env.GH_TOKEN
|
|
317
|
+
|| env.GITHUB_TOKEN
|
|
318
|
+
);
|
|
319
|
+
if (!hasToken) {
|
|
320
|
+
const ghToken = readGhAuthToken();
|
|
321
|
+
if (ghToken) {
|
|
322
|
+
env.GH_TOKEN = ghToken;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return env;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function hasGhCopilotWrapper() {
|
|
330
|
+
if (!commandExists('gh')) return false;
|
|
331
|
+
const result = spawnSync('gh', ['extension', 'list'], {
|
|
332
|
+
encoding: 'utf8',
|
|
333
|
+
env: withAgentPath(process.env)
|
|
334
|
+
});
|
|
335
|
+
return result.status === 0
|
|
336
|
+
&& typeof result.stdout === 'string'
|
|
337
|
+
&& result.stdout.includes('gh-copilot');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function commandExists(command, env = process.env) {
|
|
341
|
+
const checker = process.platform === 'win32' ? 'where' : 'which';
|
|
342
|
+
const result = spawnSync(checker, [command], {
|
|
343
|
+
stdio: 'ignore',
|
|
344
|
+
env: withAgentPath(env)
|
|
345
|
+
});
|
|
346
|
+
return result.status === 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function getAttachmentExtension(name = '') {
|
|
350
|
+
const extension = path.extname(String(name || '')).toLowerCase();
|
|
351
|
+
return extension.startsWith('.') ? extension.slice(1) : extension;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function normalizeAttachmentMimeType(mimeType = '') {
|
|
355
|
+
const value = String(mimeType || '').trim();
|
|
356
|
+
return value || 'application/octet-stream';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function getAttachmentKind(attachment = {}) {
|
|
360
|
+
const mimeType = normalizeAttachmentMimeType(attachment.mimeType);
|
|
361
|
+
if (mimeType.startsWith('image/')) {
|
|
362
|
+
return 'image';
|
|
363
|
+
}
|
|
364
|
+
if (
|
|
365
|
+
mimeType.startsWith('text/')
|
|
366
|
+
|| mimeType.includes('json')
|
|
367
|
+
|| mimeType.includes('xml')
|
|
368
|
+
|| mimeType.includes('yaml')
|
|
369
|
+
|| mimeType.includes('javascript')
|
|
370
|
+
|| mimeType.includes('typescript')
|
|
371
|
+
) {
|
|
372
|
+
return 'text';
|
|
373
|
+
}
|
|
374
|
+
const extension = getAttachmentExtension(attachment.name);
|
|
375
|
+
if (TEXT_ATTACHMENT_EXTENSIONS.has(extension)) {
|
|
376
|
+
return 'text';
|
|
377
|
+
}
|
|
378
|
+
return 'binary';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizePromptAttachment(attachment = {}) {
|
|
382
|
+
const name = String(attachment.name || 'attachment').trim() || 'attachment';
|
|
383
|
+
const mimeType = normalizeAttachmentMimeType(attachment.mimeType);
|
|
384
|
+
const size = Number.isFinite(attachment.size) ? attachment.size : 0;
|
|
385
|
+
const tempPath = String(attachment.tempPath || '').trim();
|
|
386
|
+
return {
|
|
387
|
+
id: String(attachment.id || crypto.randomUUID()),
|
|
388
|
+
name,
|
|
389
|
+
mimeType,
|
|
390
|
+
size,
|
|
391
|
+
tempPath,
|
|
392
|
+
kind: getAttachmentKind({ name, mimeType })
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function serializePromptAttachment(attachment = {}) {
|
|
397
|
+
const normalized = normalizePromptAttachment(attachment);
|
|
398
|
+
return {
|
|
399
|
+
id: normalized.id,
|
|
400
|
+
name: normalized.name,
|
|
401
|
+
mimeType: normalized.mimeType,
|
|
402
|
+
size: normalized.size,
|
|
403
|
+
kind: normalized.kind
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function makeBuiltInDefinitions() {
|
|
408
|
+
const hasGeminiBinary = commandExists('gemini');
|
|
409
|
+
const hasCopilotBinary = commandExists('copilot');
|
|
410
|
+
const hasGhCopilot = hasGhCopilotWrapper();
|
|
411
|
+
const definitions = [
|
|
412
|
+
{
|
|
413
|
+
id: 'gemini',
|
|
414
|
+
label: 'Gemini CLI',
|
|
415
|
+
description: 'Google Gemini CLI over ACP',
|
|
416
|
+
websiteUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
417
|
+
command: hasGeminiBinary ? 'gemini' : NPX_COMMAND,
|
|
418
|
+
args: hasGeminiBinary
|
|
419
|
+
? ['--acp']
|
|
420
|
+
: ['@google/gemini-cli@latest', '--acp'],
|
|
421
|
+
commandLabel: hasGeminiBinary
|
|
422
|
+
? 'gemini --acp'
|
|
423
|
+
: 'npx @google/gemini-cli@latest --acp'
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
id: 'codex',
|
|
427
|
+
label: 'Codex CLI',
|
|
428
|
+
description: 'Codex ACP adapter',
|
|
429
|
+
websiteUrl: 'https://openai.com/codex/',
|
|
430
|
+
command: NPX_COMMAND,
|
|
431
|
+
args: ['@zed-industries/codex-acp@latest'],
|
|
432
|
+
commandLabel: 'npx @zed-industries/codex-acp@latest'
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
id: 'claude',
|
|
436
|
+
label: 'Claude Agent',
|
|
437
|
+
description: 'Claude Code ACP adapter',
|
|
438
|
+
websiteUrl: 'https://www.anthropic.com/claude-code',
|
|
439
|
+
command: NPX_COMMAND,
|
|
440
|
+
args: ['@zed-industries/claude-code-acp@latest'],
|
|
441
|
+
commandLabel: 'npx @zed-industries/claude-code-acp@latest'
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
id: 'copilot',
|
|
445
|
+
label: 'GitHub Copilot',
|
|
446
|
+
description: 'GitHub Copilot CLI ACP server',
|
|
447
|
+
websiteUrl: 'https://docs.github.com/en/copilot/how-tos/copilot-cli',
|
|
448
|
+
command: hasCopilotBinary ? 'copilot' : 'gh',
|
|
449
|
+
args: hasCopilotBinary
|
|
450
|
+
? ['--acp', '--stdio']
|
|
451
|
+
: ['copilot', '--', '--acp', '--stdio'],
|
|
452
|
+
commandLabel: hasCopilotBinary
|
|
453
|
+
? 'copilot --acp --stdio'
|
|
454
|
+
: 'gh copilot -- --acp --stdio',
|
|
455
|
+
setupCommandLabel: hasGhCopilot
|
|
456
|
+
? 'gh copilot'
|
|
457
|
+
: 'Install GitHub Copilot CLI'
|
|
458
|
+
}
|
|
459
|
+
];
|
|
460
|
+
if (process.env.TABMINAL_ENABLE_TEST_AGENT === '1') {
|
|
461
|
+
definitions.unshift({
|
|
462
|
+
id: 'test-agent',
|
|
463
|
+
label: 'ACP Test Agent',
|
|
464
|
+
description: 'Local ACP smoke-test agent',
|
|
465
|
+
command: process.execPath,
|
|
466
|
+
args: [TEST_AGENT_PATH],
|
|
467
|
+
commandLabel: `${process.execPath} ${TEST_AGENT_PATH}`
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return definitions;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function getDefinitionAvailability(
|
|
474
|
+
definition,
|
|
475
|
+
agentConfig = {},
|
|
476
|
+
probes = DEFAULT_AVAILABILITY_PROBES
|
|
477
|
+
) {
|
|
478
|
+
const commandExistsFn = probes.commandExists || commandExists;
|
|
479
|
+
const runtimeEnv = mergeDefinitionEnv(definition, agentConfig);
|
|
480
|
+
if (!commandExistsFn(definition.command, runtimeEnv)) {
|
|
481
|
+
return {
|
|
482
|
+
available: false,
|
|
483
|
+
reason: 'not installed'
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (definition.id === 'gemini') {
|
|
488
|
+
const hasApiKey = Boolean(
|
|
489
|
+
runtimeEnv.GEMINI_API_KEY || runtimeEnv.GOOGLE_API_KEY
|
|
490
|
+
);
|
|
491
|
+
if (!hasApiKey) {
|
|
492
|
+
return {
|
|
493
|
+
available: false,
|
|
494
|
+
reason: 'API key missing'
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (definition.id === 'codex') {
|
|
500
|
+
const codexAvailability = (
|
|
501
|
+
probes.probeCodexAuth || probeCodexAuth
|
|
502
|
+
)(runtimeEnv);
|
|
503
|
+
if (!codexAvailability.available) {
|
|
504
|
+
return codexAvailability;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (
|
|
509
|
+
definition.id === 'copilot'
|
|
510
|
+
&& definition.command === 'gh'
|
|
511
|
+
) {
|
|
512
|
+
const hasWrapper = (
|
|
513
|
+
probes.hasGhCopilotWrapper || hasGhCopilotWrapper
|
|
514
|
+
)();
|
|
515
|
+
if (!hasWrapper) {
|
|
516
|
+
return {
|
|
517
|
+
available: false,
|
|
518
|
+
reason: 'Install the gh-copilot extension first'
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const hasCli = (
|
|
522
|
+
probes.hasGhCopilotCliInstalled || hasGhCopilotCliInstalled
|
|
523
|
+
)();
|
|
524
|
+
if (!hasCli) {
|
|
525
|
+
return {
|
|
526
|
+
available: false,
|
|
527
|
+
reason: 'Run gh copilot once to install Copilot CLI'
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
const ghAvailability = (probes.probeGhAuth || probeGhAuth)(runtimeEnv);
|
|
531
|
+
if (!ghAvailability.available) {
|
|
532
|
+
return ghAvailability;
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
available: true,
|
|
536
|
+
reason: ''
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
available: true,
|
|
542
|
+
reason: ''
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function formatAgentStartupError(definition, error) {
|
|
547
|
+
const rawMessage = error?.message || 'Failed to start agent';
|
|
548
|
+
if (/ENOENT|not found/i.test(rawMessage)) {
|
|
549
|
+
const label = definition?.label || 'Agent';
|
|
550
|
+
const command = definition?.commandLabel || definition?.command || '';
|
|
551
|
+
return command
|
|
552
|
+
? `${label} is not installed or not found on this host. `
|
|
553
|
+
+ `Install \`${command}\` and retry.`
|
|
554
|
+
: `${label} is not installed or not found on this host.`;
|
|
555
|
+
}
|
|
556
|
+
if (
|
|
557
|
+
definition?.id === 'claude'
|
|
558
|
+
&& /not servable in region|not available on your vertex deployment/i
|
|
559
|
+
.test(rawMessage)
|
|
560
|
+
) {
|
|
561
|
+
return 'Claude Vertex is configured with a region that cannot serve '
|
|
562
|
+
+ 'the current model. Use a supported region such as `global`, '
|
|
563
|
+
+ '`us-east5`, or `europe-west1`, then retry.';
|
|
564
|
+
}
|
|
565
|
+
if (
|
|
566
|
+
definition?.id === 'codex'
|
|
567
|
+
&& /authentication required/i.test(rawMessage)
|
|
568
|
+
) {
|
|
569
|
+
return 'Codex is not authenticated on this host. Run `codex login` '
|
|
570
|
+
+ 'for the user running Tabminal, or start Tabminal with a HOME '
|
|
571
|
+
+ 'that already contains Codex auth.';
|
|
572
|
+
}
|
|
573
|
+
if (
|
|
574
|
+
definition?.id === 'claude'
|
|
575
|
+
&& /auth|login|credential|api key|unauthorized/i.test(rawMessage)
|
|
576
|
+
) {
|
|
577
|
+
return 'Claude is not authenticated on this host. Use an existing '
|
|
578
|
+
+ 'Claude login, set ANTHROPIC_API_KEY, or configure Vertex '
|
|
579
|
+
+ 'with CLAUDE_CODE_USE_VERTEX=1, ANTHROPIC_VERTEX_PROJECT_ID, '
|
|
580
|
+
+ 'CLOUD_ML_REGION, and Google Cloud credentials before '
|
|
581
|
+
+ 'starting Tabminal.';
|
|
582
|
+
}
|
|
583
|
+
if (definition?.id === 'copilot' && /not installed/i.test(rawMessage)) {
|
|
584
|
+
return 'GitHub Copilot CLI is not installed on this host yet. Run '
|
|
585
|
+
+ '`gh copilot` once to download it, or install a standalone '
|
|
586
|
+
+ '`copilot` binary and restart Tabminal.';
|
|
587
|
+
}
|
|
588
|
+
if (
|
|
589
|
+
definition?.id === 'copilot'
|
|
590
|
+
&& /auth|login|token|unauthorized|forbidden/i.test(rawMessage)
|
|
591
|
+
) {
|
|
592
|
+
return 'GitHub Copilot is not authenticated on this host. If this '
|
|
593
|
+
+ 'backend can already see a `copilot login` or `gh auth` token '
|
|
594
|
+
+ 'it may reuse them, but `COPILOT_GITHUB_TOKEN` is the reliable '
|
|
595
|
+
+ 'headless fix in Tabminal setup.';
|
|
596
|
+
}
|
|
597
|
+
return rawMessage;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function makeRuntimeKey(agentId, cwd) {
|
|
601
|
+
return `${agentId}::${path.resolve(cwd)}`;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function makeRuntimeStoreKey(agentId, cwd, configVersion = 0) {
|
|
605
|
+
return `${makeRuntimeKey(agentId, cwd)}::cfg:${configVersion}`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function normalizeEnvList(envList) {
|
|
609
|
+
if (!Array.isArray(envList)) return {};
|
|
610
|
+
const env = {};
|
|
611
|
+
for (const item of envList) {
|
|
612
|
+
if (!item || typeof item.name !== 'string') continue;
|
|
613
|
+
env[item.name] = typeof item.value === 'string' ? item.value : '';
|
|
614
|
+
}
|
|
615
|
+
return env;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function truncateUtf8(text, byteLimit) {
|
|
619
|
+
if (!text) return '';
|
|
620
|
+
const buffer = Buffer.from(text, 'utf8');
|
|
621
|
+
if (buffer.length <= byteLimit) return text;
|
|
622
|
+
|
|
623
|
+
let slice = buffer.subarray(buffer.length - byteLimit);
|
|
624
|
+
while (slice.length > 0) {
|
|
625
|
+
const decoded = slice.toString('utf8');
|
|
626
|
+
if (!decoded.includes('\uFFFD')) {
|
|
627
|
+
return decoded;
|
|
628
|
+
}
|
|
629
|
+
slice = slice.subarray(1);
|
|
630
|
+
}
|
|
631
|
+
return '';
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export function buildTerminalSpawnRequest(request = {}) {
|
|
635
|
+
const command = String(request.command || '').trim();
|
|
636
|
+
const args = Array.isArray(request.args)
|
|
637
|
+
? request.args.filter((value) => typeof value === 'string')
|
|
638
|
+
: [];
|
|
639
|
+
if (!command) {
|
|
640
|
+
throw new Error('Terminal command is required');
|
|
641
|
+
}
|
|
642
|
+
if (args.length > 0) {
|
|
643
|
+
return {
|
|
644
|
+
command,
|
|
645
|
+
args,
|
|
646
|
+
shell: false
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const requiresShell = /[\s|&;<>()$`*?[\]{}~]/.test(command);
|
|
651
|
+
if (!requiresShell) {
|
|
652
|
+
return {
|
|
653
|
+
command,
|
|
654
|
+
args: [],
|
|
655
|
+
shell: false
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const shell = process.platform === 'win32'
|
|
660
|
+
? process.env.ComSpec || 'cmd.exe'
|
|
661
|
+
: process.env.SHELL || '/bin/sh';
|
|
662
|
+
const shellArgs = process.platform === 'win32'
|
|
663
|
+
? ['/d', '/s', '/c', command]
|
|
664
|
+
: ['-lc', command];
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
command: shell,
|
|
668
|
+
args: shellArgs,
|
|
669
|
+
shell: true
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function mergeAgentMessageText(previousText, chunkText) {
|
|
674
|
+
const previous = String(previousText || '');
|
|
675
|
+
const chunk = String(chunkText || '');
|
|
676
|
+
if (!previous) return chunk;
|
|
677
|
+
if (!chunk) return previous;
|
|
678
|
+
if (/\s$/.test(previous) || /^\s/.test(chunk)) {
|
|
679
|
+
return `${previous}${chunk}`;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const previousLast = previous.slice(-1);
|
|
683
|
+
const chunkFirst = chunk[0] || '';
|
|
684
|
+
if (
|
|
685
|
+
/[.!?`'")\]]/.test(previousLast)
|
|
686
|
+
&& /[A-Z`"'[(]/.test(chunkFirst)
|
|
687
|
+
) {
|
|
688
|
+
return `${previous}\n\n${chunk}`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return `${previous}${chunk}`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function formatTerminalDisplayCommand(request = {}, spawnRequest = {}) {
|
|
695
|
+
const explicitArgs = Array.isArray(request.args)
|
|
696
|
+
? request.args.filter((value) => typeof value === 'string')
|
|
697
|
+
: [];
|
|
698
|
+
if (explicitArgs.length > 0) {
|
|
699
|
+
return [request.command, ...explicitArgs].join(' ').trim();
|
|
700
|
+
}
|
|
701
|
+
if (typeof request.command === 'string' && request.command.trim()) {
|
|
702
|
+
return request.command.trim();
|
|
703
|
+
}
|
|
704
|
+
return [spawnRequest.command, ...(spawnRequest.args || [])]
|
|
705
|
+
.filter(Boolean)
|
|
706
|
+
.join(' ')
|
|
707
|
+
.trim();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function normalizePlanEntries(entries = []) {
|
|
711
|
+
if (!Array.isArray(entries)) return [];
|
|
712
|
+
return entries
|
|
713
|
+
.filter((entry) => entry && typeof entry === 'object')
|
|
714
|
+
.map((entry) => ({
|
|
715
|
+
content: typeof entry.content === 'string' ? entry.content : '',
|
|
716
|
+
priority: typeof entry.priority === 'string'
|
|
717
|
+
? entry.priority
|
|
718
|
+
: 'medium',
|
|
719
|
+
status: typeof entry.status === 'string'
|
|
720
|
+
? entry.status
|
|
721
|
+
: 'pending'
|
|
722
|
+
}))
|
|
723
|
+
.filter((entry) => entry.content);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function normalizeUsageWindow(item = {}) {
|
|
727
|
+
if (!item || typeof item !== 'object') return null;
|
|
728
|
+
const label = String(
|
|
729
|
+
item.label
|
|
730
|
+
|| item.name
|
|
731
|
+
|| item.window
|
|
732
|
+
|| item.bucket
|
|
733
|
+
|| ''
|
|
734
|
+
).trim();
|
|
735
|
+
const used = Number.isFinite(item.used)
|
|
736
|
+
? item.used
|
|
737
|
+
: Number.isFinite(item.consumed)
|
|
738
|
+
? item.consumed
|
|
739
|
+
: Number.isFinite(item.spent)
|
|
740
|
+
? item.spent
|
|
741
|
+
: null;
|
|
742
|
+
const size = Number.isFinite(item.size)
|
|
743
|
+
? item.size
|
|
744
|
+
: Number.isFinite(item.limit)
|
|
745
|
+
? item.limit
|
|
746
|
+
: Number.isFinite(item.max)
|
|
747
|
+
? item.max
|
|
748
|
+
: Number.isFinite(item.total)
|
|
749
|
+
? item.total
|
|
750
|
+
: null;
|
|
751
|
+
const remaining = Number.isFinite(item.remaining)
|
|
752
|
+
? item.remaining
|
|
753
|
+
: Number.isFinite(size) && Number.isFinite(used)
|
|
754
|
+
? Math.max(size - used, 0)
|
|
755
|
+
: null;
|
|
756
|
+
const resetAt = [
|
|
757
|
+
item.resetAt,
|
|
758
|
+
item.resetsAt,
|
|
759
|
+
item.nextResetAt,
|
|
760
|
+
item.resetTime,
|
|
761
|
+
item.resetDate
|
|
762
|
+
].find((value) => typeof value === 'string' && value.trim()) || '';
|
|
763
|
+
const resetDisplay = String(
|
|
764
|
+
item.resetDisplay
|
|
765
|
+
|| item.resetLabel
|
|
766
|
+
|| item.resetText
|
|
767
|
+
|| ''
|
|
768
|
+
).trim();
|
|
769
|
+
const subtitle = String(item.subtitle || item.description || '').trim();
|
|
770
|
+
if (!label && !resetAt && !resetDisplay && !subtitle) {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
label: label || 'Window',
|
|
775
|
+
used,
|
|
776
|
+
size,
|
|
777
|
+
remaining,
|
|
778
|
+
resetAt,
|
|
779
|
+
resetDisplay,
|
|
780
|
+
subtitle
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function extractUsageResetHints(meta = {}) {
|
|
785
|
+
if (!meta || typeof meta !== 'object') {
|
|
786
|
+
return {
|
|
787
|
+
resetAt: '',
|
|
788
|
+
windows: [],
|
|
789
|
+
vendorLabel: '',
|
|
790
|
+
sessionId: '',
|
|
791
|
+
summary: ''
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
const rawWindows = Array.isArray(meta.windows)
|
|
795
|
+
? meta.windows
|
|
796
|
+
: Array.isArray(meta.limits)
|
|
797
|
+
? meta.limits
|
|
798
|
+
: Array.isArray(meta.quotas)
|
|
799
|
+
? meta.quotas
|
|
800
|
+
: [];
|
|
801
|
+
const windows = rawWindows
|
|
802
|
+
.map((item) => normalizeUsageWindow(item))
|
|
803
|
+
.filter(Boolean);
|
|
804
|
+
const resetCandidates = [
|
|
805
|
+
meta.resetAt,
|
|
806
|
+
meta.resetsAt,
|
|
807
|
+
meta.nextResetAt,
|
|
808
|
+
meta.resetTime,
|
|
809
|
+
meta.resetDate
|
|
810
|
+
];
|
|
811
|
+
const resetAt = resetCandidates.find(
|
|
812
|
+
(value) => typeof value === 'string' && value.trim()
|
|
813
|
+
) || '';
|
|
814
|
+
return {
|
|
815
|
+
resetAt,
|
|
816
|
+
windows,
|
|
817
|
+
vendorLabel: String(
|
|
818
|
+
meta.vendorLabel
|
|
819
|
+
|| meta.provider
|
|
820
|
+
|| meta.providerLabel
|
|
821
|
+
|| meta.agent
|
|
822
|
+
|| ''
|
|
823
|
+
).trim(),
|
|
824
|
+
sessionId: String(
|
|
825
|
+
meta.sessionId
|
|
826
|
+
|| meta.session
|
|
827
|
+
|| meta.conversationId
|
|
828
|
+
|| ''
|
|
829
|
+
).trim(),
|
|
830
|
+
summary: String(
|
|
831
|
+
meta.summary
|
|
832
|
+
|| meta.status
|
|
833
|
+
|| meta.contextLabel
|
|
834
|
+
|| ''
|
|
835
|
+
).trim()
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function mergeUsageState(previous = {}, update = {}) {
|
|
840
|
+
const meta = extractUsageResetHints(update?._meta);
|
|
841
|
+
const next = {
|
|
842
|
+
used: Number.isFinite(update?.used)
|
|
843
|
+
? update.used
|
|
844
|
+
: Number.isFinite(previous?.used)
|
|
845
|
+
? previous.used
|
|
846
|
+
: null,
|
|
847
|
+
size: Number.isFinite(update?.size)
|
|
848
|
+
? update.size
|
|
849
|
+
: Number.isFinite(previous?.size)
|
|
850
|
+
? previous.size
|
|
851
|
+
: null,
|
|
852
|
+
cost: update?.cost || previous?.cost || null,
|
|
853
|
+
totals: update?.totals || previous?.totals || null,
|
|
854
|
+
updatedAt: new Date().toISOString(),
|
|
855
|
+
resetAt: meta.resetAt || previous?.resetAt || '',
|
|
856
|
+
windows: meta.windows.length > 0 ? meta.windows : previous?.windows || [],
|
|
857
|
+
vendorLabel: meta.vendorLabel || previous?.vendorLabel || '',
|
|
858
|
+
sessionId: meta.sessionId || previous?.sessionId || '',
|
|
859
|
+
summary: meta.summary || previous?.summary || ''
|
|
860
|
+
};
|
|
861
|
+
return next;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function serializeUsageState(usage) {
|
|
865
|
+
if (!usage) return null;
|
|
866
|
+
return {
|
|
867
|
+
used: Number.isFinite(usage.used) ? usage.used : null,
|
|
868
|
+
size: Number.isFinite(usage.size) ? usage.size : null,
|
|
869
|
+
cost: usage.cost || null,
|
|
870
|
+
totals: usage.totals || null,
|
|
871
|
+
updatedAt: typeof usage.updatedAt === 'string' ? usage.updatedAt : '',
|
|
872
|
+
resetAt: typeof usage.resetAt === 'string' ? usage.resetAt : '',
|
|
873
|
+
windows: Array.isArray(usage.windows) ? usage.windows : [],
|
|
874
|
+
vendorLabel: typeof usage.vendorLabel === 'string'
|
|
875
|
+
? usage.vendorLabel
|
|
876
|
+
: '',
|
|
877
|
+
sessionId: typeof usage.sessionId === 'string'
|
|
878
|
+
? usage.sessionId
|
|
879
|
+
: '',
|
|
880
|
+
summary: typeof usage.summary === 'string'
|
|
881
|
+
? usage.summary
|
|
882
|
+
: ''
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function normalizeToolStatusClass(status = '') {
|
|
887
|
+
const value = String(status || 'pending').toLowerCase();
|
|
888
|
+
if (value.includes('ready')) {
|
|
889
|
+
return 'ready';
|
|
890
|
+
}
|
|
891
|
+
if (value.includes('restore')) {
|
|
892
|
+
return 'running';
|
|
893
|
+
}
|
|
894
|
+
if (value.includes('disconnect')) {
|
|
895
|
+
return 'error';
|
|
896
|
+
}
|
|
897
|
+
if (
|
|
898
|
+
value.includes('complete')
|
|
899
|
+
|| value.includes('success')
|
|
900
|
+
|| value.includes('select')
|
|
901
|
+
|| value.includes('approve')
|
|
902
|
+
) {
|
|
903
|
+
return 'completed';
|
|
904
|
+
}
|
|
905
|
+
if (value.includes('cancel')) {
|
|
906
|
+
return 'cancelled';
|
|
907
|
+
}
|
|
908
|
+
if (value.includes('error') || value.includes('fail')) {
|
|
909
|
+
return 'error';
|
|
910
|
+
}
|
|
911
|
+
if (value.includes('run') || value.includes('progress')) {
|
|
912
|
+
return 'running';
|
|
913
|
+
}
|
|
914
|
+
return 'pending';
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function getToolCallTerminalIds(toolCall) {
|
|
918
|
+
if (!Array.isArray(toolCall?.content)) {
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
const ids = new Set();
|
|
922
|
+
for (const item of toolCall.content) {
|
|
923
|
+
const terminalId = String(item?.terminalId || '').trim();
|
|
924
|
+
if (item?.type === 'terminal' && terminalId) {
|
|
925
|
+
ids.add(terminalId);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return Array.from(ids);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function cloneSerializable(value, fallback) {
|
|
932
|
+
try {
|
|
933
|
+
const cloned = structuredClone(value);
|
|
934
|
+
return cloned === undefined ? fallback : cloned;
|
|
935
|
+
} catch {
|
|
936
|
+
return fallback;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function normalizePersistedTimelineOrder(value, fallback = 0) {
|
|
941
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function normalizePersistedMessage(message = {}, fallbackOrder = 0) {
|
|
945
|
+
const nextMessage = cloneSerializable(message, {}) || {};
|
|
946
|
+
nextMessage.id = typeof nextMessage.id === 'string'
|
|
947
|
+
? nextMessage.id
|
|
948
|
+
: crypto.randomUUID();
|
|
949
|
+
nextMessage.streamKey = typeof nextMessage.streamKey === 'string'
|
|
950
|
+
? nextMessage.streamKey
|
|
951
|
+
: nextMessage.id;
|
|
952
|
+
nextMessage.role = typeof nextMessage.role === 'string'
|
|
953
|
+
? nextMessage.role
|
|
954
|
+
: 'assistant';
|
|
955
|
+
nextMessage.kind = typeof nextMessage.kind === 'string'
|
|
956
|
+
? nextMessage.kind
|
|
957
|
+
: 'message';
|
|
958
|
+
nextMessage.text = typeof nextMessage.text === 'string'
|
|
959
|
+
? nextMessage.text
|
|
960
|
+
: '';
|
|
961
|
+
nextMessage.createdAt = typeof nextMessage.createdAt === 'string'
|
|
962
|
+
? nextMessage.createdAt
|
|
963
|
+
: '';
|
|
964
|
+
nextMessage.order = normalizePersistedTimelineOrder(
|
|
965
|
+
nextMessage.order,
|
|
966
|
+
fallbackOrder
|
|
967
|
+
);
|
|
968
|
+
nextMessage.attachments = Array.isArray(nextMessage.attachments)
|
|
969
|
+
? cloneSerializable(nextMessage.attachments, [])
|
|
970
|
+
: [];
|
|
971
|
+
return nextMessage;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function normalizePersistedTimelineEntry(entry = {}, fallbackOrder = 0) {
|
|
975
|
+
const nextEntry = cloneSerializable(entry, {}) || {};
|
|
976
|
+
nextEntry.createdAt = typeof nextEntry.createdAt === 'string'
|
|
977
|
+
? nextEntry.createdAt
|
|
978
|
+
: '';
|
|
979
|
+
nextEntry.order = normalizePersistedTimelineOrder(
|
|
980
|
+
nextEntry.order,
|
|
981
|
+
fallbackOrder
|
|
982
|
+
);
|
|
983
|
+
return nextEntry;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function normalizePersistedTerminalSummary(summary = {}) {
|
|
987
|
+
const nextSummary = cloneSerializable(summary, {}) || {};
|
|
988
|
+
return {
|
|
989
|
+
terminalId: typeof nextSummary.terminalId === 'string'
|
|
990
|
+
? nextSummary.terminalId
|
|
991
|
+
: '',
|
|
992
|
+
terminalSessionId: typeof nextSummary.terminalSessionId === 'string'
|
|
993
|
+
? nextSummary.terminalSessionId
|
|
994
|
+
: '',
|
|
995
|
+
command: typeof nextSummary.command === 'string'
|
|
996
|
+
? nextSummary.command
|
|
997
|
+
: '',
|
|
998
|
+
cwd: typeof nextSummary.cwd === 'string' ? nextSummary.cwd : '',
|
|
999
|
+
output: typeof nextSummary.output === 'string'
|
|
1000
|
+
? nextSummary.output
|
|
1001
|
+
: '',
|
|
1002
|
+
createdAt: typeof nextSummary.createdAt === 'string'
|
|
1003
|
+
? nextSummary.createdAt
|
|
1004
|
+
: '',
|
|
1005
|
+
updatedAt: typeof nextSummary.updatedAt === 'string'
|
|
1006
|
+
? nextSummary.updatedAt
|
|
1007
|
+
: '',
|
|
1008
|
+
released: !!nextSummary.released,
|
|
1009
|
+
running: !!nextSummary.running,
|
|
1010
|
+
exitStatus: nextSummary.exitStatus
|
|
1011
|
+
&& typeof nextSummary.exitStatus === 'object'
|
|
1012
|
+
? {
|
|
1013
|
+
exitCode: Number.isFinite(nextSummary.exitStatus.exitCode)
|
|
1014
|
+
? nextSummary.exitStatus.exitCode
|
|
1015
|
+
: null,
|
|
1016
|
+
signal: typeof nextSummary.exitStatus.signal === 'string'
|
|
1017
|
+
? nextSummary.exitStatus.signal
|
|
1018
|
+
: null
|
|
1019
|
+
}
|
|
1020
|
+
: null
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function restorePersistedTabSnapshot(tab, snapshot = {}) {
|
|
1025
|
+
const messages = Array.isArray(snapshot.messages)
|
|
1026
|
+
? snapshot.messages.map((message, index) =>
|
|
1027
|
+
normalizePersistedMessage(message, index + 1)
|
|
1028
|
+
)
|
|
1029
|
+
: [];
|
|
1030
|
+
const toolCalls = Array.isArray(snapshot.toolCalls)
|
|
1031
|
+
? snapshot.toolCalls.map((entry, index) =>
|
|
1032
|
+
normalizePersistedTimelineEntry(
|
|
1033
|
+
entry,
|
|
1034
|
+
messages.length + index + 1
|
|
1035
|
+
)
|
|
1036
|
+
)
|
|
1037
|
+
: [];
|
|
1038
|
+
const permissions = Array.isArray(snapshot.permissions)
|
|
1039
|
+
? snapshot.permissions.map((entry, index) =>
|
|
1040
|
+
normalizePersistedTimelineEntry(
|
|
1041
|
+
entry,
|
|
1042
|
+
messages.length + toolCalls.length + index + 1
|
|
1043
|
+
)
|
|
1044
|
+
)
|
|
1045
|
+
: [];
|
|
1046
|
+
const terminals = Array.isArray(snapshot.terminals)
|
|
1047
|
+
? snapshot.terminals.map((entry) =>
|
|
1048
|
+
normalizePersistedTerminalSummary(entry)
|
|
1049
|
+
).filter((entry) => entry.terminalId)
|
|
1050
|
+
: [];
|
|
1051
|
+
|
|
1052
|
+
tab.messages = messages;
|
|
1053
|
+
tab.toolCalls = new Map(
|
|
1054
|
+
toolCalls
|
|
1055
|
+
.filter((entry) => typeof entry.toolCallId === 'string')
|
|
1056
|
+
.map((entry) => [entry.toolCallId, entry])
|
|
1057
|
+
);
|
|
1058
|
+
tab.permissions = new Map(
|
|
1059
|
+
permissions
|
|
1060
|
+
.filter((entry) => typeof entry.id === 'string')
|
|
1061
|
+
.map((entry) => [
|
|
1062
|
+
entry.id,
|
|
1063
|
+
{
|
|
1064
|
+
...entry,
|
|
1065
|
+
resolve: null
|
|
1066
|
+
}
|
|
1067
|
+
])
|
|
1068
|
+
);
|
|
1069
|
+
tab.plan = normalizePlanEntries(snapshot.plan);
|
|
1070
|
+
tab.usage = serializeUsageState(snapshot.usage)
|
|
1071
|
+
? mergeUsageState(null, snapshot.usage)
|
|
1072
|
+
: null;
|
|
1073
|
+
tab.terminals = new Map(
|
|
1074
|
+
terminals.map((entry) => [entry.terminalId, entry])
|
|
1075
|
+
);
|
|
1076
|
+
tab.title = typeof snapshot.title === 'string'
|
|
1077
|
+
? snapshot.title
|
|
1078
|
+
: tab.title;
|
|
1079
|
+
tab.currentModeId = typeof snapshot.currentModeId === 'string'
|
|
1080
|
+
? snapshot.currentModeId
|
|
1081
|
+
: tab.currentModeId;
|
|
1082
|
+
tab.availableModes = Array.isArray(snapshot.availableModes)
|
|
1083
|
+
? cloneSerializable(snapshot.availableModes, [])
|
|
1084
|
+
: tab.availableModes;
|
|
1085
|
+
tab.availableCommands = Array.isArray(snapshot.availableCommands)
|
|
1086
|
+
? cloneSerializable(snapshot.availableCommands, [])
|
|
1087
|
+
: tab.availableCommands;
|
|
1088
|
+
tab.configOptions = Array.isArray(snapshot.configOptions)
|
|
1089
|
+
? cloneSerializable(snapshot.configOptions, [])
|
|
1090
|
+
: tab.configOptions;
|
|
1091
|
+
|
|
1092
|
+
const maxMessageOrder = messages.reduce(
|
|
1093
|
+
(maxOrder, entry) => Math.max(maxOrder, entry.order || 0),
|
|
1094
|
+
0
|
|
1095
|
+
);
|
|
1096
|
+
const maxToolOrder = toolCalls.reduce(
|
|
1097
|
+
(maxOrder, entry) => Math.max(maxOrder, entry.order || 0),
|
|
1098
|
+
0
|
|
1099
|
+
);
|
|
1100
|
+
const maxPermissionOrder = permissions.reduce(
|
|
1101
|
+
(maxOrder, entry) => Math.max(maxOrder, entry.order || 0),
|
|
1102
|
+
0
|
|
1103
|
+
);
|
|
1104
|
+
tab.timelineCounter = Math.max(
|
|
1105
|
+
tab.timelineCounter,
|
|
1106
|
+
maxMessageOrder,
|
|
1107
|
+
maxToolOrder,
|
|
1108
|
+
maxPermissionOrder
|
|
1109
|
+
);
|
|
1110
|
+
tab.messageCounter = Math.max(tab.messageCounter, messages.length);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
class LocalExecTerminal extends EventEmitter {
|
|
1114
|
+
constructor(request) {
|
|
1115
|
+
super();
|
|
1116
|
+
this.id = crypto.randomUUID();
|
|
1117
|
+
this.sessionId = String(request.sessionId || '');
|
|
1118
|
+
this.cwd = request.cwd || process.cwd();
|
|
1119
|
+
this.output = '';
|
|
1120
|
+
this.outputByteLimit = Math.max(
|
|
1121
|
+
1024,
|
|
1122
|
+
request.outputByteLimit || DEFAULT_TERMINAL_OUTPUT_LIMIT
|
|
1123
|
+
);
|
|
1124
|
+
this.exitStatus = null;
|
|
1125
|
+
this.closed = false;
|
|
1126
|
+
this.waiters = [];
|
|
1127
|
+
this.createdAt = new Date().toISOString();
|
|
1128
|
+
this.updatedAt = this.createdAt;
|
|
1129
|
+
|
|
1130
|
+
const env = {
|
|
1131
|
+
...process.env,
|
|
1132
|
+
...normalizeEnvList(request.env)
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
const spawnRequest = buildTerminalSpawnRequest(request);
|
|
1136
|
+
this.command = formatTerminalDisplayCommand(request, spawnRequest);
|
|
1137
|
+
|
|
1138
|
+
this.child = spawn(spawnRequest.command, spawnRequest.args, {
|
|
1139
|
+
cwd: request.cwd || process.cwd(),
|
|
1140
|
+
env,
|
|
1141
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
const append = (chunk) => {
|
|
1145
|
+
const text = Buffer.isBuffer(chunk)
|
|
1146
|
+
? chunk.toString('utf8')
|
|
1147
|
+
: String(chunk);
|
|
1148
|
+
this.output = truncateUtf8(
|
|
1149
|
+
`${this.output}${text}`,
|
|
1150
|
+
this.outputByteLimit
|
|
1151
|
+
);
|
|
1152
|
+
this.updatedAt = new Date().toISOString();
|
|
1153
|
+
this.emit('update', this.currentSummary());
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
this.child.stdout?.on('data', append);
|
|
1157
|
+
this.child.stderr?.on('data', append);
|
|
1158
|
+
this.child.on('error', (error) => {
|
|
1159
|
+
if (this.closed) return;
|
|
1160
|
+
append(error?.message || 'Terminal command failed.');
|
|
1161
|
+
this.closed = true;
|
|
1162
|
+
this.exitStatus = {
|
|
1163
|
+
exitCode: null,
|
|
1164
|
+
signal: null
|
|
1165
|
+
};
|
|
1166
|
+
this.updatedAt = new Date().toISOString();
|
|
1167
|
+
this.emit('update', this.currentSummary());
|
|
1168
|
+
for (const waiter of this.waiters) {
|
|
1169
|
+
waiter(this.exitStatus);
|
|
1170
|
+
}
|
|
1171
|
+
this.waiters.length = 0;
|
|
1172
|
+
});
|
|
1173
|
+
this.child.on('exit', (code, signal) => {
|
|
1174
|
+
this.closed = true;
|
|
1175
|
+
this.exitStatus = {
|
|
1176
|
+
exitCode: typeof code === 'number' ? code : null,
|
|
1177
|
+
signal: signal || null
|
|
1178
|
+
};
|
|
1179
|
+
this.updatedAt = new Date().toISOString();
|
|
1180
|
+
this.emit('update', this.currentSummary());
|
|
1181
|
+
for (const waiter of this.waiters) {
|
|
1182
|
+
waiter(this.exitStatus);
|
|
1183
|
+
}
|
|
1184
|
+
this.waiters.length = 0;
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
currentOutput() {
|
|
1189
|
+
return {
|
|
1190
|
+
output: this.output,
|
|
1191
|
+
exitStatus: this.exitStatus
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
currentSummary() {
|
|
1196
|
+
return {
|
|
1197
|
+
terminalId: this.id,
|
|
1198
|
+
sessionId: this.sessionId,
|
|
1199
|
+
command: this.command,
|
|
1200
|
+
cwd: this.cwd,
|
|
1201
|
+
output: this.output,
|
|
1202
|
+
exitStatus: this.exitStatus,
|
|
1203
|
+
createdAt: this.createdAt,
|
|
1204
|
+
updatedAt: this.updatedAt,
|
|
1205
|
+
running: !this.exitStatus
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
waitForExit() {
|
|
1210
|
+
if (this.exitStatus) {
|
|
1211
|
+
return Promise.resolve(this.exitStatus);
|
|
1212
|
+
}
|
|
1213
|
+
return new Promise((resolve) => {
|
|
1214
|
+
this.waiters.push(resolve);
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
kill() {
|
|
1219
|
+
if (!this.closed) {
|
|
1220
|
+
this.child.kill('SIGTERM');
|
|
1221
|
+
}
|
|
1222
|
+
return {};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
async release() {
|
|
1226
|
+
if (!this.closed) {
|
|
1227
|
+
this.child.kill('SIGTERM');
|
|
1228
|
+
await this.waitForExit().catch(() => {});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
class ManagedTerminalSession extends EventEmitter {
|
|
1234
|
+
constructor(request, terminalManager, agentMeta = {}) {
|
|
1235
|
+
super();
|
|
1236
|
+
this.id = crypto.randomUUID();
|
|
1237
|
+
this.sessionId = String(request.sessionId || '');
|
|
1238
|
+
this.cwd = request.cwd || process.cwd();
|
|
1239
|
+
this.outputByteLimit = Math.max(
|
|
1240
|
+
1024,
|
|
1241
|
+
request.outputByteLimit || DEFAULT_TERMINAL_OUTPUT_LIMIT
|
|
1242
|
+
);
|
|
1243
|
+
const env = normalizeEnvList(request.env);
|
|
1244
|
+
this.spawnRequest = buildTerminalSpawnRequest(request);
|
|
1245
|
+
this.command = formatTerminalDisplayCommand(request, this.spawnRequest);
|
|
1246
|
+
this.released = false;
|
|
1247
|
+
this.managedBy = {
|
|
1248
|
+
kind: 'agent-terminal',
|
|
1249
|
+
agentId: String(agentMeta.agentId || '').trim(),
|
|
1250
|
+
agentLabel: String(agentMeta.agentLabel || 'Agent').trim(),
|
|
1251
|
+
acpSessionId: this.sessionId,
|
|
1252
|
+
terminalId: this.id
|
|
1253
|
+
};
|
|
1254
|
+
this.terminalSession = terminalManager.createManagedSession({
|
|
1255
|
+
cwd: this.cwd,
|
|
1256
|
+
env,
|
|
1257
|
+
spawnRequest: this.spawnRequest,
|
|
1258
|
+
title: path.basename(this.spawnRequest.command || '') || 'Terminal',
|
|
1259
|
+
managed: this.managedBy
|
|
1260
|
+
});
|
|
1261
|
+
this.terminalSessionId = this.terminalSession.id;
|
|
1262
|
+
this.unsubscribe = this.terminalSession.onStateChange(() => {
|
|
1263
|
+
this.emit('update', this.currentSummary());
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
currentOutput() {
|
|
1268
|
+
return {
|
|
1269
|
+
output: truncateUtf8(
|
|
1270
|
+
this.terminalSession.history || '',
|
|
1271
|
+
this.outputByteLimit
|
|
1272
|
+
),
|
|
1273
|
+
exitStatus: this.terminalSession.exitStatus
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
currentSummary() {
|
|
1278
|
+
return {
|
|
1279
|
+
terminalId: this.id,
|
|
1280
|
+
sessionId: this.sessionId,
|
|
1281
|
+
terminalSessionId: this.terminalSessionId,
|
|
1282
|
+
command: this.command,
|
|
1283
|
+
cwd: this.terminalSession.cwd || this.cwd,
|
|
1284
|
+
output: truncateUtf8(
|
|
1285
|
+
this.terminalSession.history || '',
|
|
1286
|
+
this.outputByteLimit
|
|
1287
|
+
),
|
|
1288
|
+
exitStatus: this.terminalSession.exitStatus,
|
|
1289
|
+
createdAt: this.terminalSession.createdAt instanceof Date
|
|
1290
|
+
? this.terminalSession.createdAt.toISOString()
|
|
1291
|
+
: new Date(this.terminalSession.createdAt || Date.now())
|
|
1292
|
+
.toISOString(),
|
|
1293
|
+
updatedAt: this.terminalSession.updatedAt instanceof Date
|
|
1294
|
+
? this.terminalSession.updatedAt.toISOString()
|
|
1295
|
+
: new Date(this.terminalSession.updatedAt || Date.now())
|
|
1296
|
+
.toISOString(),
|
|
1297
|
+
running: !this.terminalSession.exitStatus,
|
|
1298
|
+
released: this.released
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
waitForExit() {
|
|
1303
|
+
return this.terminalSession.waitForExit();
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
kill() {
|
|
1307
|
+
if (!this.terminalSession.closed) {
|
|
1308
|
+
this.terminalSession.pty.kill('SIGTERM');
|
|
1309
|
+
}
|
|
1310
|
+
return {};
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async release({ destroy = true } = {}) {
|
|
1314
|
+
this.released = true;
|
|
1315
|
+
this.unsubscribe?.();
|
|
1316
|
+
this.unsubscribe = null;
|
|
1317
|
+
if (destroy) {
|
|
1318
|
+
await this.terminalSession.manager?.removeSession?.(
|
|
1319
|
+
this.terminalSession.id
|
|
1320
|
+
);
|
|
1321
|
+
this.terminalSessionId = '';
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
class AcpRuntime extends EventEmitter {
|
|
1327
|
+
constructor(definition, options = {}) {
|
|
1328
|
+
super();
|
|
1329
|
+
this.definition = definition;
|
|
1330
|
+
this.cwd = path.resolve(options.cwd || process.cwd());
|
|
1331
|
+
this.runtimeId = options.runtimeId || crypto.randomUUID();
|
|
1332
|
+
this.runtimeKey = makeRuntimeKey(definition.id, this.cwd);
|
|
1333
|
+
this.runtimeStoreKey = options.runtimeStoreKey || this.runtimeKey;
|
|
1334
|
+
this.env = options.env || process.env;
|
|
1335
|
+
this.terminalManager = options.terminalManager || null;
|
|
1336
|
+
this.idleTimeoutMs = options.idleTimeoutMs || DEFAULT_IDLE_TIMEOUT_MS;
|
|
1337
|
+
this.connection = null;
|
|
1338
|
+
this.process = null;
|
|
1339
|
+
this.started = false;
|
|
1340
|
+
this.startPromise = null;
|
|
1341
|
+
this.idleTimer = null;
|
|
1342
|
+
this.agentInfo = null;
|
|
1343
|
+
this.agentCapabilities = null;
|
|
1344
|
+
this.authMethods = [];
|
|
1345
|
+
this.tabs = new Map();
|
|
1346
|
+
this.sessionToTabId = new Map();
|
|
1347
|
+
this.terminals = new Map();
|
|
1348
|
+
this.cachedAvailableModes = [];
|
|
1349
|
+
this.cachedAvailableCommands = [];
|
|
1350
|
+
this.cachedConfigOptions = [];
|
|
1351
|
+
this.cachedModelState = null;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
#resolveAvailableModes(availableModes, existingModes = []) {
|
|
1355
|
+
if (Array.isArray(availableModes) && availableModes.length > 0) {
|
|
1356
|
+
this.cachedAvailableModes = availableModes;
|
|
1357
|
+
return availableModes;
|
|
1358
|
+
}
|
|
1359
|
+
if (Array.isArray(existingModes) && existingModes.length > 0) {
|
|
1360
|
+
return existingModes;
|
|
1361
|
+
}
|
|
1362
|
+
return this.cachedAvailableModes;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
#resolveAvailableCommands(availableCommands, existingCommands = []) {
|
|
1366
|
+
if (
|
|
1367
|
+
Array.isArray(availableCommands)
|
|
1368
|
+
&& availableCommands.length > 0
|
|
1369
|
+
) {
|
|
1370
|
+
this.cachedAvailableCommands = availableCommands;
|
|
1371
|
+
return availableCommands;
|
|
1372
|
+
}
|
|
1373
|
+
if (
|
|
1374
|
+
Array.isArray(existingCommands)
|
|
1375
|
+
&& existingCommands.length > 0
|
|
1376
|
+
) {
|
|
1377
|
+
return existingCommands;
|
|
1378
|
+
}
|
|
1379
|
+
return this.cachedAvailableCommands;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
#buildSyntheticModelConfigOption(modelState) {
|
|
1383
|
+
const availableModels = Array.isArray(modelState?.availableModels)
|
|
1384
|
+
? modelState.availableModels
|
|
1385
|
+
: [];
|
|
1386
|
+
const currentModelId = typeof modelState?.currentModelId === 'string'
|
|
1387
|
+
? modelState.currentModelId
|
|
1388
|
+
: '';
|
|
1389
|
+
if (!currentModelId || availableModels.length === 0) {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
return {
|
|
1393
|
+
id: '__tabminal_model__',
|
|
1394
|
+
name: 'Model',
|
|
1395
|
+
category: 'model',
|
|
1396
|
+
type: 'select',
|
|
1397
|
+
currentValue: currentModelId,
|
|
1398
|
+
options: availableModels.map((model) => ({
|
|
1399
|
+
value: model?.modelId || model?.id || '',
|
|
1400
|
+
name: model?.name || model?.modelId || model?.id || '',
|
|
1401
|
+
description: model?.description || ''
|
|
1402
|
+
})).filter((option) => option.value && option.name)
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
#resolveConfigOptions(
|
|
1407
|
+
configOptions,
|
|
1408
|
+
existingConfigOptions = [],
|
|
1409
|
+
modelState = null
|
|
1410
|
+
) {
|
|
1411
|
+
let nextOptions = Array.isArray(configOptions) && configOptions.length > 0
|
|
1412
|
+
? configOptions
|
|
1413
|
+
: Array.isArray(existingConfigOptions)
|
|
1414
|
+
&& existingConfigOptions.length > 0
|
|
1415
|
+
? existingConfigOptions
|
|
1416
|
+
: this.cachedConfigOptions;
|
|
1417
|
+
|
|
1418
|
+
if (modelState?.currentModelId && Array.isArray(modelState.availableModels)) {
|
|
1419
|
+
this.cachedModelState = modelState;
|
|
1420
|
+
}
|
|
1421
|
+
const syntheticModel = this.#buildSyntheticModelConfigOption(
|
|
1422
|
+
modelState || this.cachedModelState
|
|
1423
|
+
);
|
|
1424
|
+
if (syntheticModel) {
|
|
1425
|
+
const hasModelOption = Array.isArray(nextOptions) && nextOptions.some(
|
|
1426
|
+
(option) => option?.category === 'model'
|
|
1427
|
+
);
|
|
1428
|
+
if (!hasModelOption) {
|
|
1429
|
+
nextOptions = [
|
|
1430
|
+
...nextOptions.filter(
|
|
1431
|
+
(option) => option?.id !== syntheticModel.id
|
|
1432
|
+
),
|
|
1433
|
+
syntheticModel
|
|
1434
|
+
];
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (Array.isArray(nextOptions) && nextOptions.length > 0) {
|
|
1438
|
+
this.cachedConfigOptions = nextOptions;
|
|
1439
|
+
}
|
|
1440
|
+
return nextOptions;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
#buildTab({
|
|
1444
|
+
id,
|
|
1445
|
+
acpSessionId,
|
|
1446
|
+
terminalSessionId,
|
|
1447
|
+
cwd,
|
|
1448
|
+
createdAt,
|
|
1449
|
+
title = '',
|
|
1450
|
+
currentModeId = '',
|
|
1451
|
+
availableModes = [],
|
|
1452
|
+
availableCommands = [],
|
|
1453
|
+
configOptions = [],
|
|
1454
|
+
messages = [],
|
|
1455
|
+
toolCalls = [],
|
|
1456
|
+
permissions = [],
|
|
1457
|
+
plan = [],
|
|
1458
|
+
usage = null,
|
|
1459
|
+
terminals = []
|
|
1460
|
+
}) {
|
|
1461
|
+
const tab = {
|
|
1462
|
+
id,
|
|
1463
|
+
runtimeId: this.runtimeId,
|
|
1464
|
+
runtimeKey: this.runtimeKey,
|
|
1465
|
+
agentId: this.definition.id,
|
|
1466
|
+
agentLabel: this.definition.label,
|
|
1467
|
+
commandLabel: this.definition.commandLabel,
|
|
1468
|
+
terminalSessionId: terminalSessionId || '',
|
|
1469
|
+
cwd,
|
|
1470
|
+
acpSessionId,
|
|
1471
|
+
createdAt: createdAt || new Date().toISOString(),
|
|
1472
|
+
title: typeof title === 'string' ? title : '',
|
|
1473
|
+
status: 'ready',
|
|
1474
|
+
busy: false,
|
|
1475
|
+
errorMessage: '',
|
|
1476
|
+
messages: [],
|
|
1477
|
+
toolCalls: new Map(),
|
|
1478
|
+
permissions: new Map(),
|
|
1479
|
+
syntheticStreams: new Map(),
|
|
1480
|
+
syntheticStreamTurn: 0,
|
|
1481
|
+
pendingUserEcho: null,
|
|
1482
|
+
currentModeId,
|
|
1483
|
+
availableModes,
|
|
1484
|
+
availableCommands,
|
|
1485
|
+
configOptions,
|
|
1486
|
+
plan: [],
|
|
1487
|
+
usage: null,
|
|
1488
|
+
terminals: new Map(),
|
|
1489
|
+
clients: new Set(),
|
|
1490
|
+
messageCounter: 0,
|
|
1491
|
+
timelineCounter: 0
|
|
1492
|
+
};
|
|
1493
|
+
restorePersistedTabSnapshot(tab, {
|
|
1494
|
+
title,
|
|
1495
|
+
currentModeId,
|
|
1496
|
+
availableModes,
|
|
1497
|
+
availableCommands,
|
|
1498
|
+
configOptions,
|
|
1499
|
+
messages,
|
|
1500
|
+
toolCalls,
|
|
1501
|
+
permissions,
|
|
1502
|
+
plan,
|
|
1503
|
+
usage,
|
|
1504
|
+
terminals
|
|
1505
|
+
});
|
|
1506
|
+
return tab;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async start() {
|
|
1510
|
+
if (this.started) return;
|
|
1511
|
+
if (this.startPromise) return this.startPromise;
|
|
1512
|
+
|
|
1513
|
+
this.startPromise = this.#startInternal();
|
|
1514
|
+
try {
|
|
1515
|
+
await this.startPromise;
|
|
1516
|
+
} finally {
|
|
1517
|
+
this.startPromise = null;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
async #startInternal() {
|
|
1522
|
+
const child = spawn(this.definition.command, this.definition.args, {
|
|
1523
|
+
cwd: this.cwd,
|
|
1524
|
+
env: this.env,
|
|
1525
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
this.process = child;
|
|
1529
|
+
let startupSettled = false;
|
|
1530
|
+
let rejectStartup = null;
|
|
1531
|
+
const startupError = new Promise((_, reject) => {
|
|
1532
|
+
rejectStartup = reject;
|
|
1533
|
+
});
|
|
1534
|
+
const spawned = new Promise((resolve) => {
|
|
1535
|
+
child.once('spawn', resolve);
|
|
1536
|
+
});
|
|
1537
|
+
child.stderr?.on('data', (chunk) => {
|
|
1538
|
+
const text = Buffer.isBuffer(chunk)
|
|
1539
|
+
? chunk.toString('utf8')
|
|
1540
|
+
: String(chunk);
|
|
1541
|
+
this.emit('runtime_log', {
|
|
1542
|
+
runtimeId: this.runtimeId,
|
|
1543
|
+
level: 'warn',
|
|
1544
|
+
message: text.trim()
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
1547
|
+
child.on('error', (error) => {
|
|
1548
|
+
if (!startupSettled) {
|
|
1549
|
+
startupSettled = true;
|
|
1550
|
+
rejectStartup?.(error);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
const detail = {
|
|
1554
|
+
runtimeId: this.runtimeId,
|
|
1555
|
+
code: null,
|
|
1556
|
+
signal: null,
|
|
1557
|
+
error: error?.message || String(error)
|
|
1558
|
+
};
|
|
1559
|
+
for (const tab of this.tabs.values()) {
|
|
1560
|
+
tab.status = 'disconnected';
|
|
1561
|
+
tab.busy = false;
|
|
1562
|
+
tab.errorMessage = formatAgentStartupError(
|
|
1563
|
+
this.definition,
|
|
1564
|
+
error
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
this.emit('runtime_exit', detail);
|
|
1568
|
+
});
|
|
1569
|
+
child.on('exit', (code, signal) => {
|
|
1570
|
+
if (!startupSettled) {
|
|
1571
|
+
startupSettled = true;
|
|
1572
|
+
rejectStartup?.(
|
|
1573
|
+
new Error(
|
|
1574
|
+
signal
|
|
1575
|
+
? `Agent runtime exited (${signal}) before `
|
|
1576
|
+
+ 'initialization completed.'
|
|
1577
|
+
: `Agent runtime exited (${code ?? 'unknown'}) `
|
|
1578
|
+
+ 'before initialization completed.'
|
|
1579
|
+
)
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
startupSettled = true;
|
|
1583
|
+
const detail = {
|
|
1584
|
+
runtimeId: this.runtimeId,
|
|
1585
|
+
code: typeof code === 'number' ? code : null,
|
|
1586
|
+
signal: signal || null
|
|
1587
|
+
};
|
|
1588
|
+
for (const tab of this.tabs.values()) {
|
|
1589
|
+
tab.status = 'disconnected';
|
|
1590
|
+
tab.busy = false;
|
|
1591
|
+
tab.errorMessage = detail.signal
|
|
1592
|
+
? `Agent runtime exited (${detail.signal}).`
|
|
1593
|
+
: `Agent runtime exited (${detail.code ?? 'unknown'}).`;
|
|
1594
|
+
}
|
|
1595
|
+
this.emit('runtime_exit', detail);
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
await Promise.race([
|
|
1599
|
+
spawned,
|
|
1600
|
+
startupError
|
|
1601
|
+
]);
|
|
1602
|
+
|
|
1603
|
+
const input = Writable.toWeb(child.stdin);
|
|
1604
|
+
const output = Readable.toWeb(child.stdout);
|
|
1605
|
+
const stream = acp.ndJsonStream(input, output);
|
|
1606
|
+
this.connection = new acp.ClientSideConnection(
|
|
1607
|
+
() => ({
|
|
1608
|
+
sessionUpdate: (params) => this.#handleSessionUpdate(params),
|
|
1609
|
+
requestPermission: (params) => this.#requestPermission(params),
|
|
1610
|
+
readTextFile: (params) => this.#readTextFile(params),
|
|
1611
|
+
writeTextFile: (params) => this.#writeTextFile(params),
|
|
1612
|
+
createTerminal: (params) => this.#createTerminal(params),
|
|
1613
|
+
terminalOutput: (params) => this.#terminalOutput(params),
|
|
1614
|
+
releaseTerminal: (params) => this.#releaseTerminal(params),
|
|
1615
|
+
waitForTerminalExit: (params) =>
|
|
1616
|
+
this.#waitForTerminalExit(params),
|
|
1617
|
+
killTerminal: (params) => this.#killTerminal(params)
|
|
1618
|
+
}),
|
|
1619
|
+
stream
|
|
1620
|
+
);
|
|
1621
|
+
|
|
1622
|
+
const result = await Promise.race([
|
|
1623
|
+
this.connection.initialize({
|
|
1624
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
1625
|
+
clientInfo: {
|
|
1626
|
+
name: 'Tabminal',
|
|
1627
|
+
version: pkg.version
|
|
1628
|
+
},
|
|
1629
|
+
clientCapabilities: {
|
|
1630
|
+
fs: {
|
|
1631
|
+
readTextFile: true,
|
|
1632
|
+
writeTextFile: true
|
|
1633
|
+
},
|
|
1634
|
+
terminal: true
|
|
1635
|
+
}
|
|
1636
|
+
}),
|
|
1637
|
+
startupError
|
|
1638
|
+
]);
|
|
1639
|
+
startupSettled = true;
|
|
1640
|
+
|
|
1641
|
+
this.agentInfo = result.agentInfo || null;
|
|
1642
|
+
this.agentCapabilities = result.agentCapabilities || null;
|
|
1643
|
+
this.authMethods = result.authMethods || [];
|
|
1644
|
+
this.started = true;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
scheduleIdleShutdown(onIdle) {
|
|
1648
|
+
if (this.tabs.size > 0) return;
|
|
1649
|
+
clearTimeout(this.idleTimer);
|
|
1650
|
+
this.idleTimer = setTimeout(() => {
|
|
1651
|
+
this.idleTimer = null;
|
|
1652
|
+
void onIdle();
|
|
1653
|
+
}, this.idleTimeoutMs);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
clearIdleShutdown() {
|
|
1657
|
+
if (this.idleTimer) {
|
|
1658
|
+
clearTimeout(this.idleTimer);
|
|
1659
|
+
this.idleTimer = null;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async createTab(meta) {
|
|
1664
|
+
await this.start();
|
|
1665
|
+
this.clearIdleShutdown();
|
|
1666
|
+
const response = await this.connection.newSession({
|
|
1667
|
+
cwd: meta.cwd,
|
|
1668
|
+
mcpServers: []
|
|
1669
|
+
});
|
|
1670
|
+
const availableModes = this.#resolveAvailableModes(
|
|
1671
|
+
response.modes?.availableModes
|
|
1672
|
+
);
|
|
1673
|
+
const availableCommands = this.#resolveAvailableCommands(
|
|
1674
|
+
response.availableCommands
|
|
1675
|
+
);
|
|
1676
|
+
const configOptions = this.#resolveConfigOptions(
|
|
1677
|
+
response.configOptions,
|
|
1678
|
+
[],
|
|
1679
|
+
response.models
|
|
1680
|
+
);
|
|
1681
|
+
const tab = this.#buildTab({
|
|
1682
|
+
id: meta.id,
|
|
1683
|
+
acpSessionId: response.sessionId,
|
|
1684
|
+
terminalSessionId: meta.terminalSessionId,
|
|
1685
|
+
cwd: meta.cwd,
|
|
1686
|
+
title: response.title || '',
|
|
1687
|
+
currentModeId: response.modes?.currentModeId || '',
|
|
1688
|
+
availableModes,
|
|
1689
|
+
availableCommands,
|
|
1690
|
+
configOptions
|
|
1691
|
+
});
|
|
1692
|
+
if (meta.modeId && typeof this.connection.setSessionMode === 'function') {
|
|
1693
|
+
try {
|
|
1694
|
+
const modeResponse = await this.connection.setSessionMode({
|
|
1695
|
+
sessionId: tab.acpSessionId,
|
|
1696
|
+
modeId: meta.modeId
|
|
1697
|
+
});
|
|
1698
|
+
tab.currentModeId = modeResponse?.currentModeId
|
|
1699
|
+
|| modeResponse?.modeId
|
|
1700
|
+
|| meta.modeId;
|
|
1701
|
+
if (Array.isArray(modeResponse?.availableModes)) {
|
|
1702
|
+
tab.availableModes = modeResponse.availableModes;
|
|
1703
|
+
}
|
|
1704
|
+
} catch {
|
|
1705
|
+
// Ignore unsupported mode changes during initial tab creation.
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
await this.#hydrateFreshSessionMetadata(tab);
|
|
1709
|
+
this.tabs.set(tab.id, tab);
|
|
1710
|
+
this.sessionToTabId.set(tab.acpSessionId, tab.id);
|
|
1711
|
+
return this.serializeTab(tab);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
async restoreTab(meta) {
|
|
1715
|
+
await this.start();
|
|
1716
|
+
this.clearIdleShutdown();
|
|
1717
|
+
|
|
1718
|
+
if (
|
|
1719
|
+
!this.agentCapabilities?.loadSession
|
|
1720
|
+
|| typeof this.connection.loadSession !== 'function'
|
|
1721
|
+
) {
|
|
1722
|
+
throw new Error(
|
|
1723
|
+
`${this.definition.label} does not support session restore`
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
const tab = this.#buildTab({
|
|
1728
|
+
id: meta.id,
|
|
1729
|
+
acpSessionId: meta.acpSessionId,
|
|
1730
|
+
terminalSessionId: meta.terminalSessionId,
|
|
1731
|
+
cwd: meta.cwd,
|
|
1732
|
+
createdAt: meta.createdAt,
|
|
1733
|
+
title: meta.title || '',
|
|
1734
|
+
currentModeId: meta.currentModeId || '',
|
|
1735
|
+
availableModes: meta.availableModes || [],
|
|
1736
|
+
availableCommands: meta.availableCommands || [],
|
|
1737
|
+
configOptions: meta.configOptions || [],
|
|
1738
|
+
messages: meta.messages || [],
|
|
1739
|
+
toolCalls: meta.toolCalls || [],
|
|
1740
|
+
permissions: meta.permissions || [],
|
|
1741
|
+
plan: meta.plan || [],
|
|
1742
|
+
usage: meta.usage || null,
|
|
1743
|
+
terminals: meta.terminals || []
|
|
1744
|
+
});
|
|
1745
|
+
tab.status = 'restoring';
|
|
1746
|
+
tab.busy = true;
|
|
1747
|
+
|
|
1748
|
+
this.tabs.set(tab.id, tab);
|
|
1749
|
+
this.sessionToTabId.set(tab.acpSessionId, tab.id);
|
|
1750
|
+
|
|
1751
|
+
try {
|
|
1752
|
+
const response = await this.connection.loadSession({
|
|
1753
|
+
cwd: meta.cwd,
|
|
1754
|
+
sessionId: meta.acpSessionId,
|
|
1755
|
+
mcpServers: []
|
|
1756
|
+
});
|
|
1757
|
+
const restoredSessionId = response?.sessionId || meta.acpSessionId;
|
|
1758
|
+
if (restoredSessionId !== tab.acpSessionId) {
|
|
1759
|
+
this.sessionToTabId.delete(tab.acpSessionId);
|
|
1760
|
+
tab.acpSessionId = restoredSessionId;
|
|
1761
|
+
this.sessionToTabId.set(tab.acpSessionId, tab.id);
|
|
1762
|
+
}
|
|
1763
|
+
if (typeof response?.title === 'string') {
|
|
1764
|
+
tab.title = response.title;
|
|
1765
|
+
}
|
|
1766
|
+
tab.currentModeId = response?.modes?.currentModeId || '';
|
|
1767
|
+
tab.availableModes = this.#resolveAvailableModes(
|
|
1768
|
+
response?.modes?.availableModes,
|
|
1769
|
+
tab.availableModes
|
|
1770
|
+
);
|
|
1771
|
+
tab.availableCommands = this.#resolveAvailableCommands(
|
|
1772
|
+
response?.availableCommands,
|
|
1773
|
+
tab.availableCommands
|
|
1774
|
+
);
|
|
1775
|
+
tab.configOptions = this.#resolveConfigOptions(
|
|
1776
|
+
response?.configOptions,
|
|
1777
|
+
tab.configOptions,
|
|
1778
|
+
response?.models
|
|
1779
|
+
);
|
|
1780
|
+
tab.status = 'ready';
|
|
1781
|
+
tab.busy = false;
|
|
1782
|
+
tab.errorMessage = '';
|
|
1783
|
+
return this.serializeTab(tab);
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
this.tabs.delete(tab.id);
|
|
1786
|
+
this.sessionToTabId.delete(tab.acpSessionId);
|
|
1787
|
+
throw error;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
#markTabDirty(tab) {
|
|
1792
|
+
if (!tab) return;
|
|
1793
|
+
this.emit('tab_dirty', {
|
|
1794
|
+
tabId: tab.id,
|
|
1795
|
+
sessionId: tab.acpSessionId
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
serializeTab(tab) {
|
|
1800
|
+
return {
|
|
1801
|
+
id: tab.id,
|
|
1802
|
+
runtimeId: tab.runtimeId,
|
|
1803
|
+
runtimeKey: tab.runtimeKey,
|
|
1804
|
+
acpSessionId: tab.acpSessionId,
|
|
1805
|
+
agentId: tab.agentId,
|
|
1806
|
+
agentLabel: tab.agentLabel,
|
|
1807
|
+
commandLabel: tab.commandLabel,
|
|
1808
|
+
title: tab.title || '',
|
|
1809
|
+
terminalSessionId: tab.terminalSessionId,
|
|
1810
|
+
cwd: tab.cwd,
|
|
1811
|
+
createdAt: tab.createdAt,
|
|
1812
|
+
status: tab.status,
|
|
1813
|
+
busy: tab.busy,
|
|
1814
|
+
errorMessage: tab.errorMessage,
|
|
1815
|
+
currentModeId: tab.currentModeId,
|
|
1816
|
+
availableModes: tab.availableModes,
|
|
1817
|
+
availableCommands: tab.availableCommands,
|
|
1818
|
+
configOptions: tab.configOptions,
|
|
1819
|
+
messages: tab.messages,
|
|
1820
|
+
toolCalls: Array.from(tab.toolCalls.values()),
|
|
1821
|
+
permissions: Array.from(tab.permissions.values()).map((item) => ({
|
|
1822
|
+
id: item.id,
|
|
1823
|
+
sessionId: item.sessionId,
|
|
1824
|
+
toolCall: item.toolCall,
|
|
1825
|
+
options: item.options,
|
|
1826
|
+
status: item.status,
|
|
1827
|
+
createdAt: item.createdAt || '',
|
|
1828
|
+
order: item.order,
|
|
1829
|
+
selectedOptionId: item.selectedOptionId || ''
|
|
1830
|
+
})),
|
|
1831
|
+
plan: Array.isArray(tab.plan) ? tab.plan : [],
|
|
1832
|
+
usage: serializeUsageState(tab.usage),
|
|
1833
|
+
terminals: Array.from(tab.terminals.values())
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
#getPromptCapabilities() {
|
|
1838
|
+
return this.agentCapabilities?.promptCapabilities || {};
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
async #buildAttachmentPromptBlock(attachment) {
|
|
1842
|
+
const normalized = normalizePromptAttachment(attachment);
|
|
1843
|
+
const fileUri = pathToFileURL(normalized.tempPath).toString();
|
|
1844
|
+
const capabilities = this.#getPromptCapabilities();
|
|
1845
|
+
|
|
1846
|
+
if (normalized.kind === 'image' && capabilities.image) {
|
|
1847
|
+
const data = await fs.readFile(normalized.tempPath);
|
|
1848
|
+
return {
|
|
1849
|
+
type: 'image',
|
|
1850
|
+
data: data.toString('base64'),
|
|
1851
|
+
mimeType: normalized.mimeType,
|
|
1852
|
+
uri: fileUri
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
if (capabilities.embeddedContext) {
|
|
1857
|
+
if (normalized.kind === 'text') {
|
|
1858
|
+
const text = await fs.readFile(normalized.tempPath, 'utf8');
|
|
1859
|
+
return {
|
|
1860
|
+
type: 'resource',
|
|
1861
|
+
resource: {
|
|
1862
|
+
text,
|
|
1863
|
+
uri: fileUri,
|
|
1864
|
+
mimeType: normalized.mimeType
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
const data = await fs.readFile(normalized.tempPath);
|
|
1870
|
+
return {
|
|
1871
|
+
type: 'resource',
|
|
1872
|
+
resource: {
|
|
1873
|
+
blob: data.toString('base64'),
|
|
1874
|
+
uri: fileUri,
|
|
1875
|
+
mimeType: normalized.mimeType
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
return {
|
|
1881
|
+
type: 'resource_link',
|
|
1882
|
+
name: normalized.name,
|
|
1883
|
+
title: normalized.name,
|
|
1884
|
+
uri: fileUri,
|
|
1885
|
+
mimeType: normalized.mimeType,
|
|
1886
|
+
size: normalized.size || null
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async #buildPromptBlocks(text, attachments = []) {
|
|
1891
|
+
const blocks = [];
|
|
1892
|
+
for (const attachment of attachments) {
|
|
1893
|
+
blocks.push(await this.#buildAttachmentPromptBlock(attachment));
|
|
1894
|
+
}
|
|
1895
|
+
if (text) {
|
|
1896
|
+
blocks.push({
|
|
1897
|
+
type: 'text',
|
|
1898
|
+
text
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
return blocks;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
async #cleanupPromptAttachments(attachments = []) {
|
|
1905
|
+
const paths = attachments
|
|
1906
|
+
.map((attachment) => String(attachment?.tempPath || '').trim())
|
|
1907
|
+
.filter(Boolean);
|
|
1908
|
+
await Promise.allSettled(paths.map((filePath) =>
|
|
1909
|
+
fs.rm(filePath, { force: true })
|
|
1910
|
+
));
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
attachSocket(tabId, socket) {
|
|
1914
|
+
const tab = this.tabs.get(tabId);
|
|
1915
|
+
if (!tab) {
|
|
1916
|
+
socket.close();
|
|
1917
|
+
return false;
|
|
1918
|
+
}
|
|
1919
|
+
tab.clients.add(socket);
|
|
1920
|
+
socket.send(JSON.stringify({
|
|
1921
|
+
type: 'snapshot',
|
|
1922
|
+
tab: this.serializeTab(tab)
|
|
1923
|
+
}));
|
|
1924
|
+
socket.on('close', () => {
|
|
1925
|
+
tab.clients.delete(socket);
|
|
1926
|
+
});
|
|
1927
|
+
return true;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
async sendPrompt(tabId, text, attachments = []) {
|
|
1931
|
+
const tab = this.tabs.get(tabId);
|
|
1932
|
+
if (!tab) {
|
|
1933
|
+
throw new Error('Agent tab not found');
|
|
1934
|
+
}
|
|
1935
|
+
if (tab.busy) {
|
|
1936
|
+
throw new Error('Agent tab is already running');
|
|
1937
|
+
}
|
|
1938
|
+
const promptText = typeof text === 'string' ? text : '';
|
|
1939
|
+
const promptAttachments = Array.isArray(attachments)
|
|
1940
|
+
? attachments.map((attachment) => normalizePromptAttachment(attachment))
|
|
1941
|
+
: [];
|
|
1942
|
+
const promptBlocks = await this.#buildPromptBlocks(
|
|
1943
|
+
promptText,
|
|
1944
|
+
promptAttachments
|
|
1945
|
+
);
|
|
1946
|
+
tab.errorMessage = '';
|
|
1947
|
+
tab.busy = true;
|
|
1948
|
+
tab.status = 'running';
|
|
1949
|
+
this.#advanceSyntheticStreamTurn(tab);
|
|
1950
|
+
this.#appendMessage(tab, {
|
|
1951
|
+
role: 'user',
|
|
1952
|
+
kind: 'message',
|
|
1953
|
+
text: promptText,
|
|
1954
|
+
streamKey: crypto.randomUUID(),
|
|
1955
|
+
attachments: promptAttachments.map((attachment) =>
|
|
1956
|
+
serializePromptAttachment(attachment)
|
|
1957
|
+
)
|
|
1958
|
+
});
|
|
1959
|
+
tab.pendingUserEcho = promptText
|
|
1960
|
+
? {
|
|
1961
|
+
text: promptText,
|
|
1962
|
+
matched: 0
|
|
1963
|
+
}
|
|
1964
|
+
: null;
|
|
1965
|
+
this.#broadcast(tab, {
|
|
1966
|
+
type: 'status',
|
|
1967
|
+
status: tab.status,
|
|
1968
|
+
busy: tab.busy,
|
|
1969
|
+
errorMessage: ''
|
|
1970
|
+
});
|
|
1971
|
+
this.#markTabDirty(tab);
|
|
1972
|
+
|
|
1973
|
+
const promptPromise = this.connection.prompt({
|
|
1974
|
+
sessionId: tab.acpSessionId,
|
|
1975
|
+
prompt: promptBlocks
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
void promptPromise.then(async (response) => {
|
|
1979
|
+
if (!this.tabs.has(tabId)) return;
|
|
1980
|
+
tab.busy = false;
|
|
1981
|
+
tab.status = 'ready';
|
|
1982
|
+
this.#settleStaleToolCalls(
|
|
1983
|
+
tab,
|
|
1984
|
+
response?.stopReason === 'cancelled'
|
|
1985
|
+
? 'cancelled'
|
|
1986
|
+
: 'completed'
|
|
1987
|
+
);
|
|
1988
|
+
if (response?.usage) {
|
|
1989
|
+
tab.usage = mergeUsageState(tab.usage, {
|
|
1990
|
+
totals: response.usage
|
|
1991
|
+
});
|
|
1992
|
+
this.#broadcast(tab, {
|
|
1993
|
+
type: 'usage_state',
|
|
1994
|
+
usage: serializeUsageState(tab.usage)
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
tab.syntheticStreams.clear();
|
|
1998
|
+
tab.pendingUserEcho = null;
|
|
1999
|
+
const hydratedChanges = await this.#hydrateFreshSessionMetadata(tab);
|
|
2000
|
+
this.#broadcastHydratedSessionMetadata(tab, hydratedChanges);
|
|
2001
|
+
this.#broadcast(tab, {
|
|
2002
|
+
type: 'complete',
|
|
2003
|
+
stopReason: response.stopReason,
|
|
2004
|
+
status: tab.status,
|
|
2005
|
+
busy: false
|
|
2006
|
+
});
|
|
2007
|
+
this.#markTabDirty(tab);
|
|
2008
|
+
}).catch((error) => {
|
|
2009
|
+
if (!this.tabs.has(tabId)) return;
|
|
2010
|
+
tab.busy = false;
|
|
2011
|
+
tab.status = 'error';
|
|
2012
|
+
this.#settleStaleToolCalls(tab, 'error');
|
|
2013
|
+
tab.errorMessage = formatAgentStartupError(
|
|
2014
|
+
tab.definition,
|
|
2015
|
+
error
|
|
2016
|
+
);
|
|
2017
|
+
tab.syntheticStreams.clear();
|
|
2018
|
+
tab.pendingUserEcho = null;
|
|
2019
|
+
this.#broadcast(tab, {
|
|
2020
|
+
type: 'status',
|
|
2021
|
+
status: tab.status,
|
|
2022
|
+
busy: false,
|
|
2023
|
+
errorMessage: tab.errorMessage
|
|
2024
|
+
});
|
|
2025
|
+
this.#markTabDirty(tab);
|
|
2026
|
+
}).finally(() => {
|
|
2027
|
+
void this.#cleanupPromptAttachments(promptAttachments);
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
async cancel(tabId) {
|
|
2032
|
+
const tab = this.tabs.get(tabId);
|
|
2033
|
+
if (!tab) {
|
|
2034
|
+
throw new Error('Agent tab not found');
|
|
2035
|
+
}
|
|
2036
|
+
if (!tab.busy) return;
|
|
2037
|
+
|
|
2038
|
+
for (const permission of tab.permissions.values()) {
|
|
2039
|
+
if (permission.status !== 'pending' || !permission.resolve) {
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
permission.status = 'cancelled';
|
|
2043
|
+
permission.resolve({
|
|
2044
|
+
outcome: {
|
|
2045
|
+
outcome: 'cancelled'
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
await this.connection.cancel({
|
|
2050
|
+
sessionId: tab.acpSessionId
|
|
2051
|
+
});
|
|
2052
|
+
this.#markTabDirty(tab);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
async resolvePermission(tabId, permissionId, optionId) {
|
|
2056
|
+
const tab = this.tabs.get(tabId);
|
|
2057
|
+
if (!tab) {
|
|
2058
|
+
throw new Error('Agent tab not found');
|
|
2059
|
+
}
|
|
2060
|
+
const permission = tab.permissions.get(permissionId);
|
|
2061
|
+
if (!permission) {
|
|
2062
|
+
throw new Error('Permission request not found');
|
|
2063
|
+
}
|
|
2064
|
+
permission.status = optionId ? 'selected' : 'cancelled';
|
|
2065
|
+
permission.selectedOptionId = optionId || '';
|
|
2066
|
+
if (permission.resolve) {
|
|
2067
|
+
permission.resolve({
|
|
2068
|
+
outcome: optionId
|
|
2069
|
+
? { outcome: 'selected', optionId }
|
|
2070
|
+
: { outcome: 'cancelled' }
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
permission.resolve = null;
|
|
2074
|
+
this.#broadcast(tab, {
|
|
2075
|
+
type: 'permission_resolved',
|
|
2076
|
+
permissionId,
|
|
2077
|
+
status: permission.status,
|
|
2078
|
+
selectedOptionId: permission.selectedOptionId
|
|
2079
|
+
});
|
|
2080
|
+
this.#markTabDirty(tab);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
async setMode(tabId, modeId) {
|
|
2084
|
+
const tab = this.tabs.get(tabId);
|
|
2085
|
+
if (!tab) {
|
|
2086
|
+
throw new Error('Agent tab not found');
|
|
2087
|
+
}
|
|
2088
|
+
if (!modeId || typeof modeId !== 'string') {
|
|
2089
|
+
throw new Error('Mode ID is required');
|
|
2090
|
+
}
|
|
2091
|
+
if (typeof this.connection.setSessionMode !== 'function') {
|
|
2092
|
+
throw new Error('Agent does not support mode switching');
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
const response = await this.connection.setSessionMode({
|
|
2096
|
+
sessionId: tab.acpSessionId,
|
|
2097
|
+
modeId
|
|
2098
|
+
});
|
|
2099
|
+
tab.currentModeId = response?.currentModeId
|
|
2100
|
+
|| response?.modeId
|
|
2101
|
+
|| modeId;
|
|
2102
|
+
tab.availableModes = this.#resolveAvailableModes(
|
|
2103
|
+
response?.availableModes,
|
|
2104
|
+
tab.availableModes
|
|
2105
|
+
);
|
|
2106
|
+
this.#broadcast(tab, {
|
|
2107
|
+
type: 'session_update',
|
|
2108
|
+
update: {
|
|
2109
|
+
sessionUpdate: 'current_mode_update',
|
|
2110
|
+
currentModeId: tab.currentModeId
|
|
2111
|
+
},
|
|
2112
|
+
tab: {
|
|
2113
|
+
title: tab.title,
|
|
2114
|
+
currentModeId: tab.currentModeId,
|
|
2115
|
+
availableModes: tab.availableModes,
|
|
2116
|
+
availableCommands: tab.availableCommands,
|
|
2117
|
+
configOptions: tab.configOptions
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
this.#markTabDirty(tab);
|
|
2121
|
+
return this.serializeTab(tab);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
async setConfigOption(tabId, configId, valueId) {
|
|
2125
|
+
const tab = this.tabs.get(tabId);
|
|
2126
|
+
if (!tab) {
|
|
2127
|
+
throw new Error('Agent tab not found');
|
|
2128
|
+
}
|
|
2129
|
+
if (!configId || typeof configId !== 'string') {
|
|
2130
|
+
throw new Error('Config ID is required');
|
|
2131
|
+
}
|
|
2132
|
+
if (!valueId || typeof valueId !== 'string') {
|
|
2133
|
+
throw new Error('Config value is required');
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
if (
|
|
2137
|
+
configId === '__tabminal_model__'
|
|
2138
|
+
&& typeof this.connection.unstable_setSessionModel === 'function'
|
|
2139
|
+
) {
|
|
2140
|
+
await this.connection.unstable_setSessionModel({
|
|
2141
|
+
sessionId: tab.acpSessionId,
|
|
2142
|
+
modelId: valueId
|
|
2143
|
+
});
|
|
2144
|
+
tab.configOptions = this.#resolveConfigOptions(
|
|
2145
|
+
tab.configOptions.map((option) => (
|
|
2146
|
+
option?.id === configId
|
|
2147
|
+
? { ...option, currentValue: valueId }
|
|
2148
|
+
: option
|
|
2149
|
+
)),
|
|
2150
|
+
tab.configOptions
|
|
2151
|
+
);
|
|
2152
|
+
} else {
|
|
2153
|
+
if (typeof this.connection.setSessionConfigOption !== 'function') {
|
|
2154
|
+
throw new Error(
|
|
2155
|
+
'Agent does not support session configuration changes'
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
const response = await this.connection.setSessionConfigOption({
|
|
2159
|
+
sessionId: tab.acpSessionId,
|
|
2160
|
+
configId,
|
|
2161
|
+
value: valueId
|
|
2162
|
+
});
|
|
2163
|
+
tab.configOptions = this.#resolveConfigOptions(
|
|
2164
|
+
response?.configOptions,
|
|
2165
|
+
tab.configOptions
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
this.#broadcast(tab, {
|
|
2170
|
+
type: 'session_update',
|
|
2171
|
+
update: {
|
|
2172
|
+
sessionUpdate: 'config_option_update',
|
|
2173
|
+
configOptions: tab.configOptions
|
|
2174
|
+
},
|
|
2175
|
+
tab: {
|
|
2176
|
+
title: tab.title,
|
|
2177
|
+
currentModeId: tab.currentModeId,
|
|
2178
|
+
availableModes: tab.availableModes,
|
|
2179
|
+
availableCommands: tab.availableCommands,
|
|
2180
|
+
configOptions: tab.configOptions
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
this.#markTabDirty(tab);
|
|
2184
|
+
return this.serializeTab(tab);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
async closeTab(tabId) {
|
|
2188
|
+
const tab = this.tabs.get(tabId);
|
|
2189
|
+
if (!tab) return;
|
|
2190
|
+
|
|
2191
|
+
if (tab.busy) {
|
|
2192
|
+
try {
|
|
2193
|
+
await this.cancel(tabId);
|
|
2194
|
+
} catch {
|
|
2195
|
+
// Ignore cancellation failures during close.
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
if (this.connection.unstable_closeSession) {
|
|
2200
|
+
try {
|
|
2201
|
+
await this.connection.unstable_closeSession({
|
|
2202
|
+
sessionId: tab.acpSessionId
|
|
2203
|
+
});
|
|
2204
|
+
} catch {
|
|
2205
|
+
// Ignore unsupported or failing close-session behavior.
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
for (const permission of tab.permissions.values()) {
|
|
2210
|
+
if (permission.resolve) {
|
|
2211
|
+
permission.resolve({
|
|
2212
|
+
outcome: {
|
|
2213
|
+
outcome: 'cancelled'
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
permission.resolve = null;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
this.tabs.delete(tabId);
|
|
2221
|
+
this.sessionToTabId.delete(tab.acpSessionId);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
async dispose() {
|
|
2225
|
+
clearTimeout(this.idleTimer);
|
|
2226
|
+
this.idleTimer = null;
|
|
2227
|
+
for (const terminal of this.terminals.values()) {
|
|
2228
|
+
await terminal.release({ destroy: true }).catch(() => {});
|
|
2229
|
+
}
|
|
2230
|
+
this.terminals.clear();
|
|
2231
|
+
for (const tabId of Array.from(this.tabs.keys())) {
|
|
2232
|
+
await this.closeTab(tabId);
|
|
2233
|
+
}
|
|
2234
|
+
if (this.process && !this.process.killed) {
|
|
2235
|
+
this.process.kill('SIGTERM');
|
|
2236
|
+
}
|
|
2237
|
+
await this.connection?.closed.catch(() => {});
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
async #handleSessionUpdate(params) {
|
|
2241
|
+
const tab = this.#getTabBySession(params.sessionId);
|
|
2242
|
+
if (!tab) return;
|
|
2243
|
+
const update = params.update;
|
|
2244
|
+
let broadcastUpdate = update;
|
|
2245
|
+
let didChange = false;
|
|
2246
|
+
|
|
2247
|
+
switch (update.sessionUpdate) {
|
|
2248
|
+
case 'agent_message_chunk':
|
|
2249
|
+
this.#appendContentChunk(tab, update, 'assistant', 'message');
|
|
2250
|
+
didChange = true;
|
|
2251
|
+
break;
|
|
2252
|
+
case 'agent_thought_chunk':
|
|
2253
|
+
this.#appendContentChunk(tab, update, 'assistant', 'thought');
|
|
2254
|
+
didChange = true;
|
|
2255
|
+
break;
|
|
2256
|
+
case 'user_message_chunk':
|
|
2257
|
+
this.#appendContentChunk(tab, update, 'user', 'message');
|
|
2258
|
+
didChange = true;
|
|
2259
|
+
break;
|
|
2260
|
+
case 'tool_call': {
|
|
2261
|
+
this.#advanceSyntheticStreamTurn(tab);
|
|
2262
|
+
const nextToolCall = {
|
|
2263
|
+
...update,
|
|
2264
|
+
createdAt: new Date().toISOString(),
|
|
2265
|
+
order: this.#nextTimelineOrder(tab)
|
|
2266
|
+
};
|
|
2267
|
+
tab.toolCalls.set(update.toolCallId, nextToolCall);
|
|
2268
|
+
broadcastUpdate = nextToolCall;
|
|
2269
|
+
didChange = true;
|
|
2270
|
+
break;
|
|
2271
|
+
}
|
|
2272
|
+
case 'tool_call_update': {
|
|
2273
|
+
this.#advanceSyntheticStreamTurn(tab);
|
|
2274
|
+
const previous = tab.toolCalls.get(update.toolCallId) || {
|
|
2275
|
+
toolCallId: update.toolCallId,
|
|
2276
|
+
title: '',
|
|
2277
|
+
status: 'pending',
|
|
2278
|
+
createdAt: new Date().toISOString(),
|
|
2279
|
+
order: this.#nextTimelineOrder(tab)
|
|
2280
|
+
};
|
|
2281
|
+
const nextToolCall = {
|
|
2282
|
+
...previous,
|
|
2283
|
+
...update
|
|
2284
|
+
};
|
|
2285
|
+
tab.toolCalls.set(update.toolCallId, nextToolCall);
|
|
2286
|
+
broadcastUpdate = nextToolCall;
|
|
2287
|
+
didChange = true;
|
|
2288
|
+
break;
|
|
2289
|
+
}
|
|
2290
|
+
case 'current_mode_update':
|
|
2291
|
+
tab.currentModeId = update.currentModeId || update.modeId || '';
|
|
2292
|
+
didChange = true;
|
|
2293
|
+
break;
|
|
2294
|
+
case 'available_commands_update':
|
|
2295
|
+
tab.availableCommands = this.#resolveAvailableCommands(
|
|
2296
|
+
update.availableCommands,
|
|
2297
|
+
tab.availableCommands
|
|
2298
|
+
);
|
|
2299
|
+
didChange = true;
|
|
2300
|
+
break;
|
|
2301
|
+
case 'config_option_update':
|
|
2302
|
+
tab.configOptions = this.#resolveConfigOptions(
|
|
2303
|
+
update.configOptions,
|
|
2304
|
+
tab.configOptions
|
|
2305
|
+
);
|
|
2306
|
+
didChange = true;
|
|
2307
|
+
break;
|
|
2308
|
+
case 'session_info_update':
|
|
2309
|
+
if (typeof update.title === 'string') {
|
|
2310
|
+
tab.title = update.title;
|
|
2311
|
+
} else if (update.title === null) {
|
|
2312
|
+
tab.title = '';
|
|
2313
|
+
}
|
|
2314
|
+
didChange = true;
|
|
2315
|
+
break;
|
|
2316
|
+
case 'plan':
|
|
2317
|
+
tab.plan = normalizePlanEntries(update.entries);
|
|
2318
|
+
didChange = true;
|
|
2319
|
+
break;
|
|
2320
|
+
case 'usage_update':
|
|
2321
|
+
tab.usage = mergeUsageState(tab.usage, update);
|
|
2322
|
+
didChange = true;
|
|
2323
|
+
break;
|
|
2324
|
+
default:
|
|
2325
|
+
break;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
this.#broadcast(tab, {
|
|
2329
|
+
type: 'session_update',
|
|
2330
|
+
update: broadcastUpdate,
|
|
2331
|
+
tab: {
|
|
2332
|
+
title: tab.title,
|
|
2333
|
+
currentModeId: tab.currentModeId,
|
|
2334
|
+
availableModes: tab.availableModes,
|
|
2335
|
+
availableCommands: tab.availableCommands,
|
|
2336
|
+
configOptions: tab.configOptions
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
2339
|
+
if (didChange) {
|
|
2340
|
+
this.#markTabDirty(tab);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
#appendContentChunk(tab, update, role, kind) {
|
|
2345
|
+
const content = update.content;
|
|
2346
|
+
const text = content.type === 'text'
|
|
2347
|
+
? (content.text || '')
|
|
2348
|
+
: `[${content.type}]`;
|
|
2349
|
+
if (role === 'user' && kind === 'message' && this.#consumeUserEcho(tab, text)) {
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
const streamKey = this.#getStreamKey(tab, update, role, kind);
|
|
2353
|
+
const last = tab.messages[tab.messages.length - 1] || null;
|
|
2354
|
+
|
|
2355
|
+
if (
|
|
2356
|
+
last
|
|
2357
|
+
&& last.streamKey === streamKey
|
|
2358
|
+
&& last.role === role
|
|
2359
|
+
&& last.kind === kind
|
|
2360
|
+
) {
|
|
2361
|
+
const nextText = mergeAgentMessageText(last.text, text);
|
|
2362
|
+
const appendedText = nextText.slice(last.text.length);
|
|
2363
|
+
last.text = nextText;
|
|
2364
|
+
this.#broadcast(tab, {
|
|
2365
|
+
type: 'message_chunk',
|
|
2366
|
+
streamKey,
|
|
2367
|
+
role,
|
|
2368
|
+
kind,
|
|
2369
|
+
text: appendedText
|
|
2370
|
+
});
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
if (!update.messageId) {
|
|
2375
|
+
tab.messageCounter += 1;
|
|
2376
|
+
}
|
|
2377
|
+
const message = {
|
|
2378
|
+
id: crypto.randomUUID(),
|
|
2379
|
+
streamKey,
|
|
2380
|
+
role,
|
|
2381
|
+
kind,
|
|
2382
|
+
text,
|
|
2383
|
+
createdAt: new Date().toISOString(),
|
|
2384
|
+
order: this.#nextTimelineOrder(tab)
|
|
2385
|
+
};
|
|
2386
|
+
tab.messages.push(message);
|
|
2387
|
+
this.#broadcast(tab, {
|
|
2388
|
+
type: 'message_open',
|
|
2389
|
+
message
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
#consumeUserEcho(tab, text) {
|
|
2394
|
+
const pending = tab.pendingUserEcho;
|
|
2395
|
+
if (!pending || !text) {
|
|
2396
|
+
return false;
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
const remaining = pending.text.slice(pending.matched);
|
|
2400
|
+
if (!remaining.startsWith(text)) {
|
|
2401
|
+
tab.pendingUserEcho = null;
|
|
2402
|
+
return false;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
pending.matched += text.length;
|
|
2406
|
+
if (pending.matched >= pending.text.length) {
|
|
2407
|
+
tab.pendingUserEcho = null;
|
|
2408
|
+
}
|
|
2409
|
+
return true;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
#appendMessage(tab, message) {
|
|
2413
|
+
const entry = {
|
|
2414
|
+
id: crypto.randomUUID(),
|
|
2415
|
+
role: message.role,
|
|
2416
|
+
kind: message.kind,
|
|
2417
|
+
text: message.text,
|
|
2418
|
+
streamKey: message.streamKey || crypto.randomUUID(),
|
|
2419
|
+
createdAt: new Date().toISOString(),
|
|
2420
|
+
order: this.#nextTimelineOrder(tab),
|
|
2421
|
+
attachments: Array.isArray(message.attachments)
|
|
2422
|
+
? message.attachments.map((attachment) =>
|
|
2423
|
+
serializePromptAttachment(attachment)
|
|
2424
|
+
)
|
|
2425
|
+
: []
|
|
2426
|
+
};
|
|
2427
|
+
tab.messages.push(entry);
|
|
2428
|
+
this.#broadcast(tab, {
|
|
2429
|
+
type: 'message_open',
|
|
2430
|
+
message: entry
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
async #requestPermission(params) {
|
|
2435
|
+
const tab = this.#getTabBySession(params.sessionId);
|
|
2436
|
+
if (!tab) {
|
|
2437
|
+
return {
|
|
2438
|
+
outcome: {
|
|
2439
|
+
outcome: 'cancelled'
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
this.#advanceSyntheticStreamTurn(tab);
|
|
2444
|
+
|
|
2445
|
+
const permissionId = crypto.randomUUID();
|
|
2446
|
+
const request = {
|
|
2447
|
+
id: permissionId,
|
|
2448
|
+
sessionId: params.sessionId,
|
|
2449
|
+
toolCall: params.toolCall,
|
|
2450
|
+
options: params.options,
|
|
2451
|
+
status: 'pending',
|
|
2452
|
+
createdAt: new Date().toISOString(),
|
|
2453
|
+
order: this.#nextTimelineOrder(tab),
|
|
2454
|
+
selectedOptionId: '',
|
|
2455
|
+
resolve: null
|
|
2456
|
+
};
|
|
2457
|
+
tab.permissions.set(permissionId, request);
|
|
2458
|
+
|
|
2459
|
+
this.#broadcast(tab, {
|
|
2460
|
+
type: 'permission_request',
|
|
2461
|
+
permission: {
|
|
2462
|
+
id: request.id,
|
|
2463
|
+
sessionId: request.sessionId,
|
|
2464
|
+
toolCall: request.toolCall,
|
|
2465
|
+
options: request.options,
|
|
2466
|
+
status: request.status,
|
|
2467
|
+
createdAt: request.createdAt,
|
|
2468
|
+
order: request.order,
|
|
2469
|
+
selectedOptionId: request.selectedOptionId
|
|
2470
|
+
}
|
|
2471
|
+
});
|
|
2472
|
+
this.#markTabDirty(tab);
|
|
2473
|
+
|
|
2474
|
+
return new Promise((resolve) => {
|
|
2475
|
+
request.resolve = resolve;
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
async #hydrateFreshSessionMetadata(tab) {
|
|
2480
|
+
const previous = {
|
|
2481
|
+
title: tab.title || '',
|
|
2482
|
+
currentModeId: tab.currentModeId || '',
|
|
2483
|
+
availableModes: JSON.stringify(tab.availableModes || []),
|
|
2484
|
+
availableCommands: JSON.stringify(tab.availableCommands || []),
|
|
2485
|
+
configOptions: JSON.stringify(tab.configOptions || [])
|
|
2486
|
+
};
|
|
2487
|
+
const needsHydration = (
|
|
2488
|
+
!Array.isArray(tab.availableCommands)
|
|
2489
|
+
|| tab.availableCommands.length === 0
|
|
2490
|
+
|| !Array.isArray(tab.configOptions)
|
|
2491
|
+
|| tab.configOptions.length === 0
|
|
2492
|
+
);
|
|
2493
|
+
if (
|
|
2494
|
+
!needsHydration
|
|
2495
|
+
|| !this.agentCapabilities?.loadSession
|
|
2496
|
+
|| typeof this.connection.loadSession !== 'function'
|
|
2497
|
+
) {
|
|
2498
|
+
return null;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
try {
|
|
2502
|
+
const response = await this.connection.loadSession({
|
|
2503
|
+
cwd: tab.cwd,
|
|
2504
|
+
sessionId: tab.acpSessionId,
|
|
2505
|
+
mcpServers: []
|
|
2506
|
+
});
|
|
2507
|
+
const restoredSessionId = response?.sessionId || tab.acpSessionId;
|
|
2508
|
+
if (restoredSessionId !== tab.acpSessionId) {
|
|
2509
|
+
this.sessionToTabId.delete(tab.acpSessionId);
|
|
2510
|
+
tab.acpSessionId = restoredSessionId;
|
|
2511
|
+
this.sessionToTabId.set(tab.acpSessionId, tab.id);
|
|
2512
|
+
}
|
|
2513
|
+
if (typeof response?.title === 'string') {
|
|
2514
|
+
tab.title = response.title;
|
|
2515
|
+
}
|
|
2516
|
+
if (response?.modes?.currentModeId) {
|
|
2517
|
+
tab.currentModeId = response.modes.currentModeId;
|
|
2518
|
+
}
|
|
2519
|
+
tab.availableModes = this.#resolveAvailableModes(
|
|
2520
|
+
response?.modes?.availableModes,
|
|
2521
|
+
tab.availableModes
|
|
2522
|
+
);
|
|
2523
|
+
tab.availableCommands = this.#resolveAvailableCommands(
|
|
2524
|
+
response?.availableCommands,
|
|
2525
|
+
tab.availableCommands
|
|
2526
|
+
);
|
|
2527
|
+
tab.configOptions = this.#resolveConfigOptions(
|
|
2528
|
+
response?.configOptions,
|
|
2529
|
+
tab.configOptions,
|
|
2530
|
+
response?.models
|
|
2531
|
+
);
|
|
2532
|
+
return {
|
|
2533
|
+
titleChanged: previous.title !== (tab.title || ''),
|
|
2534
|
+
modeChanged: previous.currentModeId !== (tab.currentModeId || ''),
|
|
2535
|
+
modesChanged: previous.availableModes
|
|
2536
|
+
!== JSON.stringify(tab.availableModes || []),
|
|
2537
|
+
commandsChanged: previous.availableCommands
|
|
2538
|
+
!== JSON.stringify(tab.availableCommands || []),
|
|
2539
|
+
configChanged: previous.configOptions
|
|
2540
|
+
!== JSON.stringify(tab.configOptions || [])
|
|
2541
|
+
};
|
|
2542
|
+
} catch {
|
|
2543
|
+
// Ignore metadata hydration failures for fresh sessions.
|
|
2544
|
+
return null;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
#broadcastHydratedSessionMetadata(tab, changes) {
|
|
2549
|
+
if (!changes) return;
|
|
2550
|
+
const tabMeta = {
|
|
2551
|
+
title: tab.title,
|
|
2552
|
+
currentModeId: tab.currentModeId,
|
|
2553
|
+
availableModes: tab.availableModes,
|
|
2554
|
+
availableCommands: tab.availableCommands,
|
|
2555
|
+
configOptions: tab.configOptions
|
|
2556
|
+
};
|
|
2557
|
+
if (changes.titleChanged) {
|
|
2558
|
+
this.#broadcast(tab, {
|
|
2559
|
+
type: 'session_update',
|
|
2560
|
+
update: {
|
|
2561
|
+
sessionUpdate: 'session_info_update',
|
|
2562
|
+
title: tab.title || null
|
|
2563
|
+
},
|
|
2564
|
+
tab: tabMeta
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
if (changes.modeChanged || changes.modesChanged) {
|
|
2568
|
+
this.#broadcast(tab, {
|
|
2569
|
+
type: 'session_update',
|
|
2570
|
+
update: {
|
|
2571
|
+
sessionUpdate: 'current_mode_update',
|
|
2572
|
+
currentModeId: tab.currentModeId || '',
|
|
2573
|
+
availableModes: tab.availableModes
|
|
2574
|
+
},
|
|
2575
|
+
tab: tabMeta
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
if (changes.commandsChanged) {
|
|
2579
|
+
this.#broadcast(tab, {
|
|
2580
|
+
type: 'session_update',
|
|
2581
|
+
update: {
|
|
2582
|
+
sessionUpdate: 'available_commands_update',
|
|
2583
|
+
availableCommands: tab.availableCommands
|
|
2584
|
+
},
|
|
2585
|
+
tab: tabMeta
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
if (changes.configChanged) {
|
|
2589
|
+
this.#broadcast(tab, {
|
|
2590
|
+
type: 'session_update',
|
|
2591
|
+
update: {
|
|
2592
|
+
sessionUpdate: 'config_option_update',
|
|
2593
|
+
configOptions: tab.configOptions
|
|
2594
|
+
},
|
|
2595
|
+
tab: tabMeta
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
async #readTextFile(params) {
|
|
2601
|
+
const content = await fs.readFile(params.path, 'utf8');
|
|
2602
|
+
return { content };
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
async #writeTextFile(params) {
|
|
2606
|
+
await fs.writeFile(params.path, params.content, 'utf8');
|
|
2607
|
+
return {};
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
async #createTerminal(params) {
|
|
2611
|
+
const tab = this.#getTabBySession(params.sessionId);
|
|
2612
|
+
const terminal = this.terminalManager
|
|
2613
|
+
? new ManagedTerminalSession(params, this.terminalManager, {
|
|
2614
|
+
agentId: tab?.agentId || this.definition.id,
|
|
2615
|
+
agentLabel: tab?.agentLabel || this.definition.label
|
|
2616
|
+
})
|
|
2617
|
+
: new LocalExecTerminal(params);
|
|
2618
|
+
this.terminals.set(terminal.id, terminal);
|
|
2619
|
+
if (tab) {
|
|
2620
|
+
const syncSummary = (summary) => {
|
|
2621
|
+
tab.terminals.set(terminal.id, {
|
|
2622
|
+
...summary,
|
|
2623
|
+
terminalId: terminal.id
|
|
2624
|
+
});
|
|
2625
|
+
this.#broadcast(tab, {
|
|
2626
|
+
type: 'terminal_update',
|
|
2627
|
+
terminal: tab.terminals.get(terminal.id)
|
|
2628
|
+
});
|
|
2629
|
+
};
|
|
2630
|
+
syncSummary(terminal.currentSummary());
|
|
2631
|
+
terminal.on('update', syncSummary);
|
|
2632
|
+
}
|
|
2633
|
+
return { terminalId: terminal.id };
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
async releaseManagedTerminalSession(
|
|
2637
|
+
terminalSessionId,
|
|
2638
|
+
options = {}
|
|
2639
|
+
) {
|
|
2640
|
+
const targetSessionId = String(terminalSessionId || '').trim();
|
|
2641
|
+
if (!targetSessionId) return false;
|
|
2642
|
+
for (const [terminalId, terminal] of this.terminals.entries()) {
|
|
2643
|
+
if (terminal?.terminalSessionId !== targetSessionId) continue;
|
|
2644
|
+
await terminal.release({ destroy: options.destroy !== false });
|
|
2645
|
+
if (options.destroy !== false) {
|
|
2646
|
+
this.terminals.delete(terminalId);
|
|
2647
|
+
}
|
|
2648
|
+
this.#syncTerminalSummary(terminal.sessionId, terminal, {
|
|
2649
|
+
released: true
|
|
2650
|
+
});
|
|
2651
|
+
return true;
|
|
2652
|
+
}
|
|
2653
|
+
return false;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
async #terminalOutput(params) {
|
|
2657
|
+
const terminal = this.terminals.get(params.terminalId);
|
|
2658
|
+
if (!terminal) {
|
|
2659
|
+
throw new Error('Terminal not found');
|
|
2660
|
+
}
|
|
2661
|
+
const response = terminal.currentOutput();
|
|
2662
|
+
this.#syncTerminalSummary(params.sessionId, terminal);
|
|
2663
|
+
return response;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
async #waitForTerminalExit(params) {
|
|
2667
|
+
const terminal = this.terminals.get(params.terminalId);
|
|
2668
|
+
if (!terminal) {
|
|
2669
|
+
throw new Error('Terminal not found');
|
|
2670
|
+
}
|
|
2671
|
+
const response = await terminal.waitForExit();
|
|
2672
|
+
this.#syncTerminalSummary(params.sessionId, terminal);
|
|
2673
|
+
return response;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
async #killTerminal(params) {
|
|
2677
|
+
const terminal = this.terminals.get(params.terminalId);
|
|
2678
|
+
if (!terminal) {
|
|
2679
|
+
throw new Error('Terminal not found');
|
|
2680
|
+
}
|
|
2681
|
+
const response = await terminal.kill();
|
|
2682
|
+
this.#syncTerminalSummary(params.sessionId, terminal);
|
|
2683
|
+
return response;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
async #releaseTerminal(params) {
|
|
2687
|
+
const terminal = this.terminals.get(params.terminalId);
|
|
2688
|
+
if (!terminal) {
|
|
2689
|
+
return {};
|
|
2690
|
+
}
|
|
2691
|
+
await terminal.release({ destroy: true });
|
|
2692
|
+
this.terminals.delete(params.terminalId);
|
|
2693
|
+
this.#syncTerminalSummary(params.sessionId, terminal, {
|
|
2694
|
+
released: true,
|
|
2695
|
+
terminalSessionId: ''
|
|
2696
|
+
});
|
|
2697
|
+
return {};
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
#syncTerminalSummary(sessionId, terminal, overrides = {}) {
|
|
2701
|
+
const tab = this.#getTabBySession(sessionId || terminal.sessionId);
|
|
2702
|
+
if (!tab) return;
|
|
2703
|
+
tab.terminals.set(terminal.id, {
|
|
2704
|
+
...terminal.currentSummary(),
|
|
2705
|
+
...overrides,
|
|
2706
|
+
terminalId: terminal.id
|
|
2707
|
+
});
|
|
2708
|
+
if (!tab.busy) {
|
|
2709
|
+
this.#settleStaleToolCalls(
|
|
2710
|
+
tab,
|
|
2711
|
+
tab.status === 'error' ? 'error' : 'completed'
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
this.#broadcast(tab, {
|
|
2715
|
+
type: 'terminal_update',
|
|
2716
|
+
terminal: tab.terminals.get(terminal.id)
|
|
2717
|
+
});
|
|
2718
|
+
this.#markTabDirty(tab);
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
#toolCallHasRunningTerminal(tab, toolCall) {
|
|
2722
|
+
for (const terminalId of getToolCallTerminalIds(toolCall)) {
|
|
2723
|
+
const terminal = tab.terminals.get(terminalId);
|
|
2724
|
+
if (terminal?.running) {
|
|
2725
|
+
return true;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
return false;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
#settleStaleToolCalls(tab, nextStatus = 'completed') {
|
|
2732
|
+
let didChange = false;
|
|
2733
|
+
for (const [toolCallId, toolCall] of tab.toolCalls.entries()) {
|
|
2734
|
+
const statusClass = normalizeToolStatusClass(toolCall?.status);
|
|
2735
|
+
if (
|
|
2736
|
+
statusClass !== 'pending'
|
|
2737
|
+
&& statusClass !== 'running'
|
|
2738
|
+
) {
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
if (this.#toolCallHasRunningTerminal(tab, toolCall)) {
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
const nextToolCall = {
|
|
2745
|
+
...toolCall,
|
|
2746
|
+
status: nextStatus
|
|
2747
|
+
};
|
|
2748
|
+
tab.toolCalls.set(toolCallId, nextToolCall);
|
|
2749
|
+
this.#broadcast(tab, {
|
|
2750
|
+
type: 'session_update',
|
|
2751
|
+
update: {
|
|
2752
|
+
sessionUpdate: 'tool_call_update',
|
|
2753
|
+
...nextToolCall
|
|
2754
|
+
},
|
|
2755
|
+
tab: {
|
|
2756
|
+
title: tab.title,
|
|
2757
|
+
currentModeId: tab.currentModeId,
|
|
2758
|
+
availableModes: tab.availableModes,
|
|
2759
|
+
availableCommands: tab.availableCommands,
|
|
2760
|
+
configOptions: tab.configOptions
|
|
2761
|
+
}
|
|
2762
|
+
});
|
|
2763
|
+
didChange = true;
|
|
2764
|
+
}
|
|
2765
|
+
if (didChange) {
|
|
2766
|
+
this.#markTabDirty(tab);
|
|
2767
|
+
}
|
|
2768
|
+
return didChange;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
#getTabBySession(sessionId) {
|
|
2772
|
+
const tabId = this.sessionToTabId.get(sessionId);
|
|
2773
|
+
return tabId ? this.tabs.get(tabId) || null : null;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
#broadcast(tab, payload) {
|
|
2777
|
+
const message = JSON.stringify(payload);
|
|
2778
|
+
for (const socket of tab.clients) {
|
|
2779
|
+
if (socket.readyState === 1) {
|
|
2780
|
+
socket.send(message);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
#getStreamKey(tab, update, role, kind) {
|
|
2786
|
+
if (update.messageId) {
|
|
2787
|
+
return update.messageId;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
const bucketKey = `${update.sessionUpdate}:${role}:${kind}`;
|
|
2791
|
+
let streamKey = tab.syntheticStreams.get(bucketKey) || '';
|
|
2792
|
+
if (!streamKey) {
|
|
2793
|
+
streamKey = [
|
|
2794
|
+
'synthetic',
|
|
2795
|
+
tab.syntheticStreamTurn,
|
|
2796
|
+
update.sessionUpdate,
|
|
2797
|
+
role,
|
|
2798
|
+
kind
|
|
2799
|
+
].join(':');
|
|
2800
|
+
tab.syntheticStreams.set(bucketKey, streamKey);
|
|
2801
|
+
}
|
|
2802
|
+
return streamKey;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
#advanceSyntheticStreamTurn(tab) {
|
|
2806
|
+
tab.syntheticStreamTurn += 1;
|
|
2807
|
+
tab.syntheticStreams.clear();
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
#nextTimelineOrder(tab) {
|
|
2811
|
+
tab.timelineCounter += 1;
|
|
2812
|
+
return tab.timelineCounter;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
export class AcpManager {
|
|
2817
|
+
constructor(options = {}) {
|
|
2818
|
+
this.idleTimeoutMs = options.idleTimeoutMs || DEFAULT_IDLE_TIMEOUT_MS;
|
|
2819
|
+
this.terminalManager = options.terminalManager || null;
|
|
2820
|
+
this.runtimeFactory = options.runtimeFactory || (
|
|
2821
|
+
(definition, runtimeOptions) =>
|
|
2822
|
+
new AcpRuntime(definition, runtimeOptions)
|
|
2823
|
+
);
|
|
2824
|
+
this.definitions = makeBuiltInDefinitions();
|
|
2825
|
+
this.runtimes = new Map();
|
|
2826
|
+
this.tabs = new Map();
|
|
2827
|
+
this.loadTabs = options.loadTabs || persistence.loadAgentTabs;
|
|
2828
|
+
this.saveTabs = options.saveTabs || persistence.saveAgentTabs;
|
|
2829
|
+
this.loadConfigs = options.loadConfigs || persistence.loadAgentConfigs;
|
|
2830
|
+
this.saveConfigs = options.saveConfigs || persistence.saveAgentConfigs;
|
|
2831
|
+
this.persistenceChain = Promise.resolve();
|
|
2832
|
+
this.transcriptPersistDelayMs = options.transcriptPersistDelayMs
|
|
2833
|
+
|| DEFAULT_TRANSCRIPT_PERSIST_DELAY_MS;
|
|
2834
|
+
this.persistTabsTimer = null;
|
|
2835
|
+
this.disposing = false;
|
|
2836
|
+
this.restoring = false;
|
|
2837
|
+
this.agentConfigs = {};
|
|
2838
|
+
this.agentConfigVersions = new Map();
|
|
2839
|
+
this.definitionAvailabilityOverrides = new Map();
|
|
2840
|
+
this.availabilityOverrideTtlMs =
|
|
2841
|
+
options.availabilityOverrideTtlMs
|
|
2842
|
+
|| DEFAULT_AVAILABILITY_OVERRIDE_TTL_MS;
|
|
2843
|
+
this.availabilityProbes = options.availabilityProbes
|
|
2844
|
+
|| DEFAULT_AVAILABILITY_PROBES;
|
|
2845
|
+
this.configLoaded = false;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
#getDefinitionAvailabilityOverride(agentId) {
|
|
2849
|
+
const entry = this.definitionAvailabilityOverrides.get(agentId);
|
|
2850
|
+
if (!entry) return null;
|
|
2851
|
+
if (entry.expiresAt <= Date.now()) {
|
|
2852
|
+
this.definitionAvailabilityOverrides.delete(agentId);
|
|
2853
|
+
return null;
|
|
2854
|
+
}
|
|
2855
|
+
return entry;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
#setDefinitionAvailabilityOverride(agentId, availability = {}) {
|
|
2859
|
+
this.definitionAvailabilityOverrides.set(agentId, {
|
|
2860
|
+
available: Boolean(availability.available),
|
|
2861
|
+
reason: String(availability.reason || ''),
|
|
2862
|
+
expiresAt: Date.now() + this.availabilityOverrideTtlMs
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
#clearDefinitionAvailabilityOverride(agentId) {
|
|
2867
|
+
this.definitionAvailabilityOverrides.delete(agentId);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
#recordDefinitionStartupFailure(definition, error) {
|
|
2871
|
+
const reason = formatAgentStartupError(definition, error);
|
|
2872
|
+
if (!reason) return;
|
|
2873
|
+
this.#setDefinitionAvailabilityOverride(definition.id, {
|
|
2874
|
+
available: false,
|
|
2875
|
+
reason
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
getDefinitionAvailability(definition) {
|
|
2880
|
+
const baseAvailability = getDefinitionAvailability(
|
|
2881
|
+
definition,
|
|
2882
|
+
this.getAgentConfig(definition.id),
|
|
2883
|
+
this.availabilityProbes
|
|
2884
|
+
);
|
|
2885
|
+
if (!baseAvailability.available) {
|
|
2886
|
+
this.#clearDefinitionAvailabilityOverride(definition.id);
|
|
2887
|
+
return baseAvailability;
|
|
2888
|
+
}
|
|
2889
|
+
return this.#getDefinitionAvailabilityOverride(definition.id)
|
|
2890
|
+
|| baseAvailability;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
getAgentConfigVersion(agentId) {
|
|
2894
|
+
return this.agentConfigVersions.get(agentId) || 0;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
async ensureConfigsLoaded() {
|
|
2898
|
+
if (this.configLoaded) {
|
|
2899
|
+
return this.agentConfigs;
|
|
2900
|
+
}
|
|
2901
|
+
this.agentConfigs = await this.loadConfigs();
|
|
2902
|
+
this.configLoaded = true;
|
|
2903
|
+
return this.agentConfigs;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
getAgentConfig(agentId) {
|
|
2907
|
+
return this.agentConfigs?.[agentId] || { env: {} };
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
getSerializedAgentConfig(agentId) {
|
|
2911
|
+
return buildAgentConfigSummary(
|
|
2912
|
+
agentId,
|
|
2913
|
+
this.getAgentConfig(agentId)
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
async listAgentConfigs() {
|
|
2918
|
+
await this.ensureConfigsLoaded();
|
|
2919
|
+
const configs = {};
|
|
2920
|
+
for (const definition of this.definitions) {
|
|
2921
|
+
configs[definition.id] = this.getSerializedAgentConfig(
|
|
2922
|
+
definition.id
|
|
2923
|
+
);
|
|
2924
|
+
}
|
|
2925
|
+
return configs;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
async updateAgentConfig(agentId, nextConfig = {}) {
|
|
2929
|
+
await this.ensureConfigsLoaded();
|
|
2930
|
+
const definition = this.definitions.find((entry) => entry.id === agentId);
|
|
2931
|
+
if (!definition) {
|
|
2932
|
+
throw new Error('Unknown agent');
|
|
2933
|
+
}
|
|
2934
|
+
const currentConfig = this.getAgentConfig(agentId);
|
|
2935
|
+
const currentEnv = normalizeConfiguredEnv(agentId, currentConfig.env);
|
|
2936
|
+
const nextEnv = normalizeConfiguredEnv(agentId, nextConfig.env);
|
|
2937
|
+
const clearEnvKeys = Array.isArray(nextConfig.clearEnvKeys)
|
|
2938
|
+
? nextConfig.clearEnvKeys.filter((key) =>
|
|
2939
|
+
getAllowedAgentEnvKeys(agentId).includes(key)
|
|
2940
|
+
)
|
|
2941
|
+
: [];
|
|
2942
|
+
const mergedEnv = {
|
|
2943
|
+
...currentEnv,
|
|
2944
|
+
...nextEnv
|
|
2945
|
+
};
|
|
2946
|
+
for (const key of clearEnvKeys) {
|
|
2947
|
+
delete mergedEnv[key];
|
|
2948
|
+
}
|
|
2949
|
+
this.agentConfigs = {
|
|
2950
|
+
...this.agentConfigs,
|
|
2951
|
+
[agentId]: {
|
|
2952
|
+
env: mergedEnv
|
|
2953
|
+
}
|
|
2954
|
+
};
|
|
2955
|
+
this.#clearDefinitionAvailabilityOverride(agentId);
|
|
2956
|
+
this.agentConfigVersions.set(
|
|
2957
|
+
agentId,
|
|
2958
|
+
this.getAgentConfigVersion(agentId) + 1
|
|
2959
|
+
);
|
|
2960
|
+
await this.queuePersistence(() => this.saveConfigs(this.agentConfigs));
|
|
2961
|
+
return this.getSerializedAgentConfig(agentId);
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
async clearAgentConfig(agentId) {
|
|
2965
|
+
await this.ensureConfigsLoaded();
|
|
2966
|
+
const nextConfigs = { ...this.agentConfigs };
|
|
2967
|
+
delete nextConfigs[agentId];
|
|
2968
|
+
this.agentConfigs = nextConfigs;
|
|
2969
|
+
this.#clearDefinitionAvailabilityOverride(agentId);
|
|
2970
|
+
this.agentConfigVersions.set(
|
|
2971
|
+
agentId,
|
|
2972
|
+
this.getAgentConfigVersion(agentId) + 1
|
|
2973
|
+
);
|
|
2974
|
+
await this.queuePersistence(() => this.saveConfigs(this.agentConfigs));
|
|
2975
|
+
return this.getSerializedAgentConfig(agentId);
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
#applyRuntimeMetadataFallback(runtime, serialized) {
|
|
2979
|
+
if (!serialized || typeof serialized !== 'object') {
|
|
2980
|
+
return serialized;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
let availableModes = Array.isArray(serialized.availableModes)
|
|
2984
|
+
? serialized.availableModes
|
|
2985
|
+
: [];
|
|
2986
|
+
let availableCommands = Array.isArray(serialized.availableCommands)
|
|
2987
|
+
? serialized.availableCommands
|
|
2988
|
+
: [];
|
|
2989
|
+
let configOptions = Array.isArray(serialized.configOptions)
|
|
2990
|
+
? serialized.configOptions
|
|
2991
|
+
: [];
|
|
2992
|
+
|
|
2993
|
+
if (
|
|
2994
|
+
(
|
|
2995
|
+
availableModes.length > 0
|
|
2996
|
+
&& availableCommands.length > 0
|
|
2997
|
+
&& configOptions.length > 0
|
|
2998
|
+
)
|
|
2999
|
+
|| !(runtime?.tabs instanceof Map)
|
|
3000
|
+
) {
|
|
3001
|
+
return serialized;
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
for (const runtimeTab of runtime.tabs.values()) {
|
|
3005
|
+
if (!runtimeTab || typeof runtimeTab !== 'object') continue;
|
|
3006
|
+
if (
|
|
3007
|
+
availableModes.length === 0
|
|
3008
|
+
&& Array.isArray(runtimeTab.availableModes)
|
|
3009
|
+
&& runtimeTab.availableModes.length > 0
|
|
3010
|
+
) {
|
|
3011
|
+
availableModes = runtimeTab.availableModes;
|
|
3012
|
+
}
|
|
3013
|
+
if (
|
|
3014
|
+
availableCommands.length === 0
|
|
3015
|
+
&& Array.isArray(runtimeTab.availableCommands)
|
|
3016
|
+
&& runtimeTab.availableCommands.length > 0
|
|
3017
|
+
) {
|
|
3018
|
+
availableCommands = runtimeTab.availableCommands;
|
|
3019
|
+
}
|
|
3020
|
+
if (
|
|
3021
|
+
configOptions.length === 0
|
|
3022
|
+
&& Array.isArray(runtimeTab.configOptions)
|
|
3023
|
+
&& runtimeTab.configOptions.length > 0
|
|
3024
|
+
) {
|
|
3025
|
+
configOptions = runtimeTab.configOptions;
|
|
3026
|
+
}
|
|
3027
|
+
if (
|
|
3028
|
+
availableModes.length > 0
|
|
3029
|
+
&& availableCommands.length > 0
|
|
3030
|
+
&& configOptions.length > 0
|
|
3031
|
+
) {
|
|
3032
|
+
break;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
if (
|
|
3037
|
+
availableModes === serialized.availableModes
|
|
3038
|
+
&& availableCommands === serialized.availableCommands
|
|
3039
|
+
&& configOptions === serialized.configOptions
|
|
3040
|
+
) {
|
|
3041
|
+
return serialized;
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
return {
|
|
3045
|
+
...serialized,
|
|
3046
|
+
availableModes,
|
|
3047
|
+
availableCommands,
|
|
3048
|
+
configOptions
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
queuePersistence(operation) {
|
|
3053
|
+
this.persistenceChain = this.persistenceChain
|
|
3054
|
+
.catch(() => {})
|
|
3055
|
+
.then(operation);
|
|
3056
|
+
return this.persistenceChain;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
clearPendingTabPersistence() {
|
|
3060
|
+
if (this.persistTabsTimer) {
|
|
3061
|
+
clearTimeout(this.persistTabsTimer);
|
|
3062
|
+
this.persistTabsTimer = null;
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
schedulePersistTabs() {
|
|
3067
|
+
if (this.disposing || this.persistTabsTimer) {
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
this.persistTabsTimer = setTimeout(() => {
|
|
3071
|
+
this.persistTabsTimer = null;
|
|
3072
|
+
void this.persistTabs();
|
|
3073
|
+
}, this.transcriptPersistDelayMs);
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
getPersistedTabs() {
|
|
3077
|
+
return Array.from(this.tabs.values()).map((entry) => {
|
|
3078
|
+
const tab = entry.serialize();
|
|
3079
|
+
return {
|
|
3080
|
+
id: tab.id,
|
|
3081
|
+
agentId: tab.agentId,
|
|
3082
|
+
cwd: tab.cwd,
|
|
3083
|
+
acpSessionId: tab.acpSessionId,
|
|
3084
|
+
terminalSessionId: tab.terminalSessionId,
|
|
3085
|
+
createdAt: tab.createdAt,
|
|
3086
|
+
title: tab.title || '',
|
|
3087
|
+
currentModeId: tab.currentModeId || '',
|
|
3088
|
+
availableModes: Array.isArray(tab.availableModes)
|
|
3089
|
+
? cloneSerializable(tab.availableModes, [])
|
|
3090
|
+
: [],
|
|
3091
|
+
availableCommands: Array.isArray(tab.availableCommands)
|
|
3092
|
+
? cloneSerializable(tab.availableCommands, [])
|
|
3093
|
+
: [],
|
|
3094
|
+
configOptions: Array.isArray(tab.configOptions)
|
|
3095
|
+
? cloneSerializable(tab.configOptions, [])
|
|
3096
|
+
: [],
|
|
3097
|
+
messages: Array.isArray(tab.messages)
|
|
3098
|
+
? cloneSerializable(tab.messages, [])
|
|
3099
|
+
: [],
|
|
3100
|
+
toolCalls: Array.isArray(tab.toolCalls)
|
|
3101
|
+
? cloneSerializable(tab.toolCalls, [])
|
|
3102
|
+
: [],
|
|
3103
|
+
permissions: Array.isArray(tab.permissions)
|
|
3104
|
+
? cloneSerializable(tab.permissions, [])
|
|
3105
|
+
: [],
|
|
3106
|
+
plan: Array.isArray(tab.plan)
|
|
3107
|
+
? cloneSerializable(tab.plan, [])
|
|
3108
|
+
: [],
|
|
3109
|
+
usage: tab.usage ? cloneSerializable(tab.usage, null) : null,
|
|
3110
|
+
terminals: Array.isArray(tab.terminals)
|
|
3111
|
+
? cloneSerializable(tab.terminals, [])
|
|
3112
|
+
: []
|
|
3113
|
+
};
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
persistTabs() {
|
|
3118
|
+
this.clearPendingTabPersistence();
|
|
3119
|
+
return this.queuePersistence(() => this.saveTabs(this.getPersistedTabs()));
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
async listDefinitions() {
|
|
3123
|
+
await this.ensureConfigsLoaded();
|
|
3124
|
+
return this.definitions.map((definition) => {
|
|
3125
|
+
const availability = this.getDefinitionAvailability(definition);
|
|
3126
|
+
return {
|
|
3127
|
+
id: definition.id,
|
|
3128
|
+
label: definition.label,
|
|
3129
|
+
description: definition.description,
|
|
3130
|
+
websiteUrl: definition.websiteUrl || '',
|
|
3131
|
+
commandLabel: definition.commandLabel,
|
|
3132
|
+
setupCommandLabel: definition.setupCommandLabel || '',
|
|
3133
|
+
available: availability.available,
|
|
3134
|
+
reason: availability.reason,
|
|
3135
|
+
config: this.getSerializedAgentConfig(definition.id)
|
|
3136
|
+
};
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
async listState() {
|
|
3141
|
+
await this.ensureConfigsLoaded();
|
|
3142
|
+
return {
|
|
3143
|
+
restoring: this.restoring,
|
|
3144
|
+
definitions: await this.listDefinitions(),
|
|
3145
|
+
configs: await this.listAgentConfigs(),
|
|
3146
|
+
tabs: Array.from(this.tabs.values()).map((entry) => entry.serialize())
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
async createTab(options) {
|
|
3151
|
+
await this.ensureConfigsLoaded();
|
|
3152
|
+
const definition = this.definitions.find(
|
|
3153
|
+
(entry) => entry.id === options.agentId
|
|
3154
|
+
);
|
|
3155
|
+
if (!definition) {
|
|
3156
|
+
throw new Error('Unknown agent');
|
|
3157
|
+
}
|
|
3158
|
+
const availability = this.getDefinitionAvailability(definition);
|
|
3159
|
+
if (!availability.available) {
|
|
3160
|
+
throw new Error(availability.reason || 'Agent unavailable');
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
3164
|
+
const runtimeKey = makeRuntimeKey(definition.id, cwd);
|
|
3165
|
+
const runtimeStoreKey = makeRuntimeStoreKey(
|
|
3166
|
+
definition.id,
|
|
3167
|
+
cwd,
|
|
3168
|
+
this.getAgentConfigVersion(definition.id)
|
|
3169
|
+
);
|
|
3170
|
+
let runtimeEntry = this.runtimes.get(runtimeStoreKey);
|
|
3171
|
+
let createdRuntime = false;
|
|
3172
|
+
if (!runtimeEntry) {
|
|
3173
|
+
const runtime = this.runtimeFactory(definition, {
|
|
3174
|
+
cwd,
|
|
3175
|
+
idleTimeoutMs: this.idleTimeoutMs,
|
|
3176
|
+
runtimeStoreKey,
|
|
3177
|
+
terminalManager: this.terminalManager,
|
|
3178
|
+
env: mergeDefinitionEnv(
|
|
3179
|
+
definition,
|
|
3180
|
+
this.getAgentConfig(definition.id)
|
|
3181
|
+
)
|
|
3182
|
+
});
|
|
3183
|
+
runtimeEntry = {
|
|
3184
|
+
runtime,
|
|
3185
|
+
definition,
|
|
3186
|
+
runtimeKey,
|
|
3187
|
+
runtimeStoreKey
|
|
3188
|
+
};
|
|
3189
|
+
this.runtimes.set(runtimeStoreKey, runtimeEntry);
|
|
3190
|
+
createdRuntime = true;
|
|
3191
|
+
runtime.on('tab_dirty', () => {
|
|
3192
|
+
this.schedulePersistTabs();
|
|
3193
|
+
});
|
|
3194
|
+
runtime.on('runtime_exit', () => {
|
|
3195
|
+
if (this.disposing) return;
|
|
3196
|
+
for (const [tabId, tabEntry] of this.tabs.entries()) {
|
|
3197
|
+
if (tabEntry.runtime !== runtime) continue;
|
|
3198
|
+
this.tabs.delete(tabId);
|
|
3199
|
+
}
|
|
3200
|
+
this.runtimes.delete(runtimeStoreKey);
|
|
3201
|
+
void this.persistTabs();
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
const tabId = crypto.randomUUID();
|
|
3206
|
+
try {
|
|
3207
|
+
const rawSerialized = await runtimeEntry.runtime.createTab({
|
|
3208
|
+
id: tabId,
|
|
3209
|
+
cwd,
|
|
3210
|
+
terminalSessionId: options.terminalSessionId || '',
|
|
3211
|
+
modeId: options.modeId || ''
|
|
3212
|
+
});
|
|
3213
|
+
const serialized = this.#applyRuntimeMetadataFallback(
|
|
3214
|
+
runtimeEntry.runtime,
|
|
3215
|
+
rawSerialized
|
|
3216
|
+
);
|
|
3217
|
+
const tabEntry = {
|
|
3218
|
+
runtime: runtimeEntry.runtime,
|
|
3219
|
+
serialize: () => {
|
|
3220
|
+
const tab = runtimeEntry.runtime.tabs.get(tabId);
|
|
3221
|
+
const nextSerialized = tab
|
|
3222
|
+
? runtimeEntry.runtime.serializeTab(tab)
|
|
3223
|
+
: serialized;
|
|
3224
|
+
return this.#applyRuntimeMetadataFallback(
|
|
3225
|
+
runtimeEntry.runtime,
|
|
3226
|
+
nextSerialized
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
this.tabs.set(tabId, tabEntry);
|
|
3231
|
+
this.#clearDefinitionAvailabilityOverride(definition.id);
|
|
3232
|
+
await this.persistTabs();
|
|
3233
|
+
return tabEntry.serialize();
|
|
3234
|
+
} catch (error) {
|
|
3235
|
+
const shouldDisposeRuntime = createdRuntime
|
|
3236
|
+
|| runtimeEntry.runtime.tabs.size === 0;
|
|
3237
|
+
if (shouldDisposeRuntime) {
|
|
3238
|
+
this.runtimes.delete(runtimeStoreKey);
|
|
3239
|
+
await runtimeEntry.runtime.dispose().catch(() => {});
|
|
3240
|
+
}
|
|
3241
|
+
this.#recordDefinitionStartupFailure(definition, error);
|
|
3242
|
+
throw new Error(formatAgentStartupError(definition, error));
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
async restoreTabs(validTerminalSessionIds = new Set()) {
|
|
3247
|
+
await this.ensureConfigsLoaded();
|
|
3248
|
+
const entries = await this.loadTabs();
|
|
3249
|
+
let changed = false;
|
|
3250
|
+
|
|
3251
|
+
for (const meta of entries) {
|
|
3252
|
+
if (
|
|
3253
|
+
meta.terminalSessionId
|
|
3254
|
+
&& !validTerminalSessionIds.has(meta.terminalSessionId)
|
|
3255
|
+
) {
|
|
3256
|
+
changed = true;
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
const definition = this.definitions.find(
|
|
3261
|
+
(entry) => entry.id === meta.agentId
|
|
3262
|
+
);
|
|
3263
|
+
if (!definition) {
|
|
3264
|
+
changed = true;
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
const agentConfig = this.getAgentConfig(definition.id);
|
|
3269
|
+
const availability = this.getDefinitionAvailability(definition);
|
|
3270
|
+
if (!availability.available) {
|
|
3271
|
+
changed = true;
|
|
3272
|
+
continue;
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
const cwd = path.resolve(meta.cwd || process.cwd());
|
|
3276
|
+
const runtimeKey = makeRuntimeKey(definition.id, cwd);
|
|
3277
|
+
const runtimeStoreKey = makeRuntimeStoreKey(
|
|
3278
|
+
definition.id,
|
|
3279
|
+
cwd,
|
|
3280
|
+
this.getAgentConfigVersion(definition.id)
|
|
3281
|
+
);
|
|
3282
|
+
let runtimeEntry = this.runtimes.get(runtimeStoreKey);
|
|
3283
|
+
if (!runtimeEntry) {
|
|
3284
|
+
const runtime = this.runtimeFactory(definition, {
|
|
3285
|
+
cwd,
|
|
3286
|
+
idleTimeoutMs: this.idleTimeoutMs,
|
|
3287
|
+
runtimeStoreKey,
|
|
3288
|
+
terminalManager: this.terminalManager,
|
|
3289
|
+
env: mergeDefinitionEnv(definition, agentConfig)
|
|
3290
|
+
});
|
|
3291
|
+
runtimeEntry = {
|
|
3292
|
+
runtime,
|
|
3293
|
+
definition,
|
|
3294
|
+
runtimeKey,
|
|
3295
|
+
runtimeStoreKey
|
|
3296
|
+
};
|
|
3297
|
+
this.runtimes.set(runtimeStoreKey, runtimeEntry);
|
|
3298
|
+
runtime.on('tab_dirty', () => {
|
|
3299
|
+
this.schedulePersistTabs();
|
|
3300
|
+
});
|
|
3301
|
+
runtime.on('runtime_exit', () => {
|
|
3302
|
+
if (this.disposing) return;
|
|
3303
|
+
for (const [tabId, tabEntry] of this.tabs.entries()) {
|
|
3304
|
+
if (tabEntry.runtime !== runtime) continue;
|
|
3305
|
+
this.tabs.delete(tabId);
|
|
3306
|
+
}
|
|
3307
|
+
this.runtimes.delete(runtimeStoreKey);
|
|
3308
|
+
void this.persistTabs();
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
try {
|
|
3313
|
+
const rawSerialized = await runtimeEntry.runtime.restoreTab({
|
|
3314
|
+
...meta,
|
|
3315
|
+
cwd
|
|
3316
|
+
});
|
|
3317
|
+
const serialized = this.#applyRuntimeMetadataFallback(
|
|
3318
|
+
runtimeEntry.runtime,
|
|
3319
|
+
rawSerialized
|
|
3320
|
+
);
|
|
3321
|
+
this.tabs.set(meta.id, {
|
|
3322
|
+
runtime: runtimeEntry.runtime,
|
|
3323
|
+
serialize: () => {
|
|
3324
|
+
const tab = runtimeEntry.runtime.tabs.get(meta.id);
|
|
3325
|
+
const nextSerialized = tab
|
|
3326
|
+
? runtimeEntry.runtime.serializeTab(tab)
|
|
3327
|
+
: serialized;
|
|
3328
|
+
return this.#applyRuntimeMetadataFallback(
|
|
3329
|
+
runtimeEntry.runtime,
|
|
3330
|
+
nextSerialized
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3333
|
+
});
|
|
3334
|
+
this.#clearDefinitionAvailabilityOverride(definition.id);
|
|
3335
|
+
} catch (error) {
|
|
3336
|
+
changed = true;
|
|
3337
|
+
console.warn(
|
|
3338
|
+
`[ACP] Failed to restore agent tab ${meta.id}:`,
|
|
3339
|
+
error?.message || error
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
if (changed) {
|
|
3345
|
+
await this.persistTabs();
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
attachSocket(tabId, socket) {
|
|
3350
|
+
const tabEntry = this.tabs.get(tabId);
|
|
3351
|
+
if (!tabEntry) {
|
|
3352
|
+
socket.close();
|
|
3353
|
+
return false;
|
|
3354
|
+
}
|
|
3355
|
+
return tabEntry.runtime.attachSocket(tabId, socket);
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
async sendPrompt(tabId, text, attachments = []) {
|
|
3359
|
+
const tabEntry = this.tabs.get(tabId);
|
|
3360
|
+
if (!tabEntry) {
|
|
3361
|
+
throw new Error('Agent tab not found');
|
|
3362
|
+
}
|
|
3363
|
+
await tabEntry.runtime.sendPrompt(tabId, text, attachments);
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
async cancel(tabId) {
|
|
3367
|
+
const tabEntry = this.tabs.get(tabId);
|
|
3368
|
+
if (!tabEntry) {
|
|
3369
|
+
throw new Error('Agent tab not found');
|
|
3370
|
+
}
|
|
3371
|
+
await tabEntry.runtime.cancel(tabId);
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
async setMode(tabId, modeId) {
|
|
3375
|
+
const tabEntry = this.tabs.get(tabId);
|
|
3376
|
+
if (!tabEntry) {
|
|
3377
|
+
throw new Error('Agent tab not found');
|
|
3378
|
+
}
|
|
3379
|
+
return await tabEntry.runtime.setMode(tabId, modeId);
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
async setConfigOption(tabId, configId, valueId) {
|
|
3383
|
+
const tabEntry = this.tabs.get(tabId);
|
|
3384
|
+
if (!tabEntry) {
|
|
3385
|
+
throw new Error('Agent tab not found');
|
|
3386
|
+
}
|
|
3387
|
+
return await tabEntry.runtime.setConfigOption(tabId, configId, valueId);
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
async resolvePermission(tabId, permissionId, optionId) {
|
|
3391
|
+
const tabEntry = this.tabs.get(tabId);
|
|
3392
|
+
if (!tabEntry) {
|
|
3393
|
+
throw new Error('Agent tab not found');
|
|
3394
|
+
}
|
|
3395
|
+
await tabEntry.runtime.resolvePermission(tabId, permissionId, optionId);
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
async closeTab(tabId) {
|
|
3399
|
+
const tabEntry = this.tabs.get(tabId);
|
|
3400
|
+
if (!tabEntry) return;
|
|
3401
|
+
await tabEntry.runtime.closeTab(tabId);
|
|
3402
|
+
this.tabs.delete(tabId);
|
|
3403
|
+
await this.persistTabs();
|
|
3404
|
+
tabEntry.runtime.scheduleIdleShutdown(async () => {
|
|
3405
|
+
if (
|
|
3406
|
+
Array.from(this.tabs.values()).some(
|
|
3407
|
+
(entry) => entry.runtime === tabEntry.runtime
|
|
3408
|
+
)
|
|
3409
|
+
) {
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
await tabEntry.runtime.dispose();
|
|
3413
|
+
this.runtimes.delete(
|
|
3414
|
+
tabEntry.runtime.runtimeStoreKey || tabEntry.runtime.runtimeKey
|
|
3415
|
+
);
|
|
3416
|
+
});
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
async closeTabsForTerminalSession(terminalSessionId) {
|
|
3420
|
+
const ids = [];
|
|
3421
|
+
for (const [tabId, entry] of this.tabs.entries()) {
|
|
3422
|
+
const tab = entry.runtime.tabs.get(tabId);
|
|
3423
|
+
if (!tab) continue;
|
|
3424
|
+
if (tab.terminalSessionId === terminalSessionId) {
|
|
3425
|
+
ids.push(tabId);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
for (const id of ids) {
|
|
3429
|
+
await this.closeTab(id);
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
async releaseManagedTerminalSession(
|
|
3434
|
+
terminalSessionId,
|
|
3435
|
+
options = {}
|
|
3436
|
+
) {
|
|
3437
|
+
const targetSessionId = String(terminalSessionId || '').trim();
|
|
3438
|
+
if (!targetSessionId) return false;
|
|
3439
|
+
for (const runtimeEntry of this.runtimes.values()) {
|
|
3440
|
+
if (
|
|
3441
|
+
typeof runtimeEntry.runtime.releaseManagedTerminalSession
|
|
3442
|
+
!== 'function'
|
|
3443
|
+
) {
|
|
3444
|
+
continue;
|
|
3445
|
+
}
|
|
3446
|
+
const released = await runtimeEntry.runtime
|
|
3447
|
+
.releaseManagedTerminalSession(targetSessionId, options);
|
|
3448
|
+
if (released) {
|
|
3449
|
+
return true;
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
return false;
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
async dispose({ preserveTabs = true } = {}) {
|
|
3456
|
+
this.disposing = true;
|
|
3457
|
+
this.clearPendingTabPersistence();
|
|
3458
|
+
if (preserveTabs) {
|
|
3459
|
+
await this.persistTabs();
|
|
3460
|
+
} else {
|
|
3461
|
+
await this.saveTabs([]);
|
|
3462
|
+
}
|
|
3463
|
+
for (const runtimeEntry of this.runtimes.values()) {
|
|
3464
|
+
await runtimeEntry.runtime.dispose();
|
|
3465
|
+
}
|
|
3466
|
+
this.runtimes.clear();
|
|
3467
|
+
this.tabs.clear();
|
|
3468
|
+
}
|
|
3469
|
+
}
|